Why and What to Test

A test is a small program that fails when your code is wrong, which is why it can tell you about a bug before a user does.

Syntaxvendor/bin/phpunit [--testdox] [--filter <pattern>] <directory>

You already check your code. You load the page, type something in, and look at the result. A test is that same check written down as code, so the computer repeats it every time you change anything.

A test does two things. It runs a piece of your code with a known input, and it compares the result to the answer you expect. If the two differ, the test fails and says which value disagreed. There is nothing more to it than that, as the runnable example shows.

What to test

Not everything. Test the code where being wrong costs money or data: prices, totals, permissions, validation rules, anything that decides. Test every bug you fix, so the same bug cannot come back quietly. Skip tests that only restate the language, such as checking that a property returns what the constructor was handed.

Installing the runner

In practice you do not hand-write the comparison. PHPUnit is the standard test runner for PHP. Add it as a development-only dependency, so it never ships to production:

composer require --dev phpunit/phpunit

Then run vendor/bin/phpunit tests. PHPUnit 12 requires PHP 8.3 or newer.

Reading a failure

A clean run ends with OK (3 tests, 3 assertions). A broken one ends with FAILURES! and Tests: 3, Assertions: 3, Failures: 1., and above that it prints the failing test's name, the expected and actual values, and the file and line. It also exits with a non-zero status, which is how a deploy script knows to stop.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Lock in a pricing or tax rule so a later refactor cannot change a total without someone noticing.
  • Reproduce a reported bug as a failing test first, then fix it and keep the test as a regression check.
  • Run the suite in a deploy script and abort the release when the exit status is not zero.
  • Replace the manual click-through you repeat after every change to the checkout.

More examples

The same checks as a PHPUnit test

The hand-rolled loop from the primary example, rewritten the way you will actually write it. PHPUnit supplies the comparison, the pass and fail counting, and the report. You supply the inputs and the expected answers. This file is not runnable in the sandbox because PHPUnit is installed by Composer, which the sandbox does not have.

Example · php
<?php
declare(strict_types=1);

// tests/CartTotalTest.php
use PHPUnit\Framework\TestCase;

final class CartTotalTest extends TestCase
{
    public function test_an_empty_cart_totals_zero(): void
    {
        self::assertSame(0, cart_total([]));
    }

    public function test_one_line_is_price_times_quantity(): void
    {
        self::assertSame(500, cart_total([['price' => 250, 'qty' => 2]]));
    }

    public function test_two_lines_are_added_together(): void
    {
        $lines = [
            ['price' => 250, 'qty' => 2],
            ['price' => 100, 'qty' => 1],
        ];

        self::assertSame(650, cart_total($lines));
    }
}

What that failing run prints

Read it from the bottom. The counts tell you the scale of the damage. Above them, each numbered block names the test, states the comparison that failed, and gives the file and line to open. The dots and the F on the progress line are one character per test, in the order they ran.

Example · text
PHPUnit 12.5.31 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.5.10

..F                                                                 3 / 3 (100%)

There was 1 failure:

1) CartTotalTest::test_two_lines_are_added_together
Failed asserting that 600 is identical to 650.

/app/tests/CartTotalTest.php:24

FAILURES!
Tests: 3, Assertions: 3, Failures: 1.

Telling Composer and PHPUnit where things live

Two files, written once per project. The autoload-dev block keeps test classes out of the production autoloader. The phpunit.xml bootstrap line points at Composer's autoloader so test files never need require statements, and the testsuites block means you can type vendor/bin/phpunit with no arguments.

Example · text
// composer.json
{
    "require-dev": {
        "phpunit/phpunit": "^12.5"
    },
    "autoload": {
        "psr-4": { "App\\": "src/" }
    },
    "autoload-dev": {
        "psr-4": { "App\\Tests\\": "tests/" }
    }
}

// phpunit.xml, generated by: vendor/bin/phpunit --generate-configuration
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true" cacheDirectory=".phpunit.cache">
    <testsuites>
        <testsuite name="unit">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

Discussion

  • Be the first to comment on this lesson.