Testing

Data-driven testing in PHPUnit

There must be a lot of developers faced with a problem when they have a lot of tests in their project that duplicate their functionality.

Posted: 8 months ago
Data-driven testing in PHPUnit

There must be a lot of developers faced with a problem when they have a lot of tests in their project that duplicate their functionality. At the initial stage, this is not noticeable, but the farther, the more and more this poses a problem.

A small example to get you started:

Let's say you write a library that transforms Markdown into HTML. In your tests, similar cases will most likely be registered:

  • Transformation for h1 tags
  • Transformation for Code Blocks
  • Transformation for links

Most developers will follow a simple path, and will make a separate method for each of these cases that will test it:

class ExampleTest extends TestCase
{
  /** @test */
  public function transform_title()
  {
      //
  }

  /** @test */
  public function transform_code()
  {
      //
  }

  /** @test */
  public function transform_links()
  {
      //
  }
}

What is the problem?

In the example above, most methods will most likely have similar functionality: receive input data, call the method of the desired transformer, check the result.

Now imagine that we have not 3, but 50 transformers. The test file will turn into a huge something, completely unreadable and unsupported.

Ok Google: how will Data-driven testing help me and what is it all about?

Data-Driven Testing (DDT) is another approach to testing, in which the input test receives a data set with input and output parameters, while the test itself does not know exactly what it is testing. This approach avoids the appearance of tests with duplicate functionality, and focus on the specific data that is being tested.

Example dataset for a test:

$dataSet = [
  ['some A', TransformerA::class, 'expected output A'],
  ['some B', TransformerA::class, 'expected output B'],
  ['some C', TransformerA::class, 'expected output C'],
]

As you can see, we indicate: input data, the transformer that we want to test and the result that we expect to get. The test code might look something like this:

class ExampleTest extends TestCase
{
  /** @test */
  public function transform_content()
  {
    $dataSet = $this->dataSet();

    foreach($dataSet as $data) {
      $value = $data[0]
      $class = new $data[1];
      $expected = $data[2]
      $this->assertEquals($expected, $class->process($value));
    }
  }

  public function dataSet()
  {
    return [
      ['# Title #', TitleTransformer::class, '<h1>Title</h1>'],
      ['`$var`', CodeTransformer::class, '<code>$var</code>'],
      ['[Link](http://laravel-news.com)', TitleTransformer::class, '<a href="http://laravel-news.com">Link</a>']
    ]
  }
}

It is understood that all transformers have the same interface (which is quite expected and logical), in which there is a process() method for processing input data.

Data-driven testing and PHPUnit

Although the example above looks pretty good if you use PHPUnit, you can use the built-in functionality that adds a little sugar to our barrel.

PHPUnit supports annotations that make it easier to read and maintain code. For example, instead of:

public function test_some_method_name()
{

}

We can use the @test annotation and write this:

/** @test **/
public function some_method_name()
{

}

The @test annotation eliminates the test_ prefix in the test name.

But we are interested in another annotation: @dataProvider. It is used like this:

class ExampleTest extends TestCase
{
  /*
   * @test
   * @dataprovider dataSet
   */
  public function transform_content($input, $class, $expected)
  {
    $class = new $class;
    $this->assertEquals($expected, $class->process($value));
  }

  public function dataSet()
  {
    return [
      ['# Title #', TitleTransformer::class, '<h1>Title</h1>'],
      ['`$var`', CodeTransformer::class, '<code>$var</code>'],
      ['[Link](http://laravel-news.com)', TitleTransformer::class, '<a href="http://laravel-news.com">Link</a>']
    ]
  }
}

Is it true beautiful? PHPUnit itself loops through the array which returns the dataSet() method and calls the test method with the necessary parameters! It remains for us to focus on adding new data to the dataSet() method, rather than producing dozens of methods with the same functionality.

In case of an error, you will see something like this:

There was 1 failure:

1) TestsUnitExampleTest::transform_content with data set #1 ('# Title #', TitleTransformer::class, '<h1>Title</h1>')
...

Pretty sub-conclusion, isn't it? However, we can go even further and add keys for our data:

public function dataSet()
{
  return [
    'Transform titles' => ['# Title #', TitleTransformer::class, '<h1>Title</h1>'],
    'Transform code text' => ['`$var`', CodeTransformer::class, '<code>$var</code>'],
    'Transform links' => ['[Link](http://laravel-news.com)', TitleTransformer::class, '<a href="https://laravel-news.com">Link</a>']
  ]
}

And then the error will look like this:

There was 1 failure:

1) TestsUnitExampleTest::transform_content with data set "Transform titles" ('# Title #', TitleTransformer::class, '<h1>Title</h1>')
...

Small hint

You can even write your own iterator for your data. A class that implements the Iterator interface:

use PHPUnitFrameworkTestCase;

class CustomIterator implements Iterator {
  protected $key = 0;
  protected $current;

  public function __construct()
  {
    //
  }

  public function __destruct()
  {
    //
  }

  public function rewind()
  {
    //
  }

  public function valid() {
    //
  }

  public function key()
  {
    //
  }

  public function current()
  {
    //
  }

  public function next()
  {
    //
  }
}