testing php applications

Test-Driven Development

Red, Green, Refactor

The first failure matters. It proves the test can detect the missing behaviour. After the smallest working implementation passes, refactor names and structure without changing the expected outcome.

TDD is particularly effective for business rules, parsers, transformations, and bug fixes with a clear reproducible case.

Use It Where Feedback Is Clear

Do not force TDD into exploratory spikes where the shape of the problem is still unknown. Prototype if necessary, discard or clean up the experiment, then capture the discovered behaviour with tests.

PHP example
<?php

declare(strict_types=1);

use PHPUnit\Framework\TestCase;

function deliveryFee(int $basketTotalPence): int
{
    return $basketTotalPence >= 5_000 ? 0 : 495;
}

final class DeliveryFeeTest extends TestCase
{
    public function testDeliveryIsFreeAtTheThreshold(): void
    {
        $this->assertSame(0, deliveryFee(5_000));
    }

    public function testDeliveryIsChargedBelowTheThreshold(): void
    {
        $this->assertSame(495, deliveryFee(4_999));
    }
}

In a TDD session, write the test first and watch it fail because deliveryFee() is missing or returns the wrong value. Then add the implementation and refactor only after both boundary examples pass.

Common Mistakes

  • Writing a test after the implementation and assuming it could have failed.
  • Refactoring and adding behaviour in the same step until failures become hard to diagnose.
  • Testing trivial syntax instead of the rule.
  • Treating TDD as a substitute for integration and exploratory testing.

What To Practise

  • Run the red-green-refactor loop on a small rule.
  • Make a test fail for the expected reason.
  • Separate behaviour changes from refactoring.

Practice

Practice: Drive A Delivery Fee Rule

Requirements

  • Start from examples at 4999 and 5000 pence.
  • Record the initial expected failure.
  • Implement the smallest clear function.
  • Add one additional example after the rule passes.
Show solution

The boundary values expose the rule clearly.

PHP example
<?php

declare(strict_types=1);

function deliveryFee(int $basketTotalPence): int
{
    return $basketTotalPence >= 5_000 ? 0 : 495;
}

$examples = [0 => 495, 4_999 => 495, 5_000 => 0, 8_000 => 0];

foreach ($examples as $basket => $expected) {
    echo deliveryFee($basket) === $expected ? 'pass' : 'fail';
    echo PHP_EOL;
}

// Prints:
// pass
// pass
// pass
// pass

In PHPUnit, each comparison would be an assertion. The example stays runnable without requiring PHPUnit in the lesson runner.