Let's continue our TDD journey. The next feature for the products list will be that the products page loads, and if there are no products, it shows an empty table with the text No products found.
if there are products, they are shown in the table. Similar to what we have done already.
So, first, the test.
tests/Feature/ProductsTest.php:
class ProductsTest extends TestCase{ use RefreshDatabase; // ... public function test_homepage_contains_empty_table(): void { $user = User::factory()->create(); $response = $this->actingAs($user)->get('/products'); $response->assertStatus(200); $response->assertSee(__('No products found')); }}
Now, let's run the test.
The GET request works, and it returns status 200. This part we made in the last lesson. Now, the error is that we don't see the message when the table is empty.
In the Controller, the index method doesn't contain any code. Now, we write the code so that tests would pass. If you want to be consistent in our TDD step-by-step effort, first, we need to create a page without any data because we don't have the products DB structure yet. No Models, no Migrations.
app/Http/Controllers/ProductController.php:
use Illuminate\Contracts\View\View; class ProductController extends Controller{ public function index(): View { return view('products.index'); } // ...}
resources/views/products/index.blade.php:
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> {{ __('Products') }} </h2> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="overflow-hidden overflow-x-auto p-6 bg-white border-b border-gray-200"> <div class="min-w-full align-middle"> <table class="min-w-full divide-y divide-gray-200 border"> <thead> <tr> <th class="px-6 py-3 bg-gray-50 text-left"> <span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Name</span> </th> <th class="px-6 py-3 bg-gray-50 text-left"> <span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Price</span> </th> </tr> </thead> <tbody class="bg-white divide-y divide-gray-200 divide-solid"> <tr class="bg-white"> <td colspan="2" class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900"> {{ __('No products found') }} </td> </tr> </tbody> </table> </div> </div> </div> </div> </div></x-app-layout>
This is how it would look in the browser.
Now, let's re-launch the tests. And they are green.
Let's add the test.
tests/Feature/ProductsTest.php:
class ProductsTest extends TestCase{ use RefreshDatabase; // ... public function test_homepage_contains_non_empty_table(): void { $user = User::factory()->create(); $product = Product::create([ 'name' => 'Product 1', 'price' => 123, ]); $response = $this->actingAs($user)->get('/products'); $response->assertStatus(200); $response->assertDontSee(__('No products found')); $response->assertSee('Product 1'); $response->assertViewHas('products', function (Collection $collection) use ($product) { return $collection->contains($product); }); }}
And immediately, we can see there is no DB structure for the product.
So, we need to create a Model, Migration, and Factory.
php artisan make:model Product -mf
Now we can import the Product Model test, and then we should get a different error.
tests/Feature/ProductsTest.php:
use App\Models\Product; class ProductsTest extends TestCase{ // ...}
We are trying to create a product with a name and price, but those fields still need to be created. We need to add Migrations and fillable fields in the Model.
database/migrations/xxx_create_products_table.php:
public function up(): void{ Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('name'); $table->integer('price'); $table->timestamps(); });}
app/Models/Product.php:
class Product extends Model{ use HasFactory; protected $fillable = [ 'name', 'price', ]; }
Now the error is on the line where we shouldn't see the No products found
text. The next thing to fix is to pass data into the View.
app/Http/Controllers/ProductController.php:
use App\Models\Product;use Illuminate\Contracts\View\View; class ProductController extends Controller{ public function index(): View { $products = Product::all(); return view('products.index', compact('products')); } // ...}
resources/views/products/index.blade.php:
<x-app-layout> <x-slot name="header"> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> {{ __('Products') }} </h2> </x-slot> <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="overflow-hidden overflow-x-auto p-6 bg-white border-b border-gray-200"> <div class="min-w-full align-middle"> <table class="min-w-full divide-y divide-gray-200 border"> <thead> <tr> <th class="px-6 py-3 bg-gray-50 text-left"> <span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Name</span> </th> <th class="px-6 py-3 bg-gray-50 text-left"> <span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Price (USD)</span> </th> </tr> </thead> <tbody class="bg-white divide-y divide-gray-200 divide-solid"> @forelse($products as $product) <tr class="bg-white"> <td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900"> {{ $product->name }} </td> <td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900"> {{ $product->price }} </td> </tr> @empty <tr class="bg-white"> <td colspan="2" class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900"> {{ __('No products found') }} </td> </tr> @endforelse {{-- [tl! ++ --}} </tbody> </table> </div> </div> </div> </div> </div></x-app-layout>
And now, again, we have green tests.
Also, when we run tests, we run the older tests to make sure nothing is broken.