Write Simpler, More Maintainable DataProviders With PHPUnit 10's TestWith Attribute

Write Simpler, More Maintainable DataProviders With PHPUnit 10's TestWith Attribute

If you love using data providers in PHPUnit, but find defining them verbose (and potentially buggy) then you’ll love the attribute-based approach in PHPUnit 10. Learn the essentials in this short post.


If this is your first time hearing about them, data providers allow you to invoke a test function multiple times, providing it with different data on each invocation. They’re an excellent – and simple – way of broadening your test coverage

Take a look at example below, and then let’s step through it together.

/**
 * @dataProvider referenceIDDataProvider
 */
public function testCanValidateReferenceIDs(
    string $referenceID,
    bool $isValid
): void {
    $validator = new MyHypotheticalValidatorClass();

    $result = $validator->isValidReferenceID($referenceID);
    ($isValid)
        ? $this->assertTrue($result)
        : $this->assertFalse($result);
}

public function referenceIDDataProvider(): array
{
    return [
        [
            'MSAU2407240002',
            false,
        ],
        [
            'MSAU2407240001',
            true,
        ],
    ];
}

The test function (testCanValidateReferenceIDs) takes two arguments:

  • $referenceID, a string
  • $isValid, a bool

It passes $referenceID to the subject under test ($validator->isValidReferenceID()) and, based on the value of $isValid, asserts whether the result of the function call ($result) is true or false.

For the purposes of a simple example, assume that isValidReferenceID() is a small function that checks whether $referenceID matches two criteria:

  1. It’s a string 14 characters in length
  2. It contains only upper and lowercase letters, and numbers from 0 - 9

As the @dataProvider annotation is used in the test’s docblock, PHPUnit will then look for the function name specified (referenceIDDataProvider) and iterate over the array that the function returns, passing the values of each inner array to an invocation of testCanValidateReferenceIDs(). So, the first iteration would receive 'MSAU2407240002' and false, and the second iteration would receive 'MSAU2407240001' and true.

That’s fine, but there’s a lot of boilerplate code to write. And, the more arguments you pass to the test the more complicated the array returned from the dataProvider function can become. Consequently, while extremely versatile and flexible, it can become increasingly error prone – not to mention frustrating to maintain.

But, with the introduction of attributes in PHP 8, and their uptake in PHPUnit 10, this verbose approach becomes a thing of the past. Take a look at the revised test function definition below.

<?php

use PHPUnit\Framework\Attributes\TestWith;

#[TestWith(['MSAU2407240002', false])]
#[TestWith(['MSAU2407240001', true])]
public function testCanValidateReferenceIDs(
    string $referenceID,
    bool $isValid
): void {
    $result = $this->handler
        ->isValidReferenceID($referenceID);

    ($isValid)
        ? $this->assertTrue($result)
        : $this->assertFalse($result);
}

In this version you import the TestWith attribute. Then, you specify it once for each different data set that you want to invoke the test function with. There’s no separate function to write, and the structure is less complicated. This makes it easier to understand, clearer, and more maintainable.

Which approach do you prefer?

I appreciate that it may not, yet, be an option for you, as you may be stuck using a version of PHP prior to 8. Or, for reasons outside of your control, you may not be able to upgrade, or may be on a version of PHPUnit prior to 10.

If given the choice though, which approach would you use? Let me know in the comments.


You might also be interested in these tutorials too...

Go Interfaces Make Development and Testing Easier
Fri, Jul 21, 2023

Go Interfaces Make Development and Testing Easier

Substitutability or the Liskov Substitution Principle (LSP) is a concept that I’ve tried to adhere to for some years when writing code. It’s beneficial for many reasons, but particularly when testing, as it can indirectly force you to write code that is more testable. Recently, I’ve started appreciating how it works in Go, and will step through how in this short article.

A Short Introduction to Postman
Fri, Jan 10, 2020

A Short Introduction to Postman

Debugging requests can be a time-consuming process. However, there’s a tool that makes doing so a lot simpler. It’s called Postman. In this tutorial, I step you through its core features and show you how to use them.

How to Run Tests in PhpStorm
Fri, Jan 3, 2020

How to Run Tests in PhpStorm

PhpStorm offers so much functionality. From syntax highlighting to Docker integration, it’s an extremely comprehensive tool. However, have you ever thought of using it to run your unit tests? In this article, I step you through running tests, from an entire suite to an individual test.


Want more tutorials like this?

If so, enter your email address in the field below and click subscribe.

You can unsubscribe at any time by clicking the link in the footer of the emails you'll receive. Here's my privacy policy, if you'd like to know more. I use Mailchimp to send emails. You can learn more about their privacy practices here.

Join the discussion

comments powered by Disqus