Streamlining your test suite with PHPUnit Data Providers

In every testing talk I’ve attended (or given), there’s one stand-out feature that often has the audience saying “whoa, I had no idea you could do that!” No, it’s sadly not “hey look, you can reliably build quality software with a much lower chance of defects or regressions!”, but rather the inevitable use of PHPUnit’s Data Providers.

With Data Providers, our test suite can become more readable and maintainable while making it trivial to add new testing scenarios. Best of all? PHPUnit ships with Data Providers right out of the box.

What are Data Providers?

In essence, Data Providers are a method of feeding groups of data into a test method that share the same purpose and setup. These are especially helpful for unit testing, as those test methods often follow the pattern of “given the following inputs, I expect to see this output”.

For a practical example, assume we’re testing a method that accepts a value and determines if it is a positive, non-zero integer:

When writing our tests, we might find ourselves with test methods that look like this:

While these methods aren’t bad, you can see how there’s a lot of shared setup: we’re defining a method that’s calling our isPositiveNonZeroInteger() function against a provided value, and asserting whether the function returns true or false.

Introducing Data Providers

With Data Providers, we can take our same tests and reduce our suite to one or two methods, and there are two ways to do it:

First, we can use the @dataProvider annotation, which signals that another method will provide the data (see what they did there?) for your test method. The Data Provider returns an array of scenarios, which are then treated as arguments by the test method:

When PHPUnit runs testIsPositiveNonZeroIntegerProvider(), it will do so four times (once with each data set), for a total of four separate tests. The best part? If one data set fails, it won’t block the others from running, and adding a new scenario is as easy as adding a new entry to the array returned by isPositiveNonZeroIntegerProvider()!

Still, we’re doing an awful lot of setup to test some simple Boolean values, which is where the next method of using Data Providers comes in: the @testWith annotation. This annotation allows us to define a simple matrix of test data right in the test method’s docblock, eliminating the need for a Data Provider method:

This approach reduces our tests to a single method, where we can trivially add new entries as necessary. Want to see how it behaves with a string? Simply add a new row to the DocBlock!

Using @testWith for Data Providers is more rigid than using @dataProvider — for instance, you can’t interpolate variables or do any calculations — but for many unit tests, it’s perfect.

Considerations when using Data Providers

Just because we can use Data Providers doesn’t mean they’ll always make sense to other developers on the project. After all, if our function name wasn’t self-documenting in the form of isPositiveNonZeroInteger(), people may be confused as to why “2” is true, but “2.25” is false.

As with most things in computing, context is key, so you might consider one (or both) of the following:

Using named data sets

By default, a failing data set will look something like the following in PHPUnit:

The message “FunctionsTest::testIsNonZeroInteger with data set #1 (2.25, false)” doesn’t really tell us much about why that particular assertion failed.

If we’re using the @dataProvider annotation, we can help provide context by setting keys on the returned array:

Now, a failure for data set #1 will read:

Leveraging assertion messages

Another technique that can be helpful is including a $message parameter in your data sets:

Most (if not all) of the PHPUnit assertions permit you to pass a more-descriptive error message as the final parameter, so this can be a great way to provide additional explanation/context to your data sets.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.