Back to Course |
Testing in Laravel 11 For Beginners

Two More Tests with TDD

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.


Empty Table Test

So, first, the test. Yes, before writing the feature code, remember.

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.


New Test: Table Shows Products

Let's add another test method.

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.

So yeah, this is the essential part of TDD: just the opposite approach and mindset shift to when the tests are written. It's your personal preference whether to use TDD, but I don't see it widely adopted in the Laravel community, specifically.


Conclusion of the Course: What's Next?

So, this is where we end our course for beginners. The goal was to get you to start testing Laravel applications. I think you're ready now.

Now, of course, the topic of testing is much deeper, with various examples of what to test, extra tools, and syntax options.

For that, I have a separate course Advanced Laravel Testing. It will show you things like how to test 3rd party APIs, assert file downloads, test Artisan commands, and much more. So, when you get up to speed with simple tests, I recommend going through those advanced topics.