Skip to Content
Course content

165: Data Providers in PHPUnit

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you're testing a piece of logic that needs to be verified against a dozen different inputs, your first instinct might be to reach for a foreach loop. I've seen this in countless PRs. You have a set of inputs and expected outputs, and you just want to iterate through them and assert that the result is correct. It feels efficient because you've only written one test method.

The trap of the manual loop

public function testPasswordValidation(): void
{
    $cases = [
        ['short', false],
        ['TooShort1!', false],
        ['ValidPass123!', true],
        ['NoSpecialChar123', false],
        ['alllowercase1!', false],
    ];

    foreach ($cases as [$password, $expected]) {
        $this->assertEquals($expected, $this->validator->isValid($password));
    }
}

On the surface, this works. But here is where it breaks: the moment the second case fails, the entire test stops. PHPUnit sees this as one single test. If 'TooShort1!' fails, you have no idea if the other three cases would have passed or failed because the execution never reaches them. You end up in a tedious cycle of fixing one input, rerunning the test, finding the next failure, and repeating. It's a slow feedback loop, and it hides the full scope of your regressions.

Treating inputs as individual tests

This is where Data Providers come in. Instead of looping inside the test, you move the data to a separate method that "feeds" the test. PHPUnit then treats every single entry in that provider as a distinct test case. If case #2 fails, PHPUnit marks it as a failure but continues to run cases #3, #4, and #5. You get a full report of exactly which edge cases are broken.

public function passwordProvider(): array
{
    return [
        'too short'          => ['short', false],
        'still too short'    => ['TooShort1!', false],
        'perfectly valid'    => ['ValidPass123!', true],
        'missing special'    => ['NoSpecialChar123', false],
        'missing uppercase'  => ['alllowercase1!', false],
    ];
}

#[DataProvider('passwordProvider')]
public function testPasswordValidation(string $password, bool $expected): void
{
    $this->assertEquals($expected, $this->validator->isValid($password));
}

Notice how I used string keys like 'too short' in the array. I highly recommend doing this. When a test fails, PHPUnit will print that key in the output. Seeing "Failed asserting that false is true" is annoying; seeing "Failed asserting that false is true in dataset 'missing uppercase'" tells you exactly what's wrong before you even open the code.

The trade-off in setup

Is there a downside? You're writing a bit more boilerplate—an extra method and an attribute. If you only have two test cases, a data provider is probably overkill. But the moment you hit four or five variations, the cost of the extra method is dwarfed by the time you save during debugging. You're essentially trading a few lines of setup for a much higher resolution of failure reporting.




📋 Practical Task

Refactoring a Discount Code Validator

You have a DiscountValidator class with a method isValid(string $code): bool. The current test suite uses a foreach loop to check several codes: 'SUMMER20' (valid), 'WINTER10' (valid), 'EXPIRED' (invalid), and 'INVALID_CODE' (invalid).

Your task is to refactor the test. Create a dedicated data provider method named discountCodeProvider that returns a keyed array of these cases. Update the test method to use the #[DataProvider('discountCodeProvider')] attribute, ensuring that each single code is treated as an independent test case by PHPUnit.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.