Testcase Class per Class
The book has now been published and the content of this chapter has likely changed substanstially.Please see page 617 of xUnit Test Patterns for the latest information.
How do we organize our Test Methods onto Testcase Classes?
We put all the Test Methods for one system under test (SUT) class onto a single Testcase Class.
Sketch Testcase Class per Class embedded from Testcase Class per Class.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 Class is a simple way to start off organizing our tests.
How It Works
We create a separate Testcase Class for each class we wish to test. Each Testcase Class acts as a home to all the Test Methods that are used to verify the behavior of the SUT class.
When To Use It
Using a Testcase Class per Class is a good starting point when we don't have very many Test Methods or we are just starting to write tests for our SUT. As the number of tests grows and we get a better understanding of our test fixture requirements, we may want to split the Testcase Class into multiple classes resulting in either Testcase Class per Fixture (page X) (if we have a small number of frequently used starting points for our tests) or Testcase Class per Feature (page X) (if we have several distinct features to test.) As Kent Beck would say: "Let the code tell you what to do!"
Implementation Notes
Choosing a name for the Testcase Class is pretty simple: just use the SUT class name possibly prefixed or suffixed with "Test". The method names should try to capture at least the starting state (fixture) and feature (method) being exercised along with a summary of the parameters be passed to the SUT. Given all this, we likely won't have "room" for the expected outcome in the method name so we'll have to ask the test reader to look at the Test Method body to determine the expected outcome.
The creation of the fixture is the primary implementation concern when using a Testcase Class per Class. This is because there will be conflicting fixture requirements amongst the various Test Methods and that makes use of Implicit Setup (page X) difficult forcing us to use either Inline Setup (page X) or Delegated Setup (page X). A second consideration is how to make the nature of the fixture visible within each test method to avoid Obscure Tests (page X). Delegated Setup (using Creation Methods (page X)) tends to lead to more readable tests unless the Inline Setup is dead simple.
Example: Testcase Class per Class
Here's an example of using 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.
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 { 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 testSchedule_FromAwaitingApprovalState() 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()); } } public void testDeschedule_FromScheduledState() throws Exception { Flight flight = FlightTestHelper.getAnonymousFlightInScheduledState(); flight.deschedule(); assertTrue("isUnscheduled()", flight.isUnscheduled()); } public void testDeschedule_FromUnscheduledState() throws Exception { Flight flight = FlightTestHelper.getAnonymousFlightInUnscheduledState(); try { flight.deschedule(); fail("not allowed in unscheduled state"); } catch (InvalidRequestException e) { assertEquals("InvalidRequestException.getRequest()", "deschedule", e.getRequest()); assertTrue("isUnscheduled()", flight.isUnscheduled()); } } public void testDeschedule_FromAwaitingApprovalState() throws Exception { Flight flight = FlightTestHelper.getAnonymousFlightInAwaitingApprovalState(); try { flight.deschedule(); fail("not allowed in awaitingApproval state"); } catch (InvalidRequestException e) { assertEquals("InvalidRequestException.getRequest()", "deschedule", e.getRequest()); assertTrue( "isAwaitingApproval()", flight.isAwaitingApproval()); } } public void testApprove_FromScheduledState() throws Exception { Flight flight = FlightTestHelper.getAnonymousFlightInScheduledState(); try { flight.approve("Fred"); fail("not allowed in scheduled state"); } catch (InvalidRequestException e) { assertEquals("InvalidRequestException.getRequest()", "approve", e.getRequest()); assertTrue("isScheduled()", flight.isScheduled()); } } public void testApprove_FromUnsheduledState() throws Exception { Flight flight = FlightTestHelper.getAnonymousFlightInUnscheduledState(); try { flight.approve("Fred"); fail("not allowed in unscheduled state"); } catch (InvalidRequestException e) { assertEquals("InvalidRequestException.getRequest()", "approve", e.getRequest()); assertTrue( "isUnscheduled()", flight.isUnscheduled()); } } public void testApprove_FromAwaitingApprovalState() throws Exception { Flight flight = FlightTestHelper.getAnonymousFlightInAwaitingApprovalState(); flight.approve("Fred"); assertTrue("isScheduled()", flight.isScheduled()); } public void testApprove_NullArgument() throws Exception { Flight flight = FlightTestHelper.getAnonymousFlightInAwaitingApprovalState(); try { flight.approve(null); fail("Failed to catch no approver"); } catch (InvalidArgumentException e) { assertEquals("e.getArgumentName()", "approverName", e.getArgumentName()); assertNull( "e.getArgumentValue()", e.getArgumentValue()); assertTrue( "isAwaitingApproval()", flight.isAwaitingApproval()); } } public void testApprove_InvalidApprover() throws Exception { Flight flight = FlightTestHelper.getAnonymousFlightInAwaitingApprovalState(); try { flight.approve("John"); fail("Failed to validate approver"); } catch (InvalidArgumentException e) { assertEquals("e.getArgumentName()", "approverName", e.getArgumentName()); assertEquals("e.getArgumentValue()", "John", e.getArgumentValue()); assertTrue( "isAwaitingApproval()", flight.isAwaitingApproval()); } } } Example TestcaseClassPerClass embedded from java/com/clrstream/ex3/solution/flightbooking/domain/test/FlightStateTest.java
This example uses Delegated Setup 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. Even the "big picture" provided by our IDE is not that illuminating; we can see the test conditions being exercise but cannot tell what the expected outcome should be without looking at the method bodies:
Sketch Testcase Class per Class ScreenShot embedded from Testcase Class per Class ScreenShot.gif
Copyright © 2003-2008 Gerard Meszaros all rights reserved