Agile Zone is brought to you in partnership with:

I am a programmer and architect (the kind that writes code) with a focus on testing and open source; I maintain the PHPUnit_Selenium project. I believe programming is one of the hardest and most beautiful jobs in the world. Giorgio is a DZone MVB and is not an employee of DZone and has posted 636 posts at DZone. You can read more from them at their website. View Full User Profile

Practical PHP Testing Patterns: Derived Value

06.01.2011
| 4282 views |
  • submit to reddit

Tests are example scenarios were we exercised our code: a large enough number of examples results in a complete specification and coverage. We are exploring how to express the values that the examples consist of, being them string, integers, arrays or any kind of object.

In the Derived Value pattern, values are not hardcoded into tests like for Literal Value, but they are derived from other values with some procedure, which may be inline code, but also methods or even external helpers.

The issues

Derived Value is not to be introduced lightly: if not used correctly, its disadvantages outweigh its benefits (which will be more clear in the different variations). By the way, its goal is to eliminate duplication by producing values from a fixed source on the fly, instead of making the programmer generate them once and for all when writing the test.

If Derived Values follow the same logic of the production code, the same bug may appear in the production code and in the test at the same time, defeating our defect localization goals. In general, you must pay attention that bugs or regressions in the derivations may cause your test to become a false positive.

Variations

Each specialization of the pattern attempts to remove a bit of duplication by introducing logic in the test. The trade-off is between a simpler test harder to modify and a complex test whose logic may hide bugs.

  • Derived Input: one of the input is calculated from other inputs to made the relationship clear and avoid redundancy, keeping the input data automatically consistent. For example, if you have to extract all pairs of cards from a deck of $cards = 52 cards, you won't extract $pairs = 26 pairs but $pairs = $cards / 2 pairs.
  • One Bad Attribute: you may create a valid object, and then make one attribute invalid to focus the test on this aspect. Other values are not taken into consideration by the reader, and do not have to be updated by the programmers after unrelated changes to them. A Creation Method helps to centralize the logic for creating the valid version.
  • Derived Expectation: the expected value is calculated instead of being predefined. A simple case of this was seen in the Literal Value article (an inline multiplication and sum), but in this variation the logic may be extracted in one method (which would be exactly a replica of production code).

You may use some production code to test other production code, in case it simplifies the assertions. For example, consider testing objects equality: you can produce a new, target object and make PHP do the work by comparing them with ==. With this mechanism you become a little vulnerable to regressions in that class, but you avoid exposing fields via getters just for making a comparison on the test:

$color = ...
$expected = new Color(0, 0x66, 0x99);
$this->assertEquals($expected, $color);

Example

The sample code shows you the three variations, each with an explanation of the issues of the alternative solution: hardcoding (which would not be bad per se).

<?php
class DerivedValueTest extends PHPUnit_Framework_TestCase
{
    /**
     * In this example, $center is computed to avoid a test bug because someone
     * updated $array and forgot the $center variable.
     * In short, to remove duplication.
     */
    public function testADerivedInputRemovesDuplicationAndImprovesClarity()
    {
        $array = array('a', 'b', 'center', 'd', 'e');
        $center = floor(count($array) / 2);
        array_flip($array);
        $this->assertEquals('center', $array[$center]);
    }

    /**
     * A data structure HTTP request is created and then invalidated.
     * Compare this with redefining the array every time and make sure
     * the other keys are still valid.
     * I use an array in place of an object for brevity in this explanation.
     */
    public function testOneBadAttributeValuesAreBuiltFromValidOnes()
    {
        $request = $this->createGetRequest(); // hides module and controller details
        $request['action'] = 'this-will-cause-a-404';
        $this->markTestIncomplete('When calling the SUT, the action should be judged as invalid.');
    }

    private function createGetRequest()
    {
        return array(
            'module' => 'default',
            'controller' => 'index',
            'action' => 'index'
        );
    }

    /**
     * If we have a "casting-out-nines" test to quickly check our results, 
     * at least as in a smoke test, we can use a derived expectation.
     * Since our test cases are usually many simplified scenarios where
     * to exercise the production code, deriving the right result shouldn't 
     * require as much code as in there. If you have to mirror the production 
     * code, stop: the test would become too tied with the implementation, to 
     * the point of reproducing its bugs.
     * These tests come handy when the amount of data is huge (e.g. multimedia 
     * files) and generating a sanity check is by far faster than hardcoding
     * everything by hand (the image contains a car at pixel (45; 100) ...).
     */
    public function testADerivedExpectationLetsYouAssertWithoutHardcoding()
    {
        $array = array();
        $elements = 4; // our unique parameter
        for ($i = 1; $i <= $elements; $i++) {
            $array[] = $i;
        }
        $expectedTotal = $elements * ($elements + 1) / 2; // Gauss formula
        $total = array_sum($array);
        $this->assertEquals($expectedTotal, $total);
    }
}
Published at DZone with permission of Giorgio Sironi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)