Data Providers and Fixtures
A data provider runs one test over many inputs, and setUp rebuilds the starting state so no test inherits another test's mess.
#[DataProvider('providerMethod')] public function test_case(mixed $input, mixed $expected): voidThree tests that differ only in two values are really one test with three rows of data. A data provider is a method that returns those rows. PHPUnit runs the test once per row, passing the row's values as arguments, and names each run after the row's key.
use PHPUnit\Framework\Attributes\DataProvider;
#[DataProvider('phoneNumbers')]
public function test_it_normalises_a_number(string $raw, string $expected): void
{
self::assertSame($expected, normalise_phone($raw));
}
public static function phoneNumbers(): array
{
return [
'spaces' => ['07700 900 123', '+447700900123'],
'dashes' => ['07700-900-123', '+447700900123'],
];
}The provider must be public static, take no arguments, and return an array or any iterable. For one or two rows there is a shorthand, #[TestWith([...])] written straight on the test method; it produces exactly the same cases as a provider returning those rows, with no second method.
Fixtures
A fixture is the known starting state a test needs. setUp(): void runs before every test method and tearDown(): void runs after it, so each test gets its own fresh objects. Build them there and assign them to properties, instead of repeating the construction in every test.
Independence
Every test must pass on its own and in any order. That breaks the moment state leaks: a static property, a file left on disk, a row still in a table. If test B only passes after test A has run, you do not have two tests.
Example
When to use it
- Check a phone or postcode normaliser against twenty real-world formats without writing twenty methods.
- Give every test in a class the same freshly built service object, with no copy-and-paste constructor calls.
- Drive a parser's tests from a fixture file that a non-developer can add rows to.
- Prove tests are independent by running them in isolation after a failure you cannot reproduce alone.
More examples
A provider and a fixture in one test class
The two halves of the lesson side by side. setUp gives every run a basket holding one pen; the provider supplies the two inputs. Each row runs setUp again, which is why the second row expects 4 rather than 5. A failure is reported as BasketTest::test_it_adds_to_an_existing_line with data set "three more", so the row name is part of the error message.
<?php
declare(strict_types=1);
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
final class BasketTest extends TestCase
{
private Basket $basket;
protected function setUp(): void
{
$this->basket = new Basket();
$this->basket->add('pen', 1);
}
#[DataProvider('quantities')]
public function test_it_adds_to_an_existing_line(int $add, int $expected): void
{
$this->basket->add('pen', $add);
self::assertSame($expected, $this->basket->quantity('pen'));
}
public static function quantities(): array
{
return [
'one more' => [1, 2],
'three more' => [3, 4],
];
}
}The #[TestWith] shorthand
These two test methods produce identical cases; #[TestWith] is a data provider with the rows inlined. Use it when there are a couple of rows and no computation. Switch to a provider when the rows get numerous, need keys to name them, or are shared by more than one test method.
<?php
declare(strict_types=1);
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;
final class SlugTest extends TestCase
{
// Shorthand: the rows live on the test method.
#[TestWith(['PHP 8.5 -- Release Notes', 'php-8-5-release-notes'])]
#[TestWith(["What's new", 'what-s-new'])]
public function test_it_slugifies(string $title, string $expected): void
{
self::assertSame($expected, Slug::fromTitle($title));
}
// Longhand: the same two rows, returned by a separate provider.
#[DataProvider('titles')]
public function test_it_slugifies_the_long_way(string $title, string $expected): void
{
self::assertSame($expected, Slug::fromTitle($title));
}
public static function titles(): array
{
return [
['PHP 8.5 -- Release Notes', 'php-8-5-release-notes'],
["What's new", 'what-s-new'],
];
}
}A provider that yields instead of returning
A provider may return any iterable, so a generator works and lets the cases come from a file instead of being typed out. Useful when a product owner maintains the list of examples, or when there are hundreds of rows. The yielded key becomes the case name in the report.
<?php
declare(strict_types=1);
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
final class CsvImportTest extends TestCase
{
#[DataProvider('rowsFromFixtureFile')]
public function test_it_parses_a_row(string $line, array $expected): void
{
self::assertSame($expected, parse_csv_line($line));
}
public static function rowsFromFixtureFile(): Generator
{
$handle = fopen(__DIR__ . '/fixtures/orders.txt', 'rb');
while (($line = fgets($handle)) !== false) {
[$raw, $json] = explode('|', trim($line), 2);
yield $raw => [$raw, json_decode($json, true, flags: JSON_THROW_ON_ERROR)];
}
fclose($handle);
}
}
Discussion