code quality and tooling

Automated Testing Basics

Automated tests run code and check whether the result matches expected behaviour. They are not a replacement for thinking, reviews, static analysis, or manual exploration, but they give a team repeatable proof that important behaviour still works.

The beginner goal is to learn the shape of a test: arrange known input, run the code, compare the actual result with the expected result, and fail loudly when the result is wrong.

A tiny test without a framework

PHP example
<?php

declare(strict_types=1);

function formatPrice(int $pennies): string
{
    return 'GBP ' . number_format($pennies / 100, 2);
}

$actual = formatPrice(1250);
$expected = 'GBP 12.50';

if ($actual !== $expected) {
    throw new RuntimeException("Expected {$expected}, got {$actual}");
}

echo 'Price formatting works' . PHP_EOL;

// Prints:
// Price formatting works

This is not a replacement for PHPUnit, but it shows the core idea. A test should be repeatable and specific.

Happy paths and edge cases

A happy path checks normal expected behaviour. An edge case checks a boundary or unusual input.

PHP example
<?php

declare(strict_types=1);

function discountForSubtotal(int $subtotal): int
{
    if ($subtotal >= 5000) {
        return 500;
    }

    return 0;
}

var_dump(discountForSubtotal(4999));
var_dump(discountForSubtotal(5000));
var_dump(discountForSubtotal(5001));

// Prints:
// int(0)
// int(500)
// int(500)

The boundary value 5000 matters because that is where the rule changes.

PHPUnit shape

In real PHP projects, PHPUnit is common. A test class usually extends TestCase and contains methods that make assertions.

PHP example
<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

final class DiscountTest extends TestCase
{
    public function testAppliesDiscountAtBoundary(): void
    {
        self::assertSame(500, discountForSubtotal(5000));
    }

    public function testDoesNotApplyDiscountBelowBoundary(): void
    {
        self::assertSame(0, discountForSubtotal(4999));
    }
}

The tests describe behaviour. They do not care how the function is implemented internally.

Choose the right test level

Different risks need different tests.

Use unit tests for:

  • pure calculations
  • string formatting
  • validation helpers
  • small services with simple inputs and outputs

Use integration tests for:

  • database queries
  • filesystem code
  • queues
  • email sending boundaries
  • HTTP clients

Use API or browser tests for:

  • important user workflows
  • routing and middleware behaviour
  • form submissions
  • JavaScript-heavy UI behaviour
  • checkout, login, registration, and admin publishing

Choose the cheapest test that gives useful confidence.

Tests should fail for the right reason

Before trusting a new test, make it fail once. Change the expected value or temporarily break the code. If the test still passes, it is not protecting the behaviour.

Avoid implementation-detail tests

A weak test checks how the code happens to work internally. A stronger test checks behaviour that matters to a caller or user.

For example, a discount test should check the returned discount for important subtotals. It should not check whether the function uses an if statement or a match expression.

What to remember

Automated tests give repeatable confidence. Start with small deterministic code, test happy paths and edge cases, choose the right test level, and run tests as part of the local quality gate.

Before moving on, make sure you can write a simple assertion for a pure PHP function and explain why a checkout flow needs more than a unit test.

Practice

Task: Test A Discount Boundary

Write PHPUnit-style tests for this function.

PHP example
<?php

declare(strict_types=1);

function discountForSubtotal(int $subtotal): int
{
    if ($subtotal >= 5000) {
        return 500;
    }

    return 0;
}

Requirements

  • Test a subtotal below the discount boundary.
  • Test the exact boundary.
  • Test a subtotal above the boundary.
  • Use assertSame().
  • Explain why this is a unit test.
Show solution
PHP example
<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

final class DiscountTest extends TestCase
{
    public function testDoesNotApplyDiscountBelowBoundary(): void
    {
        self::assertSame(0, discountForSubtotal(4999));
    }

    public function testAppliesDiscountAtBoundary(): void
    {
        self::assertSame(500, discountForSubtotal(5000));
    }

    public function testAppliesDiscountAboveBoundary(): void
    {
        self::assertSame(500, discountForSubtotal(5001));
    }
}

This is a unit test because the function is pure: it uses only the integer input, returns an integer output, and does not depend on a database, filesystem, HTTP request, framework, or browser.