Courses

Testing in Laravel 11 For Beginners

First Test: Is Product Table Empty?

Now, let's start writing tests for a simple but real project that would list the products.

empty products table


Initial Project

First, we will make a simple page listing the products. For the frontend, we will be using Laravel Breeze. So first, the Model and Migration.

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
{
protected $fillable = [
'name',
'price',
];
}

Next, the Controller, View, and Route.

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</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">
${{ number_format($product->price, 2) }}
</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
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</x-app-layout>

routes/web.php:

use App\Http\Controllers\ProductController;
 
Route::get('/', function () {
return view('home');
})->name('home');
 
Route::resource('products', ProductController::class);
 
// ...

Notice: we are using Resource for Route because later, we will add other methods like create(), store(), etc.

After visiting /products, we should see an empty table.

empty products table

Now, let's write tests for this page.


Writing Tests

For the first tests, we will check two scenarios:

  • Whether the page contains the text No products found when there are no records
  • And the opposite: whether this text isn't rendered when the record in the DB exists.

Reminder: We will use Pest PHP testing framework in this course. At the end of the lesson, we will see how the same could be achieved using PHPUnit.

We can create a new test class using an artisan command. It will generate a feature test.

php artisan make:test ProductsTest

In this test, we need to get the /products URL and, from the response, assert if we see the No products found text. The test's name will be homepage contains empty table so that it will be clear what it tests.

tests/Feature/ProductsTest.php:

use function Pest\Laravel\get;
 
test('homepage contains empty table', function () {
get('/products')
->assertStatus(200)
->assertSee(__('No products found'));
});

Instead of using typical $this->get() syntax we used the get() function from the pestphp/pest-plugin-laravel plugin.

If we run the tests, we will see this new test passed successfully.

products table empty test

Let's add another test to assert that the No products found text isn't rendered when the products DB is not empty. Instead of assertSee(), the opposite assertDontSee() method is used.

For now, we will create the product manually inside the test, and later, we will see how to do it automatically in a separate testing database.

tests/Feature/ProductsTest.php:

use App\Models\Product;
use function Pest\Laravel\get;
 
test('homepage contains empty table', function () {
get('/products')
->assertStatus(200)
->assertSee(__('No products found'));
});
 
test('homepage contains non empty table', function () {
Product::create([
'name' => 'Product 1',
'price' => 123,
]);
 
get('/products')
->assertStatus(200)
->assertDontSee(__('No products found'));
});

Run the tests.

products table isn't empty test

So, this is how you write tests to see if some text is/isn't present on the page.


PHPUnit examples

To create a new PHPUnit test class, the --phpunit parameter must be added to the make:test artisan command.

php artisan make:test ProductsTest --phpunit

tests/Feature/ProductsTest.php:

class ProductsTest extends TestCase
{
public function test_homepage_contains_empty_table(): void
{
$response = $this->get('/products');
 
$response->assertStatus(200);
$response->assertSee(__('No products found'));
}
}

The second test to assert that the No products found text isn't rendered.

tests/Feature/ProductsTest.php:

use App\Models\Product;
 
class ProductsTest extends TestCase
{
public function test_homepage_contains_empty_table(): void
{
$response = $this->get('/products');
 
$response->assertStatus(200);
$response->assertSee(__('No products found'));
}
 
public function test_homepage_contains_non_empty_table(): void
{
Product::create([
'name' => 'Product 1',
'price' => 123,
]);
 
$response = $this->get('/products');
 
$response->assertStatus(200);
$response->assertDontSee(__('No products found'));
}
}

Run the tests.

products table isn't empty test


So, we have covered another simple example: simulated both cases in the products table when it is and isn't empty.

avatar

As soon as I run the test, I get an error message: Attempt to read property "name" on null. I get a status code 500 and my user is deleted from the database when I run the test. I have tried it with sqlite and mysql and also with Laravel 10.x and 11. Unfortunately I can't find the solution to the problem. Does anyone have the same problem or a solution?

Greetings Mario

avatar

Open storage/logs/laravel.log file and you should see a full stack trace of the error. Once you see that, you should be able to pin-point the broken code piece by looking at the files list.

While doing that, can you you also write the test that fails? And attach the laravel.log stacktrace for that error (tip: delete all the file contents, run test, it should only have 1 failure inside instead of many)

avatar

I have the same problem. Looks like before or after I run the tests my Users table in DB is reset - not sure when it happens. But I passed test with those changes:

public function test_homepage_contains_empty_table(): void {

    $user = User::factory()->create();  // Create a new user
    $response = $this->actingAs($user)->get('/products'); // Authenticate the user before sending the request

    $response = $this->get('/products');
    $response->assertStatus(200);
    $response->assertSee(__('No products found'));
}
avatar

Hello Mantas, with the code you provided, the test works for me too. How did you come up with your solution? @Modestas, there is so much in the log file that I don't even know what to look for.

avatar

The 500 error related to users is seen because we aren't first logging in, so the navigation blade doesn't have a $name to display. Which makes sense since the test is just simulating opening a browser to /products. If you do that before logging in you will get the same 500 error.

Commenting that out in layouts/app.blade.php worked for me.

avatar

@dabms, the problem is because products/index.blade.php uses

This layout carries the logged in user information. The error occurs when trying to load the user name information.

The solution that the colleague provided for the test is to create the user and perform authentication (actingAs) and then call the products route.

avatar

Why is my Laravel test for the homepage, which should contain an empty table, failing with a status code of 302 instead of the expected 200? FAILED Tests\Feature\ProductsTest > homepage contains empty table Expected response status code [200] but received 302. Failed asserting that 302 is identical to 200.

at tests/Feature/ProductsTest.php:6 2▕ use function Pest\Laravel\get; 3▕ 4▕ test('homepage contains empty table', function () { 5▕ get('/products') ➜ 6▕ ->assertStatus(200) 7▕ ->assertSee(__('No products found')); 8▕ }); 9▕

avatar

This is probably because you don't have a user assigned to your test session. You need to have:

actingAs($user)

In your test. The 302 is a redirect, which is most likely a /login page in your case

avatar

yes i have added thanks. use function Pest\Laravel\get; use App\Models\User; use function Pest\Laravel{actingAs};

test('homepage contains empty table', function () { $user = User::factory()->create();

actingAs($user)
    ->get('/products')
    ->assertStatus(200)
    ->assertSee(__('No products found'));

});

avatar

Does assertSee() also work the same way if I use api only?