When writing PHPUnit tests, data providers are one of the simplest ways to increase coverage without duplicating test logic. Most examples use arrays, but PHP generators (yield) are often a better fit: they’re more expressive, memory-efficient, and easier to extend.
This post walks through a clean, generic approach to using generators for PHPUnit data providers.
Why use generators instead of arrays?
A traditional data provider might look like this:
public static function provideCases(): array
{
return [
'case A' => ['input-a', 'expected-a'],
'case B' => ['input-b', 'expected-b'],
];
}
This works, but generators give you a few advantages:
- No need to build a full array in memory
- Named cases are more natural with
yield - Easier to compose or split into smaller logical chunks
- Better readability when cases grow
A simple example
Imagine you’re testing a service that transforms input values:
final class ValueTransformer
{
public function transform(string $value): string
{
return strtoupper($value);
}
}
Instead of writing multiple test methods, you can use a generator-based data provider:
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
final class ValueTransformerTest extends TestCase
{
#[Test]
#[DataProvider('provideTransformCases')]
public function it_transforms_values(string $input, string $expected): void
{
$service = new ValueTransformer();
$result = $service->transform($input);
$this->assertSame($expected, $result);
}
public static function provideTransformCases(): \Generator
{
yield 'lowercase' => ['hello', 'HELLO'];
yield 'mixed case' => ['HeLLo', 'HELLO'];
yield 'already uppercase' => ['WORLD', 'WORLD'];
}
}
Making test cases more expressive
Generators really shine when your inputs are objects instead of primitives.
For example, testing a query modifier:
final class QueryModifier
{
public function apply(QueryBuilder $query, string $field, string $direction): QueryBuilder
{
return $query->orderBy($field, $direction);
}
}
Your test can focus on behavior while the generator defines the variations:
#[Test]
#[DataProvider('provideSortingCases')]
public function it_applies_sorting(string $field, string $direction): void
{
$query = Mockery::mock(QueryBuilder::class);
$query->shouldReceive('orderBy')
->once()
->with($field, $direction)
->andReturnSelf();
$modifier = new QueryModifier();
$modifier->apply($query, $field, $direction);
}
public static function provideSortingCases(): \Generator
{
yield 'sort by name' => ['name', 'asc'];
yield 'sort by created_at desc' => ['created_at', 'desc'];
yield 'sort by nested field' => ['user.email', 'asc'];
}
The test stays minimal, while the generator clearly documents the supported scenarios.
Composing generators
One underrated benefit is that generators can be composed. You can split cases into smaller methods and reuse them:
public static function provideSortingCases(): \Generator
{
yield from self::basicFields();
yield from self::nestedFields();
}
private static function basicFields(): \Generator
{
yield 'name asc' => ['name', 'asc'];
yield 'created_at desc' => ['created_at', 'desc'];
}
private static function nestedFields(): \Generator
{
yield 'user email' => ['user.email', 'asc'];
}
This keeps large test suites maintainable without sacrificing clarity.
When to prefer generators
Generators are especially useful when:
- You have many test cases
- Test data is constructed dynamically
- You want to group or reuse subsets of cases
- Memory usage might become a concern
For small, static datasets, arrays are perfectly fine. But once your data providers grow, generators tend to scale much better.
Final thoughts
Using generators in PHPUnit data providers is a small change that improves readability and flexibility. Tests become easier to extend, and the intent of each case is clearer thanks to named yields.
If you’re already relying on data providers, switching from arrays to generators is a low-effort improvement that pays off quickly in larger test suites.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.