testing php applications

PHPUnit Data Providers

A data provider runs the same test logic with several named examples. It is useful when one rule has a compact table of inputs and expected outputs.

Use Named Cases

Named cases make CI failures readable. A provider for delivery fees should name boundary conditions such as below threshold and at threshold instead of returning an unexplained list of arrays.

Keep Each Dataset Focused

If each row needs different setup or unrelated assertions, separate tests may be clearer. Data providers remove repetition; they should not hide scenario meaning.

PHP example
<?php

declare(strict_types=1);

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class DeliveryFeeTest extends TestCase
{
    public static function deliveryFeeExamples(): array
    {
        return [
            'empty basket' => [0, 495],
            'below threshold' => [4_999, 495],
            'at threshold' => [5_000, 0],
        ];
    }

    #[DataProvider('deliveryFeeExamples')]
    public function testDeliveryFee(int $basketTotalPence, int $expected): void
    {
        $this->assertSame($expected, deliveryFee($basketTotalPence));
    }
}

Common Mistakes

  • Anonymous numeric datasets that make failures hard to read.
  • Putting unrelated behaviours into one provider.
  • Generating enormous datasets that slow the suite without adding confidence.
  • Using providers when a few explicit tests would communicate better.

What To Practise

  • Create named examples for one rule.
  • Cover boundary values.
  • Decide when separate tests are clearer.

Practice

Practice: Provide Discount Examples

Requirements

  • Include empty, below-threshold, at-threshold, and above-threshold cases.
  • Name every case.
  • Keep values and expected discounts explicit.
Show solution
PHP example
<?php

declare(strict_types=1);

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class DiscountTest extends TestCase
{
    public static function discountExamples(): array
    {
        return [
            'empty order' => [0, 0],
            'below threshold' => [9_999, 0],
            'at threshold' => [10_000, 1_000],
            'above threshold' => [15_000, 1_500],
        ];
    }

    #[DataProvider('discountExamples')]
    public function testDiscount(int $total, int $expectedDiscount): void
    {
        $actualDiscount = $total >= 10_000 ? (int) ($total * 0.10) : 0;

        $this->assertSame($expectedDiscount, $actualDiscount);
    }
}

Run vendor/bin/phpunit --filter DiscountTest. The named case appears in the failure output if a boundary breaks.