Going DeeperPro· 45 min read

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:

TypeWhat it checksExample
Feature testA whole slice of the app, through a routeVisiting /products shows the product list
Unit testOne small piece in isolationA price-formatting method returns "₹499.00"

A feature test, line by line

Generate a test with artisan:

Generate a Pest feature test
php artisan make:test ProductListTest --pest

This 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:

Arrange → Act → Assert: the shape of every test
// 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:

Run the whole test suite
php artisan test

Note: 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:

Reset the database before every test for a clean slate
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);   // wipe & rebuild the DB before each test

RefreshDatabase 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:

  1. You run php artisan test.
  2. For each test, RefreshDatabase gives a clean, empty test database.
  3. Arrange: the test creates the records it needs with factories.
  4. Act: it visits a route or calls a method.
  5. 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?

Answer: Arrange sets up data (factories), Act performs the action (e.g. get a route), and Assert checks the outcome with calls like assertStatus(200) and assertSee(). RefreshDatabase resets the DB between tests.

✍️ Practice

  1. Write a feature test that visits an index route, asserts status 200, and asserts a seeded record’s name is visible.
  2. Add uses(RefreshDatabase::class) and run php artisan test.

🏠 Homework

  1. Write feature tests for the create and delete actions of a CRUD resource, using assertDatabaseHas and assertDatabaseMissing to confirm the database changed.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →