Mastering PHPUnit: Using data providers

May 22, 2024·
Gert de Pagter
Gert de Pagter
· 6 min read

This is part 3 of a series on PHPUnit testing, you can find part 2 here. In this post we’ll focus on the use of data providers. I have written about them before, but for earlier PHPUnit versions. This post is relevant for PHPUnit version 10 or higher. For version 9 or lower check out the original data provider post

What are data providers?

A data provider is a function or method that, as the name suggests, provides data to your tests. So instead of having to write a new test case for every option, we write a single test, and then let it run multiple times with a lot of data, to make sure it handles all cases as expected.

Creating data providers.

A data provider is a static method which provides input data for our tests. This method either lives in your test case itself, or in another class. Generally we want to keep the data provider in the same class, so we keep the data of our test close to the test itself. You’d only move it to another class if the test data is re-used among multiple tests.

For this example we’ll test a ‘calculator’ class which adds two numbers, to show you how that works. So let’s create that class and test case class as discussed in the previous post. And there we add our test method like this:

//Calculator.php

namespace App;

class Calculator
{
    public function add(int $one, int $two): int
    {
        return $one + $two;
    }
}
//CalculatorTest.php

namespace test;

use App\Calculator;
use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    public function testItAdds(): void
    {
        $calculator = new Calculator();

        $this->assertSame(3, $calculator->add(1, 2));
    }
}

Now this is only one case. But we need to deal with negative numbers, zero, etc. We could write out a test case for each of these. But instead, lets create a data provider, to give us this test data.

namespace test;

use App\Calculator;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    #[DataProvider('provideAddCases')]
    public function testItAdds(int $one, int $two, int $expected): void
    {
        $calculator = new Calculator();

        $this->assertSame($expected, $calculator->add($one, $two));
    }
    
    public static function provideAddCases(): Generator
    {
        yield [1,2,3];
    }
}

As you can see we added the DataProvider attribute to our test method, and gave it input parameters. The DataProvider attribute takes as an argument the method name on the current test class that we are using. The data from the array that we yield is passed to the test method. So in our example, the dataprovider gives us $one = 1, $two = 2, $expected = 3. (From PHPUnit 11.1 you can also use string keys for the array, which are passed to the argument with the correct name.)

Now if we want to add extra test cases, all we have to do is expand our data provider. So lets add some:

namespace test;

use App\Calculator;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    #[DataProvider('provideAddCases')]
    public function testItAdds(int $one, int $two, int $expected): void
    {
        $calculator = new Calculator();

        $this->assertSame($expected, $calculator->add($one, $two));
    }

    public static function provideAddCases(): Generator
    {
        yield [1,2,3];
        yield [0,0,0];
        yield [0,3,3];
        yield [-12,7,-5];
        yield [-5,-4,-9];
        yield [-5,0,-5];
    }
}

If we now run our tests, and look at the bottom of the screen we’ll see the following. This means we ran 6 tests, while we only had to write one.

OK (6 tests, 6 assertions)

Why not test in a loop?

Now you may be thinking, why should I use a data provider for this? After all you could just write a foreach loop to go over that data, and run the assertion for each option. The downside of this, is that if one of the checks fail it stops executing right after that. So if our case of yield [-12,7,-5]; failed to work, we would not execute the 2 test cases after that. Meaning we need to fix the first bug, before we can even see if the other test cases are passing. This can be especially annoying when refactoring code, and introducing a bug. Now it becomes harder to spot if you introduced more bugs.

Another upside of these data providers is that you can add multiple data provider to one method. Now for this example it may not be needed, but you may want to split your providers up if you have a lot of test cases. So let’s say that for our calculator test we want to split up the data of negative and positive numbers, we could easily do it like this, and still not have to write a second test.

class CalculatorTest extends TestCase
{
    #[DataProvider('provideAddCases')]
    #[DataProvider('provideNegativeAddCases')]
    public function testItAdds(int $one, int $two, int $expected): void
    {
        $calculator = new Calculator();

        $this->assertSame($expected, $calculator->add($one, $two));
    }

    public static function provideAddCases(): Generator
    {
        yield [1,2,3];
        yield [0,0,0];
        yield [0,3,3];
    }

    public static function provideNegativeAddCases(): Generator
    {
        yield [-12,7,-5];
        yield [-5,-4,-9];
        yield [-5,0,-5];
    }
}

Data providers will also work better if you are tying to test with exceptions. I explain more about that in an earlier blog post about testing exceptions with PHPUnit.

Extra tips for testing with data providers

Data providers are great for quickly finding edge cases of the methods you are testing. So if you are testing something that takes an array, try testing with an empty one, with one value, with two, and maybe 5 or so. If you are testing with strings, try inserting an empty string, or maybe a really large one.

Data providers can also help you with recreating real life scenarios. For example, I had a bug report once of prices on our website displaying weirdly if the price was above 10.0000. Which hadn’t really happened because most products were under 30 bucks, but it does happen in some currencies which are a lot bigger.

Now since this code was already tested, all I had to do was add a case where I added some really large numbers, and the bug was reproduced right away. And because the code was very well tested, the change was easy to make without breaking anything for smaller numbers.

Next weeks post will discuss Mocking and Stubbing in PHPUnit, which will help us deal with dependencies we don’t want to use while running our tests. You can find that post right here.

If you want to get notified of the next blog post, join the newsletter.