skip to content

In PHPUnit 13, how does a #[DataProvider] method feed a test, and why must that provider be public and static?

level: middleimportance: must knowfreq 60%

answer

  1. provider runs while the suite is built
  2. no $this, no setUp yet
  3. iterable of arrays, one per data set
  4. string keys name the data sets
  5. empty provider is an error unless skipWhenEmpty

basics

~20 s

#[DataProvider('name')] points a test at a public static method returning an iterable of argument arrays. PHPUnit calls it while building the suite, before any setUp(), so there is no instance to call it on; each array becomes one test run.

solid answer

~40 s

`#[DataProvider('passwords')]` names a method that must be `public`, `static` and parameterless and must return an iterable - an array or a generator - whose values are arrays of arguments. PHPUnit calls it while it **builds** the test suite, before `setUpBeforeClass()` or any `setUp()`, so there is no test instance and no fixture; that is why PHPUnit 13 rejects a non-static provider as invalid. Each value becomes a separate test; a string key names it (`with data set "too short"`), an integer key gives `#0`, `#1`. Several `#[DataProvider]` attributes merge, but a duplicate string key is an error. An empty provider is an error unless declared with `skipWhenEmpty: true`, which marks the test skipped. `#[DataProviderExternal(Cases::class, 'passwords')]` reuses a provider in another class.

code

php · 24 lines
php
<?php
declare(strict_types=1);

namespace App\Tests\Security;

use App\Security\PasswordStrength;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class PasswordStrengthTest extends TestCase
{
    public static function passwords(): iterable
    {
        yield 'too short' => ['Ab1!', false];
        yield 'no digit' => ['correcthorsebattery', false];
        yield 'long passphrase' => ['correct horse battery 42', true];
    }

    #[DataProvider('passwords')]
    public function testIsStrong(string $password, bool $expected): void
    {
        $this->assertSame($expected, (new PasswordStrength())->isStrong($password));
    }
}

go deeper

for a junior

Recall that the provider is a public static method returning arrays of arguments, and that each array runs as a separate test.

for a middle

Explain the timing - providers run during suite construction, before setUp() - and use it to justify the static rule, named keys and the empty-provider error.

for a senior

Show how you keep large provider tables readable and safe: named sets, external providers for shared cases, no shared mutable objects, deliberate skipWhenEmpty.

for a principal

Treat provider design as suite architecture: where shared case tables live, how they are reviewed, and when a table has grown into a fixture file.

## The mechanism A **data provider** is a method that supplies argument lists to a test method. In PHPUnit 13 you connect them with the `#[DataProvider]` attribute from `PHPUnit\Framework\Attributes`, placed on the test method and naming the provider by method name: ```php public static function passwords(): iterable { yield 'too short' => ['Ab1!', false]; yield 'no digit' => ['correcthorsebattery', false]; yield 'long passphrase' => ['correct horse battery 42', true]; } #[DataProvider('passwords')] public function testIsStrong(string $password, bool $expected): void { $this->assertSame($expected, (new PasswordStrength())->isStrong($password)); } ``` Each yielded value is a **data set**: an array unpacked into the test method's parameters. PHPUnit wraps the method in a small suite with one test per data set, and runs `setUp()` and `tearDown()` around every one of them. ## Why public and static The timing explains the rule. PHPUnit calls every provider **while it is building the test suite** - its event stream shows "Data Provider Method Called" before "Test Suite Loaded". At that point no test has run, `setUpBeforeClass()` has not run, and no test-case instance exists whose `setUp()` could have prepared fixtures. A provider therefore has to be callable on the class alone. PHPUnit 13 checks this before calling anything and reports a **PHPUnit error** naming the provider if it is: - not `public` ("is not public"), - not `static` ("is not static"), - declared with parameters ("expects an argument"), - returning something that is not iterable. When a provider is invalid, none of that test method's data sets run. ## Shape of the returned data - **Keys**: integers or non-empty strings. A string key names the data set, so reports read `testIsStrong with data set "too short"` instead of `with data set #0`. Named sets are the single cheapest improvement to a failing report. - **Values**: each must be an array. A bare string or scalar is rejected as an invalid data set. - **Argument count**: a set with more values than the method's parameters triggers a PHPUnit warning; pass `validateArgumentCount: false` to the attribute to silence it for a deliberate case. Variadic test methods are not checked. - **Generators**: allowed, and `yield 'name' => [...]` reads well, but PHPUnit iterates the whole provider up front while building the suite, so a generator does not make data sets lazy or save memory. ## Combining and reusing providers | Attribute | What it does | |---|---| | `#[DataProvider('m')]` | Calls static `m()` on the test class (or its parents) | | `#[DataProviderExternal(Cases::class, 'm')]` | Calls static `Cases::m()` in another class | | several of the above on one method | Merges their data sets in order | Merged providers must not reuse a **string key**: PHPUnit reports that the key "has already been defined by provider" and treats the provider as invalid. Integer keys are simply appended. ## Empty providers By default an empty provider is a mistake - usually a filter or a file that yielded nothing - and PHPUnit reports "Empty data set provided by data provider" as an error. When empty data is legitimate, declare `#[DataProvider('passwords', skipWhenEmpty: true)]`; the test is then reported as **skipped** with a message saying the empty data was explicitly permitted. ## Common mistakes 1. Building fixtures in `setUp()` and reading them in the provider. The provider ran long before `setUp()`; build what it needs inside the provider itself. 2. Relying on shared mutable objects in data sets. Every data set that references the same object gets the same object handle, so a test that mutates it leaks into later sets. 3. Giving the provider a `test` prefix. PHPUnit then also treats it as a test method and warns about it. 4. Expecting a leftover `@dataProvider` docblock to work. PHPUnit 13 reads attributes only; the test is called with no arguments and fails with `ArgumentCountError`. ## Interview summary Say that the provider is public, static and parameterless because it runs during suite construction; that each array value is one run; that string keys name runs; and that `#[DataProviderExternal]` and multiple providers exist for reuse.

  • In PHPUnit 13, the provider needs a PasswordPolicy that setUp() creates. How do you restructure it?
    Build what the data needs inside the static provider, or pass only plain inputs (strings, lengths, flags) and construct the policy in the test method, which runs after `setUp()`. The provider executes during suite construction, so any instance state from `setUp()` does not exist yet.
  • In PHPUnit 13, does returning a generator from a data provider keep large data sets out of memory?
    No. PHPUnit iterates the whole provider while building the suite and stores every data set before the first test runs. A generator is a readable way to write `yield 'name' => [...]`, but it does not stream data sets lazily into test execution.
  • With PHPUnit 13, when is #[DataProvider] with skipWhenEmpty: true the right choice?
    When an empty provider is legitimate - for example cases read from a fixture directory that may be empty on some platforms. The test is reported as skipped instead of failing with 'Empty data set provided by data provider'. Use it deliberately; otherwise the default error catches providers that yield nothing by mistake.

saying these in an interview costs you the question

  • Says the provider may read fixtures that setUp() prepared
  • Believes a non-static provider still works in PHPUnit 13
  • Thinks a generator provider makes data sets lazy during execution
  • Uses integer keys and then struggles to identify which case failed
  • Expects an empty provider to pass silently with zero runs