.com
Hosted by:
Unit testing expertise at your fingertips!
Home | Discuss | Lists

Testcase Class per Feature

The book has now been published and the content of this chapter has likely changed substanstially.
Please see page 624 of xUnit Test Patterns for the latest information.
How do we organize our Test Methods onto Testcase Classes?

We group the Test Methods onto Testcase Classes based on which testable feature of the SUT they exercise.

Sketch Testcase Class per Feature embedded from Testcase Class per Feature.gif

As the number of Test Methods (page X) grows, we need to decide on which Testcase Class (page X) to put each Test Method. The choice of test organization strategy affects how easy it is to get a big picture of our tests. It also affects our choice of fixture setup strategy.

Using a Testcase Class per Feature gives us a systematic way to break up a large Testcase Class into several smaller ones without having to change our Test Methods.

How It Works

We group our Test Methods onto Testcase Classes based on which feature of the Testcase Class they verify. This allows us to have smaller Testcase Classes and to see at a glance all the test conditions for a particular feature of the class.

When To Use It

We can use a Testcase Class per Feature when we have a significant number of Test Methods and we want to make the specification of each feature of the system under test (SUT) more obvious. Unfortunately, Testcase Class per Feature does not make each individual Test Method any simpler or easier to understand; only Testcase Class per Fixture (page X) helps on that front. It doesn't make much sense to use Testcase Class per Feature of each feature of the SUT only requires one or two tests; in that case we can stick with a single Testcase Class per Class (page X).

Note that having a large number of features on a class is a "smell" indicating the possibility of the class having too many responsibilities. Testcase Class per Feature is most commonly used when writing customer tests for methods on a service Facade[GOF]. The main variations of Testcase Class per Feature are:

Variation: Testcase Class Per Method

When a class has methods that take a lot of different parameters, we may have many tests for the one method. We can group all of these Test Methods onto a single Testcase Class Per Method and put the rest of the Test Methods onto one or more other Testcase Classes.

Variation: Testcase Class Per Feature

A "feature" of a class is typically a single operation or function but it may also be a set of related methods that operate on the same instance variable of the object. For example, the set and get methods of a Java Bean would be considered a single (and trivial) "feature" of the class the contains those methods. Similarly, a Data Access Object[CJ2EEP] would provide methods to both read and write objects; it is hard to test these in isolation so we can treat the reading and writing of one kind of object as a feature.

Variation: Testcase Class Per User Story

If we are doing highly incremental development such as we might do when using eXtreme Programming, it can be useful to put the new Test Methods for each story into a different Testcase Class. This prevents commit conflicts between people who are working on different stories that affect the same SUT class. This may or may not end up being the same as Testcase Class Per Feature or Testcase Class Per Method depending on how we partition our user stories.

Implementation Notes

Since each Testcase Class represents the requirements for a single feature of the SUT, it makes sense to name the Testcase Class based on the feature it verifies. Similarly, we can name each test method based on what test condition of the SUT is being verified. This allows us to see all the test conditions at a glance by merely looking at the names of all the Test Methods of the Testcase Class.

One consequence of using Testcase Class per Feature is that we end up with a larger number of Testcase Classes. We still want to run all the tests for this class so we should put all the Testcase Classes into a single nested folder, package or name spaces. We can use an AllTests Suite (see Named Test Suite on page X) to aggregate all the Testcase Classes into a single test suite if we are using Test Enumeration (page X).

Motivating Example

Here's an example that uses Testcase Class per Class to structure the Test Methods for a Flight class that has three states (Unscheduled, Scheduled and AwaitingApproval) and four methods (schedule, requestApproval, deSchedule and approve. Since the class is stateful, we need at least one test for each state for each method. (In the interest of saving trees, I've omitted many of the method bodies; please refer to Testcase Class per Class for the full listing.)

public class FlightStateTest extends TestCase {
  
   public void testRequestApproval_FromScheduledState() throws Exception {
      Flight flight = FlightTestHelper.getAnonymousFlightInScheduledState();
      try {
         flight.requestApproval();
         fail("not allowed in in scheduled state");
      } catch (InvalidRequestException e) {
         assertEquals("InvalidRequestException.getRequest()", "requestApproval",
                      e.getRequest());
         assertTrue("isScheduled()", flight.isScheduled());
      }
   }
  
   public void testRequestApproval_FromUnsheduledState() throws Exception {
      Flight flight = FlightTestHelper.getAnonymousFlightInUnscheduledState();
      flight.requestApproval();
      assertTrue("isAwaitingApproval()", flight.isAwaitingApproval());
   }
  
   public void testRequestApproval_FromAwaitingApprovalState() throws Exception {
      Flight flight = FlightTestHelper.getAnonymousFlightInAwaitingApprovalState();
      try {
         flight.requestApproval();
         fail("not allowed in awaitingApproval state");
      } catch (InvalidRequestException e) {
         assertEquals("InvalidRequestException.getRequest()", "requestApproval",
                      e.getRequest());
          assertTrue("isAwaitingApproval()", flight.isAwaitingApproval());
      }
   }
  
   public void testSchedule_FromUnscheduledState() throws Exception {
      Flight flight = FlightTestHelper.getAnonymousFlightInUnscheduledState();
      flight.schedule();
      assertTrue( "isScheduled()", flight.isScheduled());
   }
  
   public void testSchedule_FromScheduledState() throws Exception {
    // I've ommitted the bodies of the rest of the tests to
    // save a few trees   
   }
}
Example TestcaseClassPerClassAbridged embedded from java/com/clrstream/ex3/solution/flightbooking/domain/test/FlightStateTest.java

This example uses Delegated Setup (page X) of a Fresh Fixture (page X) to achieve a more declarative style of fixture construction. Even so, this class is getting rather large and keeping track of all the Test Methods is getting to be a bit of a chore. Because the Test Methods on this Testcase Class require four distinct methods, it is a good example of a test that can be improved through refactoring to Testcase Class per Feature.

Refactoring Notes

We can reduce the size of each Testcase Class and make the names of the Test Methods more meaningful by converting them to Testcase Class per Feature. The first step is to determine how many classes we want to create and which Test Methods should go into each one. If some Testcase Classes will end up being smaller than others, it makes the job easier if we start by building the smaller classes. Next, we do an Extract Class[Fowler] refactoring to create one of the Testcase Classes and give it a name that describes the feature it exercises. Then, we do a Move Method[Fowler] refactoring on each test method that belongs in this new class along with any instance variables it uses.

We repeat this process for all but one of the features. When we are down to just one feature in the original Testcase Class, we can rename that class based on the feature it exercises. At this point, each of the Testcase Classes should compile and run but we aren't completely done. To get full benefit of Testcase Class per Feature we have one final step to carry out. We should do a Rename Method[Fowler] refactoring on each of the Test Methods to better reflect what the Test Method is verifying. We can remove any mention of the feature being exercised from each Test Method name since that should be captured in the name of the Testcase Class. That leaves us with "room" to include both the starting state (the fixture) and the expected result in the method name. If we have multiple tests for each feature with different method arguments, we'll need to find a way to include those in the method name too.

Another way to do this refactoring is simply to make several copies of the original Testcase Class and rename them as described above. Then we simply delete the Test Methods that aren't relevant for each class. We do need to be careful that we don't delete all copies of a Test Method; a less critical oversight is to leave a copy of the same method in several Testcase Classes.

Example: Testcase Class per Feature

Here, we have converted this set of tests to use Testcase Class per Feature.

public class TestScheduleFlight extends TestCase {
  
   public void testUnscheduled_shouldEndUpInScheduled() throws Exception {
      Flight flight = FlightTestHelper.getAnonymousFlightInUnscheduledState();
      flight.schedule();
      assertTrue( "isScheduled()", flight.isScheduled());
   }
  
   public void testScheduledState_shouldThrowInvalidRequestEx() throws Exception {
      Flight flight = FlightTestHelper.getAnonymousFlightInScheduledState();
      try {
         flight.schedule();
         fail("not allowed in scheduled state");
      } catch (InvalidRequestException e) {
         assertEquals("InvalidRequestException.getRequest()", "schedule",
                      e.getRequest());
         assertTrue(  "isScheduled()", flight.isScheduled());
      }
   }
  
   public void testAwaitingApproval_shouldThrowInvalidRequestEx() throws Exception {
      Flight flight = FlightTestHelper.getAnonymousFlightInAwaitingApprovalState();
      try {
         flight.schedule();
         fail("not allowed in schedule state");
      } catch (InvalidRequestException e) {
         assertEquals("InvalidRequestException.getRequest()", "schedule",
                      e.getRequest());
         assertTrue(  "isAwaitingApproval()", flight.isAwaitingApproval());
      }
   }
}
Example TestcaseClassPerFeature embedded from java/com/clrstream/ex3/solution/flightbooking/domain/flightstate/featuretests/TestScheduleFlight.java

Except for their names, the Test Methods really haven't changed. Because the names include the preconditions (fixture), feature being exercised and expected outcome, they do help us see the big picture (Tests as Documentation (see Goals of Test Automation on page X))when we look at the list of tests in our IDE's "outline view":



Sketch Testcase Class per Feature ScreenShot embedded from Testcase Class per Feature ScreenShot.gif


Page generated at Wed Feb 09 16:39:45 +1100 2011

Copyright © 2003-2008 Gerard Meszaros all rights reserved

All Categories
Introductory Narratives
Web Site Instructions
Code Refactorings
Database Patterns
DfT Patterns
External Patterns
Fixture Setup Patterns
Fixture Teardown Patterns
Front Matter
Glossary
Misc
References
Result Verification Patterns
Sidebars
Terminology
Test Double Patterns
Test Organization
Test Refactorings
Test Smells
Test Strategy
Tools
Value Patterns
XUnit Basics
xUnit Members
All "Test Organization"
Named Test Suite
Test Code Reuse:
--Test Utility Method
--Parameterized Test
Testcase Class Structure:
--Testcase Class per Feature
----Testcase Class Per Method
----Testcase Class Per User Story
--Testcase Class per Fixture
--Testcase Class per Class
Utility Method Location:
--Test Helper
--Testcase Superclass