Automated Testing (Pest & PHPUnit)
Write tests that prove your app works — and keep proving it as you change code. A standout job-ready skill.
What you will learn
- Explain feature vs unit tests
- Write a feature test that hits a route and checks the response
- Use factories and a test database safely
What is automated testing?
A test is code that checks your other code does the right thing — automatically. Instead of clicking through your app by hand after every change (and forgetting to check something), you write tests once and run them with a single command. If a future change breaks something, a test fails and tells you exactly what and where. Employers love this because it proves an app keeps working as it grows.
Laravel ships with two test styles. PHPUnit is the long-standing standard; Pest is a newer, friendlier syntax built on top of it. We will show Pest because it reads almost like plain English, and note the PHPUnit equivalent.
There are two kinds of test you will write most:
| Type | What it checks | Example |
|---|---|---|
| Feature test | A whole slice of the app, through a route | Visiting /products shows the product list |
| Unit test | One small piece in isolation | A price-formatting method returns "₹499.00" |
A feature test, line by line
Generate a test with artisan:
php artisan make:test ProductListTest --pestThis creates tests/Feature/ProductListTest.php. Now the test itself: it creates some data, visits a page, and checks what comes back. Read the comments — each line is a step:
// tests/Feature/ProductListTest.php
use App\Models\Product;
it('shows the list of products', function () {
// 1. Arrange: put three fake products in the test database
Product::factory()->count(3)->create(['name' => 'Test Keyboard']);
// 2. Act: visit the products page
$response = $this->get('/products');
// 3. Assert: the page loaded and shows our product
$response->assertStatus(200);
$response->assertSee('Test Keyboard');
});This follows the classic Arrange–Act–Assert pattern. Arrange: Product::factory()->count(3)->create(...) uses the factory you learned to put three known products in the test database. Act: $this->get('/products') pretends to be a browser visiting that URL. Assert: assertStatus(200) checks the page loaded successfully (200 means "OK"), and assertSee('Test Keyboard') checks that text actually appears on the page. If either assertion is false, the test fails.
Note: PHPUnit equivalent: the same test in PHPUnit is a class with a public function test_shows_products() method using $this->get(...)->assertStatus(200)->assertSee(...). Pest just trims the ceremony — the assertions are identical.
Run the tests
One command runs every test in your app:
php artisan testNote: Output: PASS Tests\Feature\ProductListTest ✓ it shows the list of products 0.21s Tests: 1 passed (2 assertions) Duration: 0.34s A green PASS means the route works and shows the product. Change the controller to break it and you would see a red FAIL pinpointing the line.
The test database — never touch real data
Tests create and delete records, so they must not run against your real database. Laravel uses a separate test database, and a single trait resets it between tests so each test starts clean:
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class); // wipe & rebuild the DB before each testRefreshDatabase migrates a fresh, empty database before each test and rolls it back after, so tests never interfere with each other or with your real data. This is why factories matter — each test arranges exactly the records it needs.
How a test run works, in order:
- You run
php artisan test. - For each test,
RefreshDatabasegives a clean, empty test database. - Arrange: the test creates the records it needs with factories.
- Act: it visits a route or calls a method.
- Assert: it checks the response (status code, visible text, database rows) — pass or fail.
Tip: Useful assertions: assertStatus(200), assertSee('text'), assertRedirect('/products'), and assertDatabaseHas('products', ['name' => 'Keyboard']) to confirm a row was actually saved. Start by testing your most important routes — a few feature tests catch the bugs that hurt most.
Q. In the Arrange–Act–Assert pattern, what does the “Assert” step do?
✍️ Practice
- Write a feature test that visits an index route, asserts status 200, and asserts a seeded record’s name is visible.
- Add
uses(RefreshDatabase::class)and runphp artisan test.
🏠 Homework
- Write feature tests for the create and delete actions of a CRUD resource, using
assertDatabaseHasandassertDatabaseMissingto confirm the database changed.