Courses

Testing in Laravel 9 For Beginners: PHPUnit, Pest, TDD

Our First Test: Products Table - Empty or Not?

Now, let's start writing tests for a simple but real project. For the first tests, we will check if the page contains the text No products found when there are no records and if this text isn't rendered where the record in the DB exists.


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);
 
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
 
require __DIR__.'/auth.php';

We are using Resource for Route because later we will add other methods like create.

After visiting /products, we should see an empty table. Now, let's write tests.

empty products table


Writing Tests

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 itself name will be test_homepage_contains_empty_table so that it will be clear what it tests.

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'));
}
}

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

products table empty test

In later lessons, we will properly test when the table isn't empty. But for now, add a DB record manually. We should see one product on the /products page now.

test-product-table

Now, our tests will fail because we have at least one record in the DB, and they are shown in the table.

products table test fails

Let's add another test to assert that the No products found text isn't rendered. Instead of assertSee, the assertDontSee method is used.

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

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'));
}
}

Now, truncate the products table manually and re-run the tests.

products table isn't empty test

So, we simulated both cases in the products table when it is and isn't empty.


Repository commit for this lesson

Previous: Default Tests: How They Work and How to Launch Them
avatar
LUIS DAVID CARDENAS BUCIO

I want to mention that while I was writing my own tests, they were not running. The filename must end with 'Test,' and the functions within the class must begin with 'test' (respecting the lowercase and uppercase)

avatar
You can use Markdown
avatar

my tests keep failing and i get this massage "Expected response status code [200] but received 500. Failed asserting that 500 is identical to 200.", also when i register from breeze dashboard and i go to '/products' it deletes my new accoute from the database and if i update the page i get this massage "Attempt to read property "name" on null" (this massage desipear when i register and login to the account but when im at the '/produtc' page and refresh it, it happens again and the accoute get deleted and i get the massage agian)

avatar

Ok its simply due to NavBar trquesting Auth so when i put the navbar.blade as comment it worked

avatar
Артем Гаврюк

If you have a "Expected response status code [200] but received 500"

just make your responses like this

$user = User::factory()->create();
$response = $this
    ->actingAs($user)
    ->get('/products');
avatar
You can use Markdown
avatar

This is my error message:

SQLSTATE[42S02]: Base table or view not found: 1146 Table 'testing.products' doesn't exist (Connection: mysql, SQL: insert into products (name, price, updated_at, created_at) values (Product 2, 12, 2024-04-24 19:44:59, 2024-04-24 19:44:59))

avatar

Did you run php artisan migrate after you created the migrations file?

avatar

i did but forcsome reason artisan wants to test testing database instead of laravel db. Migration populated laravel db

avatar
You can use Markdown
avatar
You can use Markdown