melp.nl

< Return to main page

PHPUnit: Closures as dataProviders help with repetitive code

Since PHP5.3 we have closures. The concept behind closures is unbelievably powerful, and even though PHP has struggled with the concept of typing, callables and whatnot, the main concept of closures remains: passing logic in stead of data, or even: as if it were data. In my previous post I argued that functional and declarative programming will prevail over pure OO on the long run. This post proves another example of why.

One simple example of proving closures' power is in the concept of PHPUnit's dataProviders. A data provider provides data to a test, in which a test consumes the data and asserts that the code is ok. As data can also be closures, you can also alter the execution of the tests appropriate to the data you want to test, without actually altering the test. I'll show you an example 1.

Consider the following class:

#!php
class FooBuilder {
    function add($property, $value) {
        $this->properties[$property][] = $value;
        return $this;
    }


    function remove() {
        if (!empty($this->properties[$property])) {
            array_pop($this->properties[$property]);
            if (count($this->properties[$property]) == 0) {
                unset($this->properties[$property]);
            }
        }
        return $this;
    }


    function build() {
        return $this->properties;
    }
}

Though there is no obvious use case for this class, the pattern is simple: we have some builder with methods to create something, and the builder provides a fluent interface to do so. This is also very common for query builders, including jQuery. With such a pattern, there is a meriad of possibilities to use this code, with at least as much edge conditions. The order in which the methods are executed might influence the way it is implemented, what would happen if properties are added, removed and then removed again, etc etc.

These edge cases won't be displayed by simple use of code coverage, and introducing separate tests for each of these conditions would be cumbersome, having to introduce separate test names for each of the cases.

So, here's where closures come in handy. We'll simply call the test testBuildingWithFluentInterfaceWillResultInExpectedArray:

#!php
class FooBuilderTest extends \PHPUnit_Framework_TestCase {
    /**
     * @dataProvider builderCases
     */
    function testBuildingWithFluentInterfaceWillResultInExpectedArray($expected, $builderImpl, $description) {
        $builder = new FooBuilder();
        $this->assertEquals($expectedArray, $builderImpl($builder)->build(), "$description failed");
    }


    function builderCases() {
        return array(
            array(
                array(),
                function (FooBuilder $builder) {
                    return $builder;
                },
                "Initial construction"
            ),
            /* .... */
            array(
                array('a' => array('b', 'c'), 'x' => array('y', 'z')),
                function (FooBuilder $builder) {
                    return $builder
                        ->add('a', 'b')
                        ->add('x', 'y')
                        ->add('x', 'z')
                        ->add('a', 'c')
                    ;
                },
                "Adding different properties multiple times in a different order"
            ),
            /* .... */
            array(
                array(),
                function (FooBuilder $builder) {
                    return $builder
                        ->add('x', 'y')
                        ->add('x', 'z')
                        ->remove('x')
                        ->remove('x')
                    ;
                },
                "Adding different properties multiple times in a different order"
            ),
        );

}

This way, you´ll have a clear view of all the test cases and expected output, without having to only provide data through your data provider, but logic too. Clear separation of use case and consumer logic makes the test very clear and easily extendable.

Hope this offers more food for creativity in your tests :)


  1. The example is available as a GIST ↩︎


< Return to main page


You're looking at a very minimalistic archived version of this website. I wish to preserve to content, but I no longer wish to maintain Wordpress, nor handle the abuse that comes with that.