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 Feature

04.18.2011
| 5191 views |
  • submit to reddit

There are cases when you want to break the parallelism between production code classes and test cases. Let's start from some of these scenarios.

You may have too much things to test in a single class, which presents many methods in its public Api. Or you may want to get a better picture of what a group of classes does.

Then break down the giant Testcase Class derived from the Testcase Class per Class pattern in two or more test cases.

The criterion for dividing the Test Methods is their relatedness to different features. By feature, a generic term, I mean a set of operations used together, but we'll see more examples of this division. The result of breaking up a large test case is lots of smaller and more understandable tests, and a better picture of the responsibilities of the SUT.

This 600-line class smells...

I bet you have said that too many features can be a smell of a God class. And indeed you're right; but if the test is a functional or end-to-end one is quite handy to divide Testcases by feature: you're testing the whole application and you cannot just divide end-to-end tests by the class they act on. They act on a whole copy of the system.
The same goes for certain all-encompassing classes such as Facades. This pattern comes handy for tests that do not have a natural single production code class equivalent.

Implementation

Transitioning from Testcase per Class to Testcase per Feature should not be really difficult as test methods should already be independent. If you have common code shared between test cases, you can use a superclass.

Since you have now different test case classes, choose a good convention for naming. You should nevertheless name them with a 'Test' suffix and keep the -Test.php file name, so that PHPUnit automatically sees them.

A common way of refactoring towards this pattern is to duplicate the class. Then, renaming takes place along with the deletion of everything that is responsibility of another class (Test Methods of other features) and that is not used in the current one (unrelated Test Utility Methods).

Variations

Again, remember these are examples of a class not tested in isolation, but more of tests required for an application. So many features to test does not imply a God class, only a system which does many things, which is normal when you're paid to develop it (although many of your tests should be at the lowest possible level, which is often the unit one.)

  • a Testcase Class per Method is used when methods are really complex to call, with many parameters. Different methods are addressed by different classes.
  • a Testcase Class per User Story is used for high level tests which address the various acceptance criteria for a story; in the xUnit patterns book it is said that this division prevents commit conflicts as long as working on user stories is not simultaneous.

The ultimate criteria is to keep cohesive Testcase Classes: if you have many Test Methods and they can be logically divided into groups, you can break the Testcase into pieces. A rule of thumb for measuring cohesion is checking how many methods use a Testcase field: when fields are referencedonly in a small subset of methods, those methods become candidates for extraction.

Example

The code sample of today is related to a small e-commerce Facade, which is tested at the acceptance level. Since the Facade hides the underlying system, it has a lot of features to test: you can consider these tests as functional over the whole object graph referred internally by the Facade.

Here's one of the two tests:

<?php
require_once 'Facade.php';

class FacadeBuyingTest extends PHPUnit_Framework_TestCase
{
    public function setUp()
    {
        /**
         * For simplicity I instance the object with new, but in reality
         * it would be an object graph created by a Factory or a DI
         * container.
         */
        $this->facade = new Facade();
        // other setup code: database of the users for example
    }

    public function testUserCanBuyAnItem()
    {
        $userId = 42;
        $productId = 1000000;
        $this->facade->buy($userId, $productId);
    }

    public function testUserCanPayAnItem()
    {
        $userId = 42;
        $transactionId = 200000000;
        $this->facade->payFor($userId, $transactionId);
    }
}

And here is the other:

<?php
require_once 'Facade.php';

class FacadeSearchTest extends PHPUnit_Framework_TestCase
{
    public function setUp()
    {
        $this->facade = new Facade();
        // other setup code: product database for example
    }

    public function testUserCanSearchForProducts()
    {
        $products = $this->facade->search('hard disks');
        $this->assertTrue(count($products) > 0);
        // ... more assertions
    }

    public function testUserCanSearchForProductsAndExcludeUnwantedOnes()
    {
        $products = $this->facade->search('hard disks -ssd');
        $this->assertTrue(count($products) == 1);
        // ... more assertions
    }
}

Sometimes you can't split a class, but you can always split its tests.

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

Comments

Don Zampano replied on Mon, 2011/04/25 - 11:08am

Shouldn't this be the preferred way of testing (or at least test-first) anyawy?

The ideal class has only one public method I test to.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.