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
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
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
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
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
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.