Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Data Providers

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We treat our source code as a first-class citizen. That means, among other things, we avoid duplication. Why not do the same with our tests? Our three tests for the size are... repetitive. They test the same thing just with slightly different input and then a different assertion. Is there a way to improve this? Absolutely: thanks to PHPUnit Data Providers.

Refactor our tests

Move to the bottom of DinosaurTest and add public function sizeDescriptionProvider(). Inside, yield an array with [10, 'Large'], then yield [5, 'Medium'], and finally yield [4, 'Small']:

... lines 1 - 7
class DinosaurTest extends TestCase
{
... lines 10 - 31
public function sizeDescriptionProvider()
{
yield [10, 'Large'];
yield [5, 'Medium'];
yield [4, 'Small'];
}
}

Yield is just a fancy way of returning arrays using PHP's built-in Generator function. As you'll see in a minute, these values - like 10 and large will become arguments to our test.

Alrighty, up in our test method, add an int $length argument and then string $expectedSize:

... lines 1 - 7
class DinosaurTest extends TestCase
{
... lines 10 - 27
public function testDino10MetersOrGreaterIsLarge(int $length, string $expectedSize): void
{
... lines 30 - 32
}
... lines 34 - 40
}

Now instead of Big Eaty's length being 10, use $length. And for our assertion, use $expectedSize instead of Large:

... lines 1 - 7
class DinosaurTest extends TestCase
{
... lines 10 - 27
public function testDino10MetersOrGreaterIsLarge(int $length, string $expectedSize): void
{
$dino = new Dinosaur(name: 'Big Eaty', length: $length);
self::assertSame($expectedSize, $dino->getSizeDescription(), 'This is supposed to be a large Dinosaur');
}
... lines 34 - 40
}

We do not need the medium and small tests anymore, so we can remove both of them.

Ok! Move back to your terminal and run our tests:

./vendor/bin/phpunit --testdox

Uh oh... Our test is failing because! It says:

ArgumentCountError - Too few arguments were provided. 0 passed and exactly 2 expected.

Tell our test to use the Data Provider

Oops, we never told our test method to use the data provider. Move back into our test and add a DocBlock with @dataProvider sizeDescriptionProvider:

... lines 1 - 7
class DinosaurTest extends TestCase
{
... lines 10 - 24
/**
* @dataProvider sizeDescriptionProvider
*/
public function testDino10MetersOrGreaterIsLarge(int $length, string $expectedSize): void
{
$dino = new Dinosaur(name: 'Big Eaty', length: $length);
self::assertSame($expectedSize, $dino->getSizeDescription(), 'This is supposed to be a large Dinosaur');
}
... lines 34 - 40
}

When PHPUnit 10 gets released, we'll be able to use a fancy #[DataProvider] attribute instead of this annotation.

Back to the terminal! Run the tests again:

./vendor/bin/phpunit --testdox

And... Yes! Our tests are passing!

Message Keys instead of Arguments

In the output, we see that each test ran with datasets 0, 1, & 2. Those are the arrays from the data provider. We can spruce this up a bit... because it's not going to be very helpful later if PHPUnit tells us that dataset 2 failed. Which one is that?

Move back to our test and, down here after the first yield statement, add the message key '10 Meter Large Dino' =>. Copy and paste this for our medium dino with 5 instead of 10 and this needs to be Medium. Do the same for our small dino with 4 and Small:

... lines 1 - 7
class DinosaurTest extends TestCase
{
... lines 10 - 34
public function sizeDescriptionProvider()
{
yield '10 Meter Large Dino' => [10, 'Large'];
yield '5 Meter Medium Dino' => [5, 'Medium'];
yield '4 Meter Small Dino' => [4, 'Small'];
}
}

Back in our terminal, let's see our tests now:

./vendor/bin/phpunit --testdox

And... Cool Beans! We now have

Dino 10 meters or greater is large with 10 Meter Large Dino

This looks a lot better than just seeing data set 0... though we do need to fix one more thing. That test method name doesn't make sense anymore. Change it to testDinoHasCorrectSizeDescriptionFromLength().

And, looking at our assertion, the message argument isn't very useful anymore... so let's remove it.

... lines 1 - 7
class DinosaurTest extends TestCase
{
... lines 10 - 34
public function sizeDescriptionProvider()
{
yield '10 Meter Large Dino' => [10, 'Large'];
yield '5 Meter Medium Dino' => [5, 'Medium'];
yield '4 Meter Small Dino' => [4, 'Small'];
}
}

Return Types Everywhere!

Finally, although not required... We can use either array or \Generator as the return type for the data provider. Let's go with \Generator- after all, we may need those for the park fences one day...

... lines 1 - 7
class DinosaurTest extends TestCase
{
... lines 10 - 34
public function sizeDescriptionProvider(): \Generator
{
... lines 37 - 39
}
}

To make sure this didn't break anything, try the tests one more time:

./vendor/bin/phpunit --testdox

Ummm... Awesome! Green Checks Everywhere!

And there you have it, with a little TLC, our tests are now nice and tidy... Coming up next, let's figure out how we can get our Dino's health status from GitHub and use it in our app...

Leave a comment!

5
Login or Register to join the conversation
Lechu85 Avatar
Lechu85 Avatar Lechu85 | posted 1 month ago

Data provider attributes, doesn't work :/

When I use: #[DataProvider('sizeDescriptionProvider')]

PhpStorm shows me "Undefined class 'DataProvider'" :)

Reply

Hey Lechu,

I think PHPUnit does not support PHP attributes yet, you need to use annotations instead

Cheers!

Reply

It seems the English subtitle timing is a bit offset.

Reply

It's now fixed, I hope you enjoy it, and thank you again for letting us know

Cheers!

Reply
Jesse-Rushlow Avatar
Jesse-Rushlow Avatar Jesse-Rushlow | SFCASTS | JoshuaGugun | posted 2 months ago

Howdy! Thanks for pointing that out to us. It does indeed seam slightly off. We'll look into it and see what we can come up with.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "symfony/asset": "6.1.*", // v6.1.0
        "symfony/console": "6.1.*", // v6.1.4
        "symfony/dotenv": "6.1.*", // v6.1.0
        "symfony/flex": "^2", // v2.2.3
        "symfony/framework-bundle": "6.1.*", // v6.1.4
        "symfony/http-client": "6.1.*", // v6.1.4
        "symfony/runtime": "6.1.*", // v6.1.3
        "symfony/twig-bundle": "6.1.*", // v6.1.1
        "symfony/yaml": "6.1.*" // v6.1.4
    },
    "require-dev": {
        "phpunit/phpunit": "^9.5", // 9.5.23
        "symfony/browser-kit": "6.1.*", // v6.1.3
        "symfony/css-selector": "6.1.*", // v6.1.3
        "symfony/phpunit-bridge": "^6.1" // v6.1.3
    }
}