A Vanilla TDD Example

We’ve been using C# until now for the example mapplet code, so to even things out a little we’ll switch to Java for this example. For our unit test framework, we’ll use the near-ubiquitous JUnit.[4.]

Let’s say we’re creating a web-based system for making hotel bookings that’s intended for use by travel agents rather than “Joe Public” users.[5.] As such, we’ll need a way of managing customers, creating new bookings, and tracking or modifying the bookings afterward.

Written up as user stories, these requirements would be of the following form:

As a <role>, I would like <feature> so that I can <value>.

For example:

As a travel agent, I would like a bookings facility so that I can book my customers into hotels.

This would then be broken down into tasks as follows:

  • Create a new customer.

  • Create a hotel booking for a customer.

  • Retrieve a customer (so that we can place the booking).

  • Place the booking.

We could also have another user story for checking how many bookings have been placed:

As a travel agent, I would like to be able to check how many bookings a customer has so that I can bill him accordingly.

We’ll use this small set of requirements as the basis for our first release. (In fact, for this example, we’ll just cover the first task, “Create a new customer.”)

For this first release, we don’t need to retrieve specific bookings or get a list of bookings for a customer. That would be in the next release (which, given that this is very limited functionality, would probably be as early as the next day!).

A nice benefit of TDD is that it encourages you to keep the presentation and model/controller code separate (ICONIX Process also does this by separating your classes into boundaries, controllers, and entities). While this makes the code easier to test (UI code is notoriously difficult to write automated tests for[6.]), it also produces a cleaner design with reusable, highly modular code. From a practical point of view, this means that we end up with lots of micro-tested source code containing all the business logic, over which the UI is placed like a thin shell.

Using TDD (even on an XP project), it would be perfectly reasonable to hold a quick design workshop prior to beginning coding for the day. This could include UML diagrams, lines and boxes, and so on—whatever gets the team to start thinking like designers. For this example, we’ll show the design emerge purely through the code and tests. This is primarily so that we can illustrate the contrast between vanilla TDD (seeing the design emerge by writing unit tests) and TDD combined with an up-front design process.

image from book
PROPERTIES OF A GOOD STORY

User stories generally need to follow the criteria summarized by the INVEST acronym:

  • Independent: The user story doesn’t rely on any other stories (see below).

  • Negotiable: There is a range of solutions here that the customer can choose from.

  • Valuable and vertical: The user story has value for the customer and touches every layer of the system.

  • Estimable: It is possible (after negotiation) to put a time estimate on how long the user story will take to implement.

  • Small: The user story shouldn’t take more than an iteration to implement.

  • Testable: We could easily write tests for this user story.

You may be thinking that the “Check how many bookings a customer has” story relies on the previous story, as you can hardly check the customer’s bookings count if you haven’t done the story to place bookings and create customers yet, but this is where the negotiations start.

The customer decides which story gets done first. Her tests and the functionality required to pass them at that time will determine the estimate for the story. It may be apparent to some that doing the other story first makes the greatest sense, but there are times when it doesn’t. For example, it may be that the customer needs to demonstrate this functionality in order to sell the system.

It is possible to implement this story with just enough of the customer and bookings entities for it to be testable and demonstrable.

image from book

Our First Test: Testing for Nothingness

Before we can create any bookings, we will, of course, need to be able to create customers. For this purpose we’ll use a CustomerManager class. Note this is really a bit of a leap on our part—following TDD to the letter, you might simply start with a class called CreateCustomer or CustomerCreator and its associated test class. But we know that pretty soon we’re also going to need to add functions for deleting, retrieving, and counting customers, and that we would later need to refactor all of these into a single “home” or “controller” class, so (to shorten the example a bit) let’s do that now.

Checking back to our shortlist of requirements in the previous section, for this first release we only need to be able to create new customers. We don’t yet need the ability to retrieve or search for customers. But for the purposes of testing that adding customers works, we’ll need to be able to retrieve customers as well (or at least count the number of customers that have been added to the database).

Here’s our CustomerManagerTest class with its first test:

 public class CustomerManagerTest extends TestCase {      public CustomerManagerTest(String testName) {          super(testName);      }      public static Test suite() {          TestSuite suite = new TestSuite(CustomerManagerTest.class);          return suite;      }      public void testCustomerCreated() {          Customer customer = CustomerManager.create("bob");          Customer found = CustomerManager.findCustomer("bob");          assertEquals("Should have found Bob", customer, found);      }  } 

As you can see, we’ve straightaway defined two methods for the CustomerManager interface: create(name) and findCustomer(name). First, let’s try to compile the test class (e.g., using NetBeans, we simply hit F9). Naturally the compilation fails.

Tip 

Compilation is the first level of testing. It tells us that the classes and methods either do or don’t exist. Imagine if you created tests for a class or method you hadn’t yet implemented and it compiled without errors. There’d be something wrong there—obviously the class or method already exists, and it’s much easier to rename it and fix things at this stage than it is to later try to figure out why the wrong method or class is being called.

Let’s implement the two methods on CustomerManager in the simplest way possible (at this stage, we just want to get the test class compiling):

 public class CustomerManager {      public static Customer create(String name) {          return null;      }      public static Customer findCustomer(String name) {          return null;      }  } 

We also need a “skeleton” Customer class so that the code compiles:

 public class Customer {  } 

Let’s run this test through JUnit’s TestRunner. Note that what we’re hoping for initially is a failure, because that verifies the test itself (it’s a two-way process). We can then write the code to make the test pass, confident that the test works.

However, the output that we get from JUnit is as follows:

 .  Time: 0  OK (1 test) 

Rather surprisingly, our test passed, even though there isn’t any code in there to create or retrieve a customer! So what’s going on? Each method in CustomerManager is currently just returning null, but our unit test is checking for equality. In the eyes of JUnit, null equals null. But adding a Customer and then trying to retrieve it—and getting null for our efforts—really should cause a test failure. So let’s add a test that makes this condition fail:

 public void testCustomerNotNull() {      Customer customer = CustomerManager.create("bob");      Customer found = CustomerManager.findCustomer("bob");      assertNotNull("Retrieved Customer should not be null.", found);  } 

This still doesn’t seem quite right. Our micro-test is actually testing two things here: the create method and the findCustomer method. However, it should be testing only the create method. So let’s get rid of our new testCustomerNotNull() method—it just doesn’t feel right—and redo our original testCustomerCreated() method. The test here should simply be to determine whether the returned customer value is null. Let’s rewrite the test to do just that:

 public void testCustomerCreated() {      Customer customer = CustomerManager.create("bob");      assertNotNull("New Customer should not be null.", customer);  } 

Running CustomerManagerTest through JUnit’s TestRunner now, we get a nice satisfying failure:

 .F.  Time: 0  There was 1 failure:  1) testCustomerCreated(CustomerManagerTest)junit.framework.AssertionFailedError:          New Customer should not be null.          at CustomerManagerTest.testCustomerCreated(CustomerManagerTest.java:14)  FAILURES!!!  Tests run: 1,  Failures: 1,  Errors: 0 

Now we have something to test against. When this test passes, it’s a safe bet that the creating customers part of the system works. Later when we want to be able to retrieve customers, we can add in some methods to test for that (including the testCustomerNotNull() method).

At the moment, our testCustomerCreated() test method is still failing, so we need to do something about that straightaway. We’ll add some code into CustomerManager:

 public class CustomerManager {      public static Customer create(String name) {          return new Customer(name);      }  } 

Attempting to compile this fails because Customer needs a new constructor to take the name, so let’s add that:

 public class Customer {      public Customer(String name) {      }  } 

Running this, our test passes.

So that’s just about that, at least for the first task (“Create a new customer”) on the list. Let’s pick another task from the list: “Retrieve a customer.” Ah, yes. We guess we’ll need our testCustomerNotNull() method back now

So we add that back into CustomerManagerTest and add the following method to CustomerManager:

 public static Customer findCustomer(String name) {      return new Customer("bob");  } 

Okay, but that’s cheating, right? Well, in theory at least, for a system that creates and deals with only one customer (who happens to be called “bob”), this is entirely sufficient. And at the moment, our unit tests are specifying a system that does exactly that. What we actually need is a test that tries adding a second customer. Remember, our goal here, as always, is to write a test that initially fails (to validate the test), so that we then write code to make it pass.

 public class CreateTwoCustomersTest extends TestCase {      private Customer bob;      private Customer alice;      public CreateTwoCustomersTest(java.lang.String testName) {          super(testName);      }      public static Test suite() {          TestSuite suite = new TestSuite(CreateTwoCustomersTest.class);          return suite;      }      public void setUp() {          bob = CustomerManager.create("bob");          alice = CustomerManager.create("alice");      }      public void testBobNotNull() {          Customer foundBob = CustomerManager.findCustomer("bob");          assertNotNull("Bob should have been found.", foundBob);      }      public void testAliceNotNull() {          Customer foundAlice = CustomerManager.findCustomer("alice");          assertNotNull("Alice should have been found.", foundAlice);      }  } 

It’s worth stressing again that the test methods are very fine-grained. As a general rule, each test method should contain only one “assert” statement (two at most), and it should test only one method on the class under test.

Tip 

If you find yourself adding more assert statements into one test method, you should think about splitting them into different methods (using setUp() and private member variables to create and track the test data). If you then find that setUp() is having to do different things for different tests, then immediately break the test class into two or more classes.

But that’s all well and good. This test class actually passes when run:

 ..  Time: 0.01  OK (2 tests) 

Evidently, just checking for null isn’t enough to make the tests prove that Bob or Alice was created. We need to prove that the Customer object returned from findCustomer is either Bob or Alice, as expected. So let’s replace the testBob/AliceNotNull tests as follows:

 public void testBobFound() {      Customer foundBob = CustomerManager.findCustomer("bob");      assertEquals("Bob should have been found.", foundBob, bob);  }  public void testAliceFound() {      Customer foundAlice = CustomerManager.findCustomer("alice");      assertEquals("Alice should have been found.", foundAlice, alice);  } 

Running this now, we get a failure, though not quite as expected:

 .F.F  Time: 0.01  There were 2 failures:  1) testBobNotNull(CreateTwoCustomersTest)junit.framework.AssertionFailedError:          Bob should have been found. expected:<Customer@6eb38a> but              was:<Customer@1cd2e5f>          at CreateTwoCustomersTest.testBobNotNull(CreateTwoCustomersTest.java:17)  2) testAliceNotNull(CreateTwoCustomersTest)junit.framework.AssertionFailedError:          Alice should have been found. expected:< Customer@1fee6fc> but              was:<Customer@1eed786>          at CreateTwoCustomersTest.testAliceNotNull(CreateTwoCustomersTest.java:22)  FAILURES!!!  Tests run: 2,  Failures: 2,  Errors: 0 

That Customer@6eb38a in the test output doesn’t look too helpful. It’s the default output from Java’s toString() method, so let’s override that in Customer to get something a bit more pleasant:

 public class Customer {      private String name;      public Customer(String name) {          this.name = name;      }      public String toString() {          return name;      }  } 

and rerun the test:

 .F.F  Time: 0.01  There were 2 failures:  1) testBobFound(CreateTwoCustomersTest)junit.framework.AssertionFailedError:          Bob should have been found. expected:<bob> but was:<bob>          at CreateTwoCustomersTest.testBobFound(CreateTwoCustomersTest.java:17)  2) testAliceFound(CreateTwoCustomersTest)junit.framework.AssertionFailedError:          Alice should have been found. expected:<bob> but was:<alice>          at CreateTwoCustomersTest.testAliceFound(CreateTwoCustomersTest.java:22)  FAILURES!!!  Tests run: 2,  Failures: 2,  Errors: 0 

The interesting thing here is that testAliceFound() is failing (which we’d expect, because CustomerManager.findCustomer(..) is “cheating” and always returning a new Bob), and testBobFound() is also failing (which we wouldn’t expect). testBobFound() is failing because the equals() method isn’t doing the right thing—currently it’s still using Java’s default Object.equals() implementation—so at this time, one Bob instance doesn’t equal another Bob instance.

Evidently, equality is a subjective thing in Bob-land. So we need to override the equals() method in Customer as follows:

 public boolean equals(Object obj) {      if (obj == null ||          !(obj instanceof Customer)) {          return false;      }      Customer customer = (Customer) obj;      return name.equals(customer.getName());  }  public String getName() {      return name;  } 

Notice also the new getName() method. This is the first time we’ve needed to retrieve the customer’s name, and we don’t add the method until we need it.

Our new equals() method first tests to see if the object being passed in is null or isn’t a Customer object. If either case is true, it’s a fair bet that the object isn’t equal. Our real equality test (the one we’re really interested in), though, is comparing the customer names.

If we compile and run the tests again now, we hope that the testAliceFound() method still fails, but at least the testBobFound() method passes:

 ..F  Time: 0.01  There was 1 failure:  1) testAliceFound(CreateTwoCustomersTest)junit.framework.AssertionFailedError:          Alice should have been found. expected:<bob> but was:<alice>          at CreateTwoCustomersTest.testAliceFound(CreateTwoCustomersTest.java:22)  FAILURES!!!  Tests run: 2,  Failures: 1,  Errors: 0 

Getting there—adding the Customer.equals() method passed the “should be the real Bob” test. But, as we expected, the “real Alice” test is still failing. We’d be kind of worried if it passed, in fact, because there’s currently no source code in CustomerManager that actually does anything.

Let’s do something about that next.

Making CustomerManager Do Something

We need to add some code to CustomerManager, so that it really does manage customers, which should make our failing “real Alice” test pass:

 public class CustomerManager {      private static HashMap customersByName = new HashMap();      public static Customer create(String name) {          Customer customer = new Customer(name);          customersByName.put(name, customer);          return customer;      }      public static Customer findCustomer(String name) {          return (Customer) customersByName.get(name);      }  } 

Running JUnit again now, we find that all the tests pass. Hurrah!

We also need to test the negative path: what happens if we try to find a customer that hasn’t been added to the system? Let’s add a test to find out:

 public void testCustomerNotFound() {      Customer jake = CustomerManager.findCustomer("jake");      assertNull("Jake isn't in the system so should be null.", jake);  } 

We’ll put this in a test class called CustomerNotFoundTest. With our new implementation of CustomerManager, this test passes straightaway.

Another useful test would be to try adding the same customer twice and ensure that we get a suitable error—but you get the general idea.

Now that we have the customer management side of things more or less wrapped up, we’ll now move onto managing the customer’s bookings.

Our First BookingManager Test: Testing for Nothingness

We need a component that returns the number of hotel bookings a customer has made. As we don’t yet have a way of placing bookings, our first test should be pretty straightforward: we can test that no bookings have yet been placed. We would, of course, expect this test to pass with flying colors!

image from book
DRIVING THE DESIGN FROM THE REQUIREMENTS

But how do we know that we need a component that returns the number of hotel bookings a customer has made? Using XP, there would need to be a user story that states simply

As a travel agent, I would like to be able to check the number of bookings a customer has made so I can bill him accordingly.

Using ICONIX Process, there would be a use case, either for specifically getting the number of bookings or for a larger task such as viewing a Customer Details screen (which would include the number of bookings made by that customer).

image from book

Let’s start by creating a JUnit test class called BookingManagerTest. Initially, this contains just our single test method:

 public void testInitialState() {      BookingManager.clearAll();      Customer bob = CustomerManager.create("Bob");      assertEquals("Bob hasn't booked any rooms yet.",                   0, BookingManager.getNumBookings(bob));  } 

To make this code pass its test, we’d write the minimum amount of code needed:

 public class BookingManager {      public static void clearAll() {      }      public static int getNumBookings(Customer customer) {          return 1;      }  } 

Note 

As with CustomerManager, we’re making BookingManager a very simple class consisting purely of static methods, to keep the example short. In reality, you’d probably make it a Singleton[7.] or instantiate it based on some context or other (e.g., a session or transaction ID).

Notice that getNumBookings() currently returns 1. This is deliberate because we initially want to see our unit test fail, to prove that it works. Running BookingManagerTest, then, we get this:

 .F  Time: 0  There was 1 failure:  1) testInitialState(BookingManagerTest)junit.framework.AssertionFailedError:          Bob hasn't booked any rooms yet. expected:<0> but was:<1>          at BookingManagerTest.testInitialState(BookingManagerTest.java:13)  FAILURES!!!  Tests run: 1,  Failures: 1,  Errors: 0 

Thus reassured, we can change getNumBookings() to return 0 (a more valid number at this stage), and the test passes:

 .  Time: 0.01  OK (1 test) 

image from book
RUN ALL YOUR TESTS REGULARLY, BY HOOK OR BY CROOK

Running through the example in this chapter, you’ll notice that we’re mostly running the test classes individually. In a production environment, it’s better (essential even) to run your entire test suite every time, instead of running individual test classes. This is actually a fundamental part of TDD: knowing that with each change you make, you haven’t broken the functionality that’s been written so far.

Of course, as the number of tests grows, it becomes more difficult to run an entire test suite, because the tests take longer and longer. When this happens, there are a few things you can do:

  • Look at ways of speeding up the tests (e.g., is the code that’s in each setUp() and teardown() method strictly necessary?).

  • Use mock objects to avoid having to connect to remote databases or application servers, which can slow tests down by several orders of magnitude.

  • Look for (and eliminate) duplicated tests.

  • Break code into separate modules that can be developed and tested independently (while avoiding the dreaded “big-bang integration” syndrome, where modules that have never been run together are jammed together near the end of a project and expected to somehow interoperate perfectly).

  • Run a separate build PC that regularly gets all the latest code, builds it, and runs the tests. This is an increasingly commonplace practice, and it is nowadays regarded as essential (not only by teams that follow TDD).

Of course, these practices also apply if you’re using TDD combined with ICONIX Process (see Chapter 12).

image from book

Adding a Vector: Testing for Something

All well and good so far. Our first BookingManager test passes. But of course that isn’t the whole story—we now need to add an additional test, a vector, to ensure that the correct number of bookings is returned:

 public void testOneBookingPlaced() {      BookingManager.clearAll();      Customer bob = CustomerManager.create("Bob");      Booking booking = new Booking (bob, Booking.WALDORF_ASTORIA);      BookingManager.placeBooking(bob, booking);      assertEquals("Bob has made one booking.", 1,                   BookingManager.getNumBookings(bob));  } 

To get the code to compile, we also need to add the placeBooking(..) method to BookingManager. For now, let’s add it as an empty method:

 public static void placeBooking(Customer customer, Booking booking) {  } 

This is also the first time we’ve needed a Booking class, so let’s create that now:

 public class Booking {      public static final String WALDORF_ASTORIA = "Waldorf Astoria";      public Booking(Customer customer, Object hotel) {      }  } 

Notice that we’ve included a WALDORF_ASTORIA constant. Of course, in a real-life system we’d want to be able to look up hotels by various search criteria. We’d also want a proper Hotel class (probably with attributes for rooms, amenities, and so on).

But, you know, something doesn’t seem quite right here. Our BookingManager class is being used for creating (placing) a Booking, but it seems like it would make sense for it to operate in a consistent way with the CustomerManager class that we created earlier—that is, there should be a create(..) method (instead of placeBooking()) that returns the new Booking instance. We should then have a test method that creates a booking and asserts that the returned Booking object wasn’t null. Once we have this, we can add further tests to test for the alternative paths (i.e., to test that one booking was placed, two bookings, and so on). So, instead of writing this:

 Customer bob = CustomerManager.create("Bob");  Booking booking = new Booking(bob, Booking.WALDORF_ASTORIA);  BookingManager.placeBooking(bob, booking); 

we would write this:

 Customer bob = CustomerManager.create("Bob");  Booking booking = BookingManager.create(bob, Booking.WALDORF_ASTORIA); 

The testOneBookingPlaced() test method creates a new Customer (Bob) and a Booking at the Waldorf, and then places the Booking with the BookingManager. Finally, the test asserts that Bob has exactly one booking (the one we just placed). Of course, running this test will fail at this stage, because getNumBookings() always returns 0. We could change it to return 1 instead, but then the testNoBookingsMade() test would fail because it’s expecting 0. So it looks as if we’re going to need to write some additional code to keep track of the bookings that have been placed. (D’oh! And we thought this was going to be an easy ride!)

A Quick Bout of Refactoring

But before we do that, there’s some common code in the previous two tests, so let’s quickly refactor what we have so far to root out the commonality. Both test methods start by calling BookingManager.clearAll() to set up the test, and then both test methods create a Customer called Bob. So it makes sense to move this code into a separate method. Once again, we can use JUnit’s setUp() method:

 public void setUp() {      BookingManager.clearAll();      Customer bob = CustomerManager.create("Bob");  }  public void testInitialState() {      assertEquals("Bob hasn’t booked any rooms yet.", 0,                   BookingManager.getNumBookings(bob));  }  public void testOneBookingMade() {      Booking booking =                    BookingManager.create(bob, Booking.WALDORF_ASTORIA);      assertEquals("Bob has made one booking.", 1,                    BookingManager.getNumBookings(bob));  } 

Note 

If BookingManager wasn’t a Singleton, then we wouldn’t need to clear it out in this way in the setUp() method. Instead, we’d probably create a local instance of it to be used by the test methods in this test class.

Another Note 

Strictly following TDD, we also shouldn’t add the call to clearAll() at all, until the tests start failing. At this point, we’d identify the need to clear out the data prior to each test method, and add clearAll() so that the tests pass.

Now let’s modify the BookingManager class to make both tests pass:

 public class BookingManager {      protected static int numBookings = 0;      public static void clearAll() {          numBookings = 0;      }      public static Booking create(Customer customer, Object hotel) {          numBookings++;          return new Booking(customer, hotel);      }      public static int getNumBookings(Customer customer) {          return numBookings;      }  } 

Running this, both tests now pass. However, at this point you’d do well to bury your face in your hands and groan loudly. It’s painfully obvious that the preceding code isn’t particularly useful. True, it passes the tests, but we also know (from peeking at the requirements) that we’ll need to track and find bookings that have previously been placed. So let’s add the code now for retrieving a Booking.

Retrieving a Booking

In TDD, you never write code without first writing a test, so we’ll add a test that retrieves a Booking:

 public void testRetrieveBooking() {      Booking booking = BookingManager.create(bob, Booking.WALDORF_ASTORIA);      Object bookingID = booking.getID();      Booking retrievedBooking = BookingManager.findBooking(bookingID);      assertEquals("Booking ID should find the same booking.", booking, retrievedBooking);  } 

Remember, we don’t need to create Bob here as he’s already created in the setUp() method.

This test reveals the need for a booking ID, so that bookings can be uniquely looked up. Notice how writing the test itself is driving the design. TDD is useful for designing interfaces, as the tests are written from a client object’s perspective.

Note that as we’re using assertEquals(), we’d also need a booking.equals() method that tests that two bookings are the same (alternatively, we could test booking.getID()). If we had a technical requirement that each Booking object must be a Singleton (not recommended!), then we could use assertSame() instead of assertEquals() to test that the retrieved Booking object is the same instance.

We now update both the Booking and BookingManager classes so that our test compiles:

 public class Booking {      public static final String WALDORF_ASTORIA = "Waldorf Astoria";      private Object id;      private Customer customer;      private Object hotel;      public Booking(Object id, Customer customer, Object hotel) {          this.id = id;          this.customer = customer;          this.hotel = hotel;      }      public Object getID() {          return id;      }      public String toString() {          return "Booking " + id + " for Customer " + customer + " in hotel " + hotel;      }  } 

The main change to Booking is that we’ve added an ID to its constructor and a getID() method, which the test needs. We’ve also given Booking a toString() method so that we get some useful contextual information when the tests fail. And we’ve added fields for customer and hotel (at the moment, they’re needed only by toString()).

We also need to modify BookingManager (which creates Bookings) so that it allocates a new ID and passes this into new Bookings:

 public class BookingManager {      protected static int numBookings = 0;      public static void clearAll() {          numBookings = 0;      }      public static Booking create(Customer customer, Object hotel) {          numBookings++;          Object bookingID = new Integer(numBookings);          return new Booking(bookingID, customer, hotel);      }      public static Booking findBooking(Object bookingID) {          return null;      }      public static int getNumBookings(Customer customer) {          return numBookings;      }  } 

If we rerun BookingManagerTest now, we’d expect a failure because BookingManager.findBooking() is returning null:

 ...F  Time: 0.02  There was 1 failure:  1) testRetrieveBooking(BookingManagerTest)junit.framework.AssertionFailedError:          Booking ID should find the same booking. expected:<Booking 1 for              Customer Bob in hotel Waldorf Astoria> but was:<null>          at BookingManagerTest.testRetrieveBooking(BookingManagerTest.java:29)  FAILURES!!!  Tests run: 3,  Failures: 1,  Errors: 0 

Exactly right! Okay, let’s do something to make this test pass.

 public class BookingManager {      private static int numBookings = 0;      private static Map bookingsByID = new HashMap();      public static void clearAll() {          numBookings = 0;          bookingsByID.clear();      }      public static Booking create(Customer customer, Object hotel) {          numBookings++;          Object bookingID = new Integer(numBookings);          Booking booking = new Booking(bookingID, customer, hotel);          bookingsByID.put(bookingID, booking);          return booking;      }      public static Booking findBooking(Object bookingID) {          return (Booking) bookingsByID.get(bookingID);      }      public static int getNumBookings(Customer customer) {          return numBookings;      }  } 

To keep the code as simple as possible, we’re using a HashMap to store the Bookings and retrieve them by their ID. Let’s give this a quick test now to see if it passes:

 ...  Time: 0.01  OK (3 tests) 

Looking at the new version of BookingManager, we could also refactor it slightly, as numBookings is now superfluous—we can get the same information from our new bookingsByID HashMap. Now that we have the confidence of a passing suite of tests, we can take a moment to tidy up the code. If the tests still pass, it’s a fairly safe bet that our refactoring didn’t break anything.

 public class BookingManager {      private static Map bookingsByID = new HashMap();      public static void clearAll() {          bookingsByID.clear();      }      public static Booking create(Customer customer, Object hotel) {          Object bookingID = newBookingID();          Booking booking = new Booking(bookingID, customer, hotel);          bookingsByID.put(bookingID, booking);          return booking;      }      public static Object newBookingID() {          return new Integer(++newBookingID);      }      private static int newBookingID = 0;      public static Booking findBooking(Object bookingID) {          return (Booking) bookingsByID.get(bookingID);      }      public static int getNumBookings(Customer customer) {          return bookingsByID.size();      }  } 

We’ve also separated out the part that allocates new booking IDs into a separate method, which makes more sense and keeps each method focused on doing one thing.

Our tests still pass, so let’s move on.

Testing for Bookings by More Than One Customer

It’s been said that programmers only really care about three numbers: 0, 1, and infinity.[8.] Any number greater than 1 might as well be infinity, because the code that we write to deal with quantities of 2 to n tends to be basically the same.[9.]

For our hotel booking system, we’re obviously going to need a system that works for more than one customer. Let’s add a test method to BookingManagerTest to verify this:

 public void testBookingsPlacedByTwoCustomers() {      Customer bob = CustomerManager.create("Bob");      Booking bobsBooking = BookingManager.create(bob, Booking.WALDORF_ASTORIA);      Customer alice = CustomerManager.create("Alice");      Booking alicesBooking =                       BookingManager.create(alice, Booking.PENNSYLVANIA);      Booking alicesSecondBooking =                       BookingManager.create(alice, Booking. WALDORF_ASTORIA);      assertEquals("Bob has placed one booking.", 1,                       BookingManager.getNumBookings(bob));      assertEquals("Alice has placed two bookings.", 2 ,                   BookingManager.getNumBookings(alice));  } 

This test fails, of course, because getNumBookings() always returns the total number of bookings placed, regardless of which customer is being passed in:

 ....F  Time: 0.01  There was 1 failure:  1) testBookingsPlacedByTwoCustomers(BookingManagerTest)          junit.framework.AssertionFailedError:          Bob has placed one booking. expected:<1> but was:<3>          at BookingManagerTest.testBookingsPlacedByTwoCustomers(          BookingManagerTest.java:39)  FAILURES!!!  Tests run: 4,  Failures: 1,  Errors: 0 

Clearly we need a way of isolating Bookings that belong to a single Customer. We could replace the bookingsByID HashMap with, say, bookingListsByCustomer (which stores Lists of Bookings keyed by Customer). But this would cause our testRetrieveBooking() test to fail (in fact, the code probably wouldn’t compile). We definitely don’t want to break or remove existing functionality by adding new stuff.

So instead, let’s add bookingListsByCustomer as an additional HashMap. The BookingManager class would then look something like this:

 public class BookingManager {      private static Map bookingsByID = new HashMap();      protected static HashMap bookingListsByCustomer = new HashMap();      public static void clearAll() {          bookingsByID.clear();          bookingListsByCustomer.clear();      }      public static Booking create(Customer customer, Object hotel) {          Object bookingID = newBookingID();          Booking booking = new Booking(bookingID, customer, hotel);          bookingsByID.put(bookingID, booking);          List customerBookings = findBookings(customer);          customerBookings.add(booking);          return booking;      }      public static Object newBookingID() {          return new Integer(++newBookingID);      }      private static int newBookingID = 0;      public static Booking findBooking(Object bookingID) {          return (Booking) bookingsByID.get(bookingID);      }      public static int getNumBookings(Customer customer) {          return findBookings(customer).size();      }      private static synchronized List findBookings(Customer customer) {          List customerBookings = (List) bookingListsByCustomer.get(customer);          if (customerBookings == null) {              customerBookings = new ArrayList();              bookingListsByCustomer.put(customer, customerBookings);          }          return customerBookings;      }  } 

We’ve added a worker method, findBookings(Customer), which gets called by both create() and getNumBookings(). The findBookings() method handles the case where a List of Bookings hasn’t been added yet for a Customer. Currently it’s also the way that new Booking Lists are instantiated.

This code compiles and passes the tests, so we can move on, but just before we do, here’s a word from our conscience

Note 

Of course, this code is adding quite a bit of responsibility to BookingManager. When you start having Lists of Lists, or Maps of Lists, it’s a sign that you probably need another class in there to handle the complex relationship between Customers and their Bookings. This is the sort of situation that we would identify early on in an up-front design approach (see the next chapter). As it is, we could now refactor BookingManager so that it has a separate BookingList class, which contains the Bookings for a single Customer.

We won’t cover that here because the pub closes soon (and we’re guessing that you can see what TDD is about by now), so let’s move on.

image from book
TESTING PRIVATE METHODS (OR NOT)

If we were particularly diligent, we’d add a test or two for the findBookings() method. This would involve making it public, though, which we don’t want to do, because currently no client code needs to call it directly. The answer, then, is to make sure that all its possible failure cases are covered by the tests that invoke it indirectly (e.g., via BookingManager.create()).

If a private method has a failure mode that is never caused by the public methods that use it, then it shouldn’t actually be a problem because that failure mode is never going to occur. Of course, if there was an entire subsystem behind that method (i.e., the method was delegating to some other class or group of methods to do some complex processing), then we would want to make sure that code was adequately unit-tested.

This tends to be more of a problem with legacy code. Using TDD, it’s less likely that the failure mode would appear: the code is only written to pass tests, so where did the failure mode come from? If every public method is micro-tested in every way that it can be used, and the application is exercised in every way that it is used by the acceptance tests, any problems would be discovered very early on.

Similarly, if we did make the findBookings() method public, then we would first have to write some additional tests for it. In that sense, TDD is about testing functionality that has been exposed via a public interface. How the classes go about implementing the code “behind the scenes” is largely immaterial (from the tests’ perspective). If there are failure modes that never occur because the program never enters that state, then it probably doesn’t matter—as long as the tests cover all states that the program could enter when it goes live.

image from book

One Last User Story and We’re Done (So We’re 90% Complete, Then!)

At this stage, the programming team members hope they’re finished. There’s only one more user story to go, the customer is expecting a live demo that afternoon, and the team prepares to party like there’s no tomorrow. However, to remind us that software is never done, the final user story reads

All the Customer and Booking data must be persisted.[10.]

Dang! After the project manager is picked up off the floor and given some smelling salts, the team settles down and works out what must be done to make the data persistent. Because our basic framework for adding and retrieving Customer and Booking data is in place, and we have a set of unit tests that tell us if these classes stop working, then it shouldn’t be too difficult to gradually replace the back-end code with some code that stores/retrieves the data from a database management system (DBMS). At each stage, with each microscopic change that we make, we can run the tests again to make sure we haven’t broken the basic functionality.

Although the process is long-winded, a nice side effect of TDD is that the code interfaces usually do end up being very clean and shouldn’t impose a particular database persistence solution on the client code. That is, the client code doesn’t have to know whether we’re using Enterprise JavaBeans (EJB), or CORBA, or web services, or punched card, or whatever behind the scenes. In fact, the client code doesn’t have to know any details at all about our data access layer. (In the next chapter, we’ll revisit the example to show how a similarly clean design can be achieved using ICONIX Process.)

In the Java universe, we have many surprisingly diverse choices for adding a persistence layer, including the following:

  • Using a persistence framework like JDO or Hibernate[11.]

  • Using a code generator such as JGenerator[12.] to create the persistence layer for us

  • Using EJB

  • Adding SQL directly into our BookingManager class (or adding a data access object that BookingManager delegates to)

  • Persisting to local text files (really not a recommended solution for the majority of cases, as we’d very quickly run into limitations)

Persistence Option: Hibernate

For our travel example, Hibernate would actually be a good choice, because it’s well suited to persisting plain old Java objects (POJOs). In our example, we’ve created Java classes such as Customer and Booking. Using Hibernate, it would be very easy to create a database schema and middle tier directly from these objects.

Hibernate includes a tool for generating database schemas from POJOs (actually from an intermediate XML mapping file, which can be created manually or via a generator such as XDoclet[13.]).

The steps involved in creating a persistence framework from our Java classes are as follows:

  1. Write the JavaBeans (Customer, Booking, etc.).

  2. Add XDoclet tags to the classes to generate the Hibernate XML mapping (or create the mapping by hand).

  3. Generate our database schema using a Hibernate utility called hbm2ddl.

  4. Export the schema to a database.

Using Hibernate would be a good option. But for this example, we want to show how we would use the tests to help us safely modify the code after making a slightly sharp right-hand turn in the design. So to demonstrate this, let’s take the road less traveled

Persistence Option: Raw SQL via Handwoven DAOs

So we’ve decided to add a BookingManagerDAO (data access object) class, which BookingManager delegates to. BookingManagerDAO in turn contains simple SQL statements to persist and read back the data from a database. We would change the methods in BookingManager one at a time, rerunning the complete suite of unit tests at each stage to make sure we haven’t broken anything.

Here’s the initial, rather empty version of BookingManagerDAO:

 public class BookingManagerDAO {      public BookingManagerDAO() {      }      public void save (Booking booking) {      }  public Booking find (Object bookingID) {          return null;      }      public int getNumBookings(Customer customer) {          return 0;      }  } 

Unlike its counterpart, BookingManager, we’re giving BookingManagerDAO a constructor, and BookingManager will create and destroy it as needed. This is primarily so that we can control the object’s life cycle—that is, the point at which it connects to the database, closes its connections, and so on.

The BookingManagerDAO class compiles, but as you can see, it doesn’t do very much. It also doesn’t have any unit tests of its own. We could add some, but for our purposes the tests that we’ve already written for BookingManager will confirm whether the DAO is writing and returning Booking objects correctly.

Note 

This approach is borderline “testing by side-effect,” which isn’t particularly desirable. You really want tests that assert specifically that something passes or fails. But in this case, we’re testing persistence code behind the scenes (aka black-box testing) and are mainly interested in making sure our existing functionality isn’t broken. In a production environment, however, we probably would add additional tests to cover the new persistence model.

To put this to the test, let’s make our first modification to BookingManager to make it use the new DAO:

 public static synchronized BookingManagerDAO getBookingManagerDAO() {      if (dao == null) {          dao = new BookingManagerDAO();      }      return dao;  }  private static BookingManagerDAO dao = null;  public static Booking create(Customer customer, Object hotel) {      Object bookingID = newBookingID();      Booking booking = new Booking(bookingID, customer, hotel);      getBookingManagerDAO().save(booking);      return booking;  } 

BookingManager creates a Booking instance as before, but now instead of using Lists and Maps to keep track of Bookings, it’s going to rely on the new data-access class to save and retrieve the Bookings in the database.

If we compile this and run the tests, of course we get failures all over the place. Our data is no longer being stored anywhere (not even in memory!). Changing the underlying persistence model can safely be called a “big refactoring,” with far-reaching consequences. We still want to try and take this in baby steps if we can, but we need to get back to having a passing suite of tests again as soon as possible, so that we can validate our steps as we go along.

Let’s add the code to BookingManagerDAO to save the booking:

 public void save(Booking booking) {      try {          String url = " jdbc:microsoft:sqlserver://localhost:1433";          String sql = "INSERT INTO Bookings " +                                  "(BookingID, CustomerName, HotelID) " +                       "VALUES (" +                                  booking.getID() + ", " +                                  booking.getCustomer().getName() + ", " +                                  booking.getHotel() + ")";          Class.forName(                   "com.microsoft.jdbc.sqlserver.SQLServerDriver");          Connection con =                   DriverManager.getConnection(url, "MyLoginName", "MyPassword");          Statement stmt = con.createStatement();          stmt.execute(sql);               stmt.close();          con.close();      }      catch (Exception e) {          // do some error handling...      }  } 

We’d then add similar code to find Bookings and count the number of Bookings saved for a Customer.

Although this “solution” leaves quite a bit to be desired, it’s sufficient for our purposes: to illustrate how we add a persistence layer and use our existing tests to assert that nothing has changed semantically or been broken. At a stretch, this code might also just scrape past the “just good enough to fit the requirements” criterion, if the requirements are for a single-user, nonscalable system!

Leave It for the Next Release

The functionality that we’ve created thus far is obviously limited, and some of the implementation details (e.g., the use of “raw” SQL instead of a decent persistence framework, the lack of a connection pool, etc.) very much limit the system to precisely what was in the user stories. Therein lies the value (and danger) of TDD: we write only what we need, at the time, to make the customer’s tests pass,[14.] and no more. The tests, in turn, are driven by the requirements that have been scheduled for this release.

The danger is that we may store up an unnecessary amount of rework for ourselves later. For example, in the next release, a user story that the customer had prioritized as low on the list might be “Must be able to scale to 1,000 users, at least 100 concurrently.” So we would then have to rewrite the entire persistence layer. The work that we did on separating responsibilities and the unit tests that we wrote should allow us to do this without too many problems. But even so

You probably noticed that we didn’t write any front-end code (mainly in the interest of keeping the example short). This could be, for example, a Swing or SWT front-end using our code as a middle tier to connect to a database, or a JSP front-end if we’re creating a web application. When you’re implementing the UI, it’s prudent to use a customer acceptance testing framework such as FitNesse or Exactor.

You’ll find that customer acceptance testing frameworks drive the requirements from the UI (though not quite to the extent that a use case–driven approach does, as you’ll see in the next chapter). The requirements define the UI behavior, and the UI in turn defines what tests we need to write. Then the tests define the source code.

Summary of Vanilla TDD

So you get the basic idea of how vanilla TDD works. As you can see, only a small part of TDD is actually about testing. Its main drive has to do with design: proving code as you go along, and only writing code that you strictly need. As you can see, TDD also promotes fine-grained methods and encapsulation of “dirty” implementation details. The fact that you also end up with a complete set of unit tests at the end is a nice bonus.

The main drawback is that the process can be long-winded, to say the least. If you wrote all your code like this, you’d be there all night. At the risk of generalizing, “test-infected” programmers inevitably start to take shortcuts, writing small batches of tests and then writing the code to implement those tests. In our previous example, if we’d written the testTwoBookingsPlaced() and testTwoCustomers() unit tests in one go, and then written the code to make them pass, we wouldn’t have gone through the intermediate solution of using a numBookings int field—we’d have gone straight to the HashMap solution.

However, when using TDD without in-depth design modeling up front, you’ll often want to go through these intermediate steps, because then you’re much more likely to avoid overlooking design issues or failure modes. It sure can take a long time, though.

In the next chapter, we’ll discuss how to apply some of the TDD principles to design modeling (where change and refactoring are much less expensive). In particular, we’ll show how to use the “vector” principle (two tests at different extremes) to model expected class and UI behavior. We’ll also show how to cut down the number of “intermediate” design steps needed in TDD, by creating a design model up front. To demonstrate these principles, we’ll repeat the preceding example using a suitable mixture of up-front design and TDD.

[4.]See www.junit.org. JUnit is part of a family of testing frameworks for different platforms, including CppUnit for C++ and NUnit for C#/.NET.

[5.]We’ll take it as read that we’ve done the necessary persona design/role modeling to determine which are the most important user stories for the customer, as that aspect of software development is covered elsewhere in this book.

[6.]With the advent of GUI fixtures for the FIT framework (see www.fitnesse.org) and the Exactor framework from Exoftware (www.exoftware.com), both of which are open source, this is becoming a lot easier. Unlike the usual “record and play” type of GUI tester, these can be written up front and used to drive development.

[7.]In fact, Singletons these days are widely considered to be an antipattern. See Matt’s article titled “Perils of the Singleton” at www.softwarereality.com/design/singleton.jsp.

[8.]Alan Cooper, The Inmates Are Running the Asylum: Why High Tech Products Drive Us Crazy and How to Restore the Sanity (Indianapolis, IN: Sams Publishing, 1999), p. 100.

[9.]Obviously that’s a bit of a generalization—we’re not taking into account really big values or quantities, and what to do if your computer runs out of heap space, bandwidth, processing time, hard disk space, and so on. But as a general rule

[10.]Of course, we’re being quite tongue-in-cheek with this example. Realistically, this requirement would have been discussed in the planning game. It is possible, though, that the customer might have given one answer—for example, deciding to go with “in-memory” state first (i.e., no persistence)—to get an initial prototype working, or he may have decided to go with flat-file persistence first and decided upon database persistence later.

[11.]See www.hibernate.org.

[12.]See www.javelinsoft.com/jgenerator

[13.]See http://xdoclet.sourceforge.net.

[14.]More specifically, using TDD we only write code to make the customer’s tests pass (i.e., the acceptance tests). We haven’t shown these here, as they’re outside this chapter’s scope. See http://c2.com/cgi/wiki?AcceptanceTest for more information about acceptance testing.



Agile Development with ICONIX Process. People, Process, and Pragmatism
Agile Development with ICONIX Process: People, Process, and Pragmatism
ISBN: 1590594649
EAN: 2147483647
Year: 2005
Pages: 97

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net