Data Providers
Data providers let you run one test with different sets of input data. Each set runs as a separate test.
The plugin does not require setup.
#[DataSet]
Declares a set of arguments for a parameterized test. Can be used multiple times — each attribute creates a separate test run.
#[DataSet(array $arguments, ?string $name = null)]Parameters:
$arguments- Array of values passed to the test method.
$name- Label displayed in reports. Helps identify which scenario failed.
Examples:
#[Test]
#[DataSet([1, 1, 2])]
#[DataSet([2, 3, 5])]
#[DataSet([0, 0, 0])]
public function testSum(int $a, int $b, int $expected): void
{
Assert::same($expected, $a + $b);
}With labels:
#[DataSet([1, 1, 2], 'positive numbers')]
#[DataSet([-1, -1, -2], 'negative numbers')]
#[DataSet([0, 0, 0], 'zeros')]
public function testSum(int $a, int $b, int $expected): void { ... }#[DataProvider]
Provides data for a parameterized test from a method or callable.
#[DataProvider(callable|string $provider)]Parameters:
$provider- Data source: method name (
'method'), callable ([Class::class, 'method']), closure, or invokable object. Must returniterable. String keys of elements become dataset labels in reports.
Examples:
#[Test]
#[DataProvider('userDataProvider')]
public function testUserValidation(string $email, bool $expected): void
{
$isValid = $this->validator->validate($email);
Assert::same($expected, $isValid);
}
public function userDataProvider(): iterable
{
yield ['valid@example.com', true];
yield ['invalid', false];
yield ['test@domain.co.uk', true];
}Flexible Provider Sources
#[DataProvider]#[DataProvider(callable|string $provider)]Provides data for a parameterized test from a method or callable. accepts various callable types:
Method name from the same class:
#[DataProvider('dataProvider')]
public function testSomething($data): void { ... }Method from another class:
#[DataProvider([DataSets::class, 'userScenarios'])]
public function testUser($data): void { ... }Closure directly in attribute (PHP 8.5+):
#[DataProvider(fn() => [
[1, 2, 3],
[5, 5, 10],
])]
public function testAddition(int $a, int $b, int $expected): void { ... }Invokable object:
#[DataProvider(new UserDataProvider())]
public function testUser($data): void { ... }Invokable objects are particularly useful for separating data loading logic. For example, loading test cases from JSON/CSV files into a dedicated class keeps your test code clean.
Dataset Labels
Labels are set via string array keys:
public function userDataProvider(): array
{
return [
'valid email' => ['test@example.com', true],
'invalid format' => ['not-an-email', false],
'empty string' => ['', false],
];
}#[DataZip]
Pairs up providers element by element.
#[DataZip(DataProviderAttribute ...$providers)]The first item from the first provider joins with the first from the second, second with second, and so on. Arguments from all providers merge into a single test call.
Parameters:
$providers- Data providers to pair up.
Examples:
#[DataZip(
new DataProvider('credentials'),
new DataProvider('expectedPermissions'),
)]
public function testUserPermissions(string $login, string $password, array $permissions): void
{
$user = $this->auth->login($login, $password);
Assert::same($permissions, $user->getPermissions());
}
// credentials: [['admin', 'secret'], ['guest', '1234']]
// expectedPermissions: [[['read', 'write', 'delete']], [['read']]]
//
// Test runs 2 times:
// 1. admin/secret → ['read', 'write', 'delete']
// 2. guest/1234 → ['read']Providers of Different Lengths
If providers have different lengths, the number of datasets is determined by the shortest provider:
#[DataZip(
new DataProvider('inputs'), // 3 items
new DataProvider('outputs'), // 2 items
)]
public function testTransform(string $input, string $output): void { ... }
// inputs: [['a'], ['b'], ['c']]
// outputs: [['x'], ['y']]
//
// Runs 2 times (limited by outputs):
// 1. 'a', 'x'
// 2. 'b', 'y'
// Third item from inputs ('c') is ignoredKeys in Reports
Dataset labels are joined with |. If datasets are named admin and full-access, the report shows admin|full-access.
#[DataCross]
Creates all possible combinations from providers (cartesian product).
#[DataCross(DataProviderAttribute ...$providers)]Parameters:
$providers- Data providers to combine.
Examples:
#[DataCross(
new DataProvider('browsers'),
new DataProvider('screenSizes'),
)]
public function testResponsiveLayout(string $browser, int $width, int $height): void
{
$this->driver->setBrowser($browser);
$this->driver->setViewport($width, $height);
Assert::true($this->page->isLayoutCorrect());
}
// browsers: [['chrome'], ['firefox'], ['safari']]
// screenSizes: [[1920, 1080], [768, 1024], [375, 667]]
//
// Runs 9 times — each browser with each screen size:
// chrome × 1920×1080, chrome × 768×1024, chrome × 375×667,
// firefox × 1920×1080, ...Watch the Combination Count
Test count grows multiplicatively. Three providers with 5 items each means 125 tests. Use #[DataCross]#[DataCross(DataProviderAttribute ...$providers)]Creates all possible combinations from providers (cartesian product). mindfully.
Keys in Reports
Labels are joined with ×. Datasets chrome and mobile produce the key chrome×mobile.
#[DataUnion]
Merges data from multiple providers into a single sequential set.
#[DataUnion(DataProviderAttribute ...$providers)]To combine data from multiple sources, you can simply list multiple #[DataProvider]#[DataProvider(callable|string $provider)]Provides data for a parameterized test from a method or callable. or #[DataSet]#[DataSet(array $arguments, ?string $name = null)]Declares a set of arguments for a parameterized test. Can be used multiple times — each attribute creates a separate test run. above the method. #[DataUnion]#[DataUnion(DataProviderAttribute ...$providers)]Merges data from multiple providers into a single sequential set. is needed when combining must happen inside another attribute — for example, inside #[DataCross]#[DataCross(DataProviderAttribute ...$providers)]Creates all possible combinations from providers (cartesian product). or #[DataZip]#[DataZip(DataProviderAttribute ...$providers)]Pairs up providers element by element..
Parameters:
$providers- Data providers to merge into a single set.
Examples:
#[DataCross(
new DataUnion(
new DataProvider('legacyFormats'),
new DataProvider('modernFormats'),
),
new DataProvider('compressionLevels'),
)]
public function testExport(string $format, int $compression): void
{
// All formats (legacy + modern) are crossed with each compression level
}Combining Providers
Inside #[DataZip]#[DataZip(DataProviderAttribute ...$providers)]Pairs up providers element by element., #[DataCross]#[DataCross(DataProviderAttribute ...$providers)]Creates all possible combinations from providers (cartesian product)., and #[DataUnion]#[DataUnion(DataProviderAttribute ...$providers)]Merges data from multiple providers into a single sequential set. you can use any data providers — #[DataProvider]#[DataProvider(callable|string $provider)]Provides data for a parameterized test from a method or callable., #[DataSet]#[DataSet(array $arguments, ?string $name = null)]Declares a set of arguments for a parameterized test. Can be used multiple times — each attribute creates a separate test run., and even nest them within each other.
Mixing Types
Handy when some parameters are fixed while others come from a provider:
#[DataCross(
new DataSet(['mysql'], 'mysql'),
new DataSet(['pgsql'], 'pgsql'),
new DataProvider('migrationScenarios'),
)]
public function testMigration(string $driver, array $scenario): void { ... }Or more compact with a #[DataProvider]#[DataProvider(callable|string $provider)]Provides data for a parameterized test from a method or callable. for drivers:
#[DataCross(
new DataProvider('databaseDrivers'),
new DataProvider('migrationScenarios'),
)]
public function testMigration(string $driver, array $scenario): void { ... }Nested Combinations
For complex scenarios, providers can be nested:
#[DataZip(
new DataCross(
new DataProvider('users'),
new DataProvider('roles'),
),
new DataProvider('expectedResults'),
)]
public function testAccessControl(string $user, string $role, bool $expected): void
{
$this->actAs($user)->withRole($role);
Assert::same($expected, $this->canAccess('/admin'));
}
// users × roles produces all user-role combinations,
// then they're paired with expected results