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 638 posts at DZone. You can read more from them at their website. View Full User Profile

Practical PHP Testing Patterns: Testcase Class per Fixture

04.20.2011
| 5131 views |
  • submit to reddit

Apart from Testcase Class per Feature, a different division of Test Methods into multiple Testcase Classes is also possible. The criteria for division could be roughly speaking the setUp() code required by methods, which means the fixtures they need in order to run.

A benefit of this division rule, for example, is that you will be able to easily see if you are testing all the interesting operation for a certain initial state; there won't be noise deriving from other unrelated tests on the same class/object group.

Behavior-Driven Development describes the specification of a software system by dividing test cases exactly like this, and calling them scenarios. There is no required mapping between scenarios and classes like in Testcase Class per Class: each scenario naturally corresponds to a different fixture and setUp() method, both in classic xUnit testing and in BDD frameworks.

When should you try it

A refactoring towards Testcase Class per Fixture is commonly executed when you feel you need multiple setUp() in a Testcase Class. Creating Test Utility Methods that contain it results in duplication of calling code, while setUp() has not to be called explicitly. Another common case is when the current setUp() is not used by many Test Methods. For example, you create a giant object on $this which is used only in 2 out of 10 Test Methods.

When there are multiple initial states in a test case, they also get duplicated in methods names: suddenly 5 methods start with whenSUTisEmpty, 5 start with whenSUTisFull, and 5 start with whenSUTisClosed.

A too large setUp() may be a sign of too many responsibilities of the SUT, but it would be too simple to shout just extract another class, this is too big and it smells. In case of groups of objects, or Facades, it's not a sign of too many responsibilities but of a lack of abstraction. In Growing Object-Oriented Software a principle is written which goes: The total must be easy to use that the sum of its parts.

However, like for Testcase Class per Feature, when you're testing at the end-to-end level it's common to have an high number of tests: not in the hundreds, but higher than the average number of unit tests for a single class. Both patterns provide you a way to divide these tests when a division by class under test does not cut it.

Thus when we test too much at the integration level we find ourselves managing dozens of setups. In this case fewer unit tests will be more helpful to reach the same coverage of code and scenarios. Consider for example testing a calculator: you have to cover theoretically every combination of expressions, like a sum plus a product, or a products of subtraction and so on. If you are able to test the objects representing the single operations, you'll get away with a lower number of unit tests.

Implementation

The setUp() and teardown() hook methods, present in PHPUnit and in all xUnit frameworks, strongly support this pattern. Of course if you create multiple setUp() in different classes, tearDown() has to be separated too, when present.

As always, use instance variables on $this to keep references to objects between setUp(), tests and teardown(). Remember that each test method is executed on a different object for the sake of isolation.

Remember to perform the Rename Method refactoring and other substitutions in the test methods, to eliminate references to the initial state. This starting point will be assumed and should be cited only once, in the name of the Testcase Class.

Example

The example shows you how to go from a Testcase Class with many different initial states of the Facade to a few Testcase Classes really more cohesive.

This is the original test: I have inserted some comments instead of code to keep it bearable.

<?php
require_once 'Facade.php';

class FacadeMonolithicTest extends PHPUnit_Framework_TestCase
{
    public function setUp()
    {
        $pdo = new PDO('sqlite::memory:');
        $this->facade = new Facade($pdo);
        // other setup code: CREATE TABLE statements

        $this->transactionNumber = 42;
        $this->dummyCreditCard = '1234567812345678';
    }

    public function testGivenAnEmptyListOfProductsUsersAreDisplayedWithAnErrorMessage()
    {
        $result = $this->facade->search('product name');
        $this->assertContains('Sorry for the inconvenience.', $result);
    }

    /**
     * @expectedException ProductNotExistentException
     */
    public function testGivenAnEmptyListOfTransactionsUsersCannotPayForNotLoadedItems()
    {
        $result = $this->facade->payFor($userId = 1, $transactionId = null);
    }

    public function testGivenAListOfProductsUsersAreDisplayedWithTheListOfRelatedProducts()
    {
        // INSERT on products
        $result = $this->facade->search('product name');
        $this->assertTrue(count($result) > 10);
    }

    public function testUsersMayPayAnActiveTransactionWithACreditCard()
    {
        // INSERT on products and transactions
        $result = $this->facade->payFor($this->transactionNumber, $this->dummyCreditCard);
        $this->assertTrue($result);
    }

    public function testUsersCannotPayAnActiveTRansactionWithANotValidCreditCardNumber()
    {
        // INSERT on products and transactions
        $result = $this->facade->payFor($this->transactionNumber, $tooLong = '12345678123456781234567812345678');
        $this->assertFalse($result);
    }
}

We can divide the Testcase according to the state: an empty application, an application filled with products, and an user with an active transaction (an object to pay; not a database transaction).

<?php
require_once 'Facade.php';

class FacadeProductListEmptyTest extends PHPUnit_Framework_TestCase
{
    public function setUp()
    {
        $pdo = new PDO('sqlite::memory:');
        $this->facade = new Facade();
        // other setup code: CREATE TABLE statements
    }

    public function testUsersAreDisplayedWithAnErrorMessage()
    {
        $result = $this->facade->search('product name');
        $this->assertContains('Sorry for the inconvenience.', $result);
    }

    /**
     * @expectedException ProductNotExistentException
     */
    public function testUsersCannotPayForNotLoadedItems()
    {
        $result = $this->facade->payFor($userId = 1, $transactionId = null);
    }
}
<?php
require_once 'Facade.php';

class FacadeStandardProductListTest extends PHPUnit_Framework_TestCase
{
    public function setUp()
    {
        $pdo = new PDO('sqlite::memory:');
        $this->facade = new Facade();
        // other setup code: CREATE TABLE statements and INSERT
    }

    public function testUsersAreDisplayedWithTheListOfRelatedProducts()
    {
        $result = $this->facade->search('product name');
        $this->assertTrue(count($result) > 10);
    }
}

<?php
require_once 'Facade.php';

class FacadeProductBoughtTest extends PHPUnit_Framework_TestCase
{
    private $transactionNumber;

    public function setUp()
    {
        $pdo = new PDO('sqlite::memory:');
        $this->facade = new Facade();
        // other setup code: CREATE TABLE statements and INSERT
        // also insert the transaction
        $this->transactionNumber = 42;
        $this->dummyCreditCard = '1234567812345678';
    }

    public function testUsersMayPayTheTransactionWithACreditCard()
    {
        $result = $this->facade->payFor($this->transactionNumber, $this->dummyCreditCard);
        $this->assertTrue($result);
    }

    public function testUsersCannotPayWithANotValidCreditCardNumber()
    {
        $result = $this->facade->payFor($this->transactionNumber, $tooLong = '12345678123456781234567812345678');
        $this->assertFalse($result);
    }
}

The description of the initial state vanish from methods names, and each Testcase is short andeasy to check for coverage of business case (can pay with X, cannot pay with Y, and so on).

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.)