The Coad Letter: Test-driven Development, Issue 108, A TDD Session, by Dave Astels

By: Coad Letter Test Driven Development Editor

Abstract: This issue shows what it is like to perform test-driven development on a development project.

Dear Friend,

Welcome to this issue of the Test-driven Development Edition of The Coad Letter. In previous issues we've explored some of the background and practices behind TDD. In this issue, we're going to grab the keyboard and roll up our sleeves. We'll be working through the coding of some initial tasks of a small Java application. This is based on a partial chapter from my upcoming TDD book.

-- Dave

Dave Astels
dave@adaptionsoft.com

PS. Do you like this approach to coding? Do you see how it would improve your speed and quality? Would you like to learn more? Visit www.adaptionsoft.com.


1. Introduction

Here's the story that we will be working on:

Provide a way to keep a list of movies and a way to add movies to it. Ordering of the list isn't a concern.

This story is broken into three tasks:

  1. Make a container for movies with a way to add to it. The container doesn't have to be ordered or sorted.
  2. Make a GUI that shows a list of movies. List order is just what is in the underlying collection. The list should scroll as required.
  3. Provide a text field and ``Add'' button for adding movies to the list via the GUI.

We will will only be working on the first of these. In future issues we will explore how we can apply TDD to GUI development.

2. The First Test

We begin, of course, with a single test. What should that test be? Well, it needs to be something simple since there is absolutely no code yet. How do we figure out what the first test should be? Look at the task. We need to keep a list of movies. Should we write a test for a movie? Let's not get ahead of ourselves. The task doesn't say much about movies. It just says that we have some and we want some place to keep them. The some place to keep them is the focus of the task, and should be our focus as well.

OK, so we want a test of something that holds movies. What should we test? There's a rule of thumb in TDD: Test the simple stuff first. One instance of this is: test empty collection behavior. OK, let's write a test for an empty list of movies.

Here it is, with the basic overhead. Notice that we've included a main() method. This allows the test to be run from the command line.

Eclipse provides a wizard for creating test cases. One of the options is to automatically include exactly the main method we would like to have.
public class TestMovieList extends TestCase {

  public void testEmptyListSize() {
    MovieList emptyList = new MovieList();
    assertEquals("Size of empty movie list should be 0.", 0, emptyList.size()); 
  }

  public static void main(String[] args) {
    junit.textui.TestRunner.run(TestMovieList.class);
  }
}

This is about as simple as it gets. We create a new instance of MovieList, and assert that it contains zero movies.

Try compiling... it won't. We need to add some code stubs to make it compile. Let's look at what we've written and get a list of the assumptions we've made.

Another feature of Eclipse is the flagging of situations which prevent code from compiling, and generally providing a list of possible corrections. Choose from the list to make it happen.

Interestingly, the indicator used is a yellow light bulb in the margin (see Issue 105, on the TDD Traffic Light).

What we find is the class MovieList and the method MovieList.size(). We need to create the class and the method. Remember, all we are trying to do at this point is to get the test compiling. We do the absolute minimum that will accomplish that. With that in mind, here's what we get:

public class MovieList {

  public int size() {
    return 0;
  }
}

That's it. Our test compiles now. Try running it. It passes. What! Aren't we supposed to have a failing test first? Generally yes. In Smalltalk, and other similar languages, yes. However in a strongly typed language like Java, if we define a method that returns something (an int in this case) we must provide a return value. What do we return? The simplest thing. That would be 0, false, or null depending on the return type of the method. Since the size() method returns an int, we have it return 0. It just so happens that 0 is the size of an empty MovieList. It also just so happens that this is exactly what we would have done to get the text to pass.

Is there any duplication that needs cleaning up? No. Any refactoring that has to be done? No. OK, next test.

3. A Non-Empty List

This test is a bit more involved, so lets slow down and look at building it one step a a time. We won't go this slowly often, but it's worth doing it in the beginning so that you can see how a test is built up from nothingness.

First we need the skeleton:

public void testSizeAfterAddingOne() {
}

The significant thing here is the method name. What are we going to test? The method name should indicate that. It should say something meaningful to us when we look at the test results. It should communicate intent to someone else who is reading the tests. Looking at the list of test methods should be equivalent to looking at the table of contents in a How to use this system manual. Our next step is to decide what the test will be, i.e. what assertion(s) we need. Since we are testing the size of the list after the addition of one movie, it makes sense to check that the movie list reports its size as 1 afterward. So now the test is:

public void testSizeAfterAddingOne() {
  assertEquals("Size of one item list should be 1.", 1, oneItemList.size());
}

Notice that we use
  1. assertEquals, and
  2. a message in the assertion.
It's a good thing. However, if a test is simple and has a single assertion, I generally do without the message unless the assertion needs some explanation. And if that's the case maybe some names need to be reconsidered.

Next, we consider what needs to be done to set things up for the assertion. We need an instance of MovieList called oneItemList to which we've added a single movie. Let's add that to the test:

public void testSizeAfterAddingOne() {
  oneItemList.add(starWars);
  assertEquals("Size of one item list should be 1.", 1, oneItemList.size());
}

OK, now we need to define oneItemList:

public void testSizeAfterAddingOne() {
  MovieList oneItemList = new MovieList();
  oneItemList.add(starWars);
  assertEquals("Size of one item list should be 1.", 1, oneItemList.size());
}

The variable starWars has to be defined next. What is it? It's being stored in a MovieList, so it must be a Movie. Make it so:

public void testSizeAfterAddingOne() {
  Movie starWars = new Movie("Star Wars");
  MovieList oneItemList = new MovieList();
  oneItemList.add(starWars);
  assertEquals("Size of one item list should be 1.", 1, oneItemList.size());
}

This all looks good, but we're still missing Movie. We need to create a class now, and the appropriate constructor:

public class Movie {
  public Movie(String aString) {
  }
}

Did we jump the gun by passing a name to the Movie constructor? Maybe... but how many movies don't have a name?

Now we see that we need an add() method in MovieList. All we are worried about at the moment is getting our test to compile, so we write the simplest stub possible:

public void add(Movie aMovie) {
}

Since this doesn't return anything, all we need is an empty method. Now everything compiles. Run the test. Red bar! Specifically, JUnit reports:

Size of one item list should be 1. expected:<1> but was:<0>

Oh no! Wait a minute, we want to see that red bar. It's good at this point, as long as it's only the new test that is failing (i.e. we haven't broken anything that used to work). This is the case (testEmptyList still works), so we're happy since we've written just enough test to fail. Now we need to write just enough code to pass. Onward!

Our test expects MovieList.size() to return 1 after MovieList.add() has been called. Let's consider the simplest thing that could possibly work. Could we have MovieList.size() return 1? No, that would break TestMovieList.testEmptyListSize(). We need to return 1 after MovieList.add() has been called. Let's try capturing that information:

public void add(Movie aMovie) {
  numberOfMovies = 1;
}

If we think ahead we can see that this is too simple... Ha! Caught you! Don't look ahead. Solve the problem at hand. Break the rules to get it working. That's the TDD mantra.

OK, we need some support for that, specifically an instance variable, numberOfMovies, so we add it to MovieList:

private int numberOfMovies = 0;

Always explicitly initialize variables. It helps avoid confusion and problems later.

Now we need to use this new information, in order to return the expected value:

public int size() {
  return numberOfMovies;
}

Compile and run the tests... green bar! Now we check to see if introduced duplication. No. Anything smells? Nothing really bad. The implementation of MovieList.add() smells a bit... that constant being assigned to numberOfMovies is suspicious. Nothing obvious to do about it now. We'll wait until we know more.

4. More Movies

We've tested the base case (an empty list) and the first inductive case (a list with one thing in it). Time to take the inductive leap. We know it works for zero and one, so if we get it working for 2, then we can reasonably assume it will work for anything.

We write another test, this time testing that we can add two movies and the size is returned as 2:

public void testSizeAfterAddingTwo() {
  Movie starWars = new Movie("Star Wars");
  Movie starTrek = new Movie("Star Trek");
  MovieList twoItemList = new MovieList();
  twoItemList.add(starWars);
  twoItemList.add(starTrek);
  assertEquals("Size of a two item list should be 2.", 2, twoItemList.size());
}

Of course this fails. Each call to add() simply sets the size to 1. The solution is obvious, increment the size (i.e. the numberOfMovies instance variable) each time add() is called. Let's make that change:

public void add(Movie aMovie) {
  numberOfMovies++;
}

Compile. Test. Green bar. Nothing to clean up.

I can hear you thinking "Wait a minute! We're not doing anything with the Movie instances that are passed in to add()!" No. We're not. We don't have a test that requires it. Maybe it's time we did.

5. Refactoring Tests

Before we continue with more tests, let's look at what we have so far:

public class TestMovieList extends TestCase {

  public void testEmptyListSize() {
    MovieList emptyList = new MovieList();
    assertEquals("Size of empty movie list should be 0.", 0, emptyList.size()); 
  }

  public void testSizeAfterAddingOne() {
    Movie starWars = new Movie("Star Wars");
    MovieList oneItemList = new MovieList();
    oneItemList.add(starWars);
    assertEquals("Size of one item list should be 1.", 1, oneItemList.size());
  }

  public void testSizeAfterAddingTwo() {
    Movie starWars = new Movie("Star Wars");
    Movie starTrek = new Movie("Star Trek");
    MovieList twoItemList = new MovieList();
    twoItemList.add(starWars);
    twoItemList.add(starTrek);
    assertEquals("Size of a two item list should be 2.", 2, twoItemList.size());
  }

  public static void main(String[] args) {
    junit.textui.TestRunner.run(TestMovieList.class);
  }
}

Notice a couple things:

  • We use an instance of MovieList in each test, each time creating it using new MovieList().
  • We use some instances of Movie in a couple of the tests.

In short, we have some duplication in our tests. Furthermore, that duplication is in setting up fixtures for the tests. Let's refactor that duplicated code into a setUp() method.

For instructional purposes we'll go through the process step by step.

We'll start by making an instance variable for each local that is part of a fixture. The Movie instances are easy since we have some common names already. The MovieList instance is a bit more complex because we use a different name in each test. My approach is to generalize them to movieList. So we add the following instance variables:

private MovieList movieList = null;
private Movie starWars = null;
private Movie starTrek = null;

Note that we didn't do anything to the test methods yet, they are still using local variables in each fixture. We have to be even more cautious when refactoring test code because we don't have a test suite backing us up. We do have the fact that the tests run successfully, so as we make changes, they should still run.

Our next step is to add a setUp() method and initialize the new instance variables:

protected void setUp() {
  movieList = new MovieList();
  starWars = new Movie("Star Wars");
  starTrek = new Movie("Star Trek");
}

Everything still works. Now, one by one, we'll replace the local variables with our new fixture. We compile and test after each step. First we'll replace starWars. The test methods are now:

public void testSizeAfterAddingOne() {
  MovieList oneItemList = new MovieList();
  oneItemList.add(starWars);
  assertEquals("Size of one item list should be 1.", 1, oneItemList.size());
}

public void testSizeAfterAddingTwo() {
  Movie starTrek = new Movie("Star Trek");
  MovieList twoItemList = new MovieList();
  twoItemList.add(starWars);
  twoItemList.add(starTrek);
  assertEquals("Size of a two item list should be 2.", 2, twoItemList.size());
}

Likewise, we'll replace starTrek in the third test:

public void testSizeAfterAddingTwo() {
  MovieList twoItemList = new MovieList();
  twoItemList.add(starWars);
  twoItemList.add(starTrek);
  assertEquals("Size of a two item list should be 2.", 2, twoItemList.size());
}

Finally, we tackle the MovieList instance. We work one method at a time, moving it to use the new instance variable. Using Eclipse with it's built-in refactoring, we can simple rename the local variable to movieList, compile and test, remove the local variable, and compile and test a final time. We'll just step through working on testEmptyListSize().

So, rename the local variable to movieList:

public void testEmptyListSize() {
  MovieList movieList = new MovieList();
  assertEquals("Size of empty movie list should be 0.", 0, movieList.size()); 
}

Then remove the local:

public void testEmptyListSize() {
  assertEquals("Size of empty movie list should be 0.", 0, movieList.size()); 
}

Now we do the same to the other two tests and end up with:

public void testSizeAfterAddingOne() {
  movieList.add(starWars);
  assertEquals("Size of one item list should be 1.", 1, movieList.size());
}

public void testSizeAfterAddingTwo() {
  movieList.add(starWars);
  movieList.add(starTrek);
  assertEquals("Size of a two item list should be 2.", 2, movieList.size());
}

Not only is the duplication removed, but we have less code!

6. Checking Contents

Due to the previous tests, we have the basic scaffolding in place. We have a Movie class, a MovieList class, and a basic concept of adding to the list and finding out how many things we have added. The next step is to get back the things we've added. Here's the next test:

public void testContents() {
  movieList.add(starWars);
  movieList.add(starTrek);
  assertTrue("List should contain starWars.", movieList.contains(starWars));
  assertTrue("List should contain starTrek.", movieList.contains(starTrek));
  assertFalse("List should not contain stargate.", movieList.contains(stargate));
}

To get this to compile we need to add a new variable to the fixture:

private Movie stargate = null;
//...
stargate = new Movie("StarGate");
and stub a contains() method in MovieList:
public boolean contains(Movie aMovie) {
  return false;
}

It compiles, and fails with:

List should contain starWars.

Of course it fails, for two reasons:

  1. MovieList doesn't contain anything, we've been ignoring the the objects passed into the add() method
  2. the contains() method just returns false

Now, let's do something about that. First, we'll do something with the arguments to add(). We want to capture and accumulate them. The simplest way to do this is to toss them into a Collection of some sort. For lack of a good reason otherwise, we'll use an ArrayList.

First, we need an instance variable. Notice that we declare it as a Collection and instantiate an ArrayList. By using the most abstract interface possible, we commit to the least:

private Collection movies = new ArrayList();

We need to take the argument to add() and put it in the ArrayList:

public void add(Movie aMovie) {
  numberOfMovies++;
  movies.add(aMovie);
}

Notice that we didn't remove the line that increments numberOfMovies. If we had, the tests that do work would stop. When we're working in this fashion (replacing functionality that works) we leave in the old code until we have made sure that the new code works the same way, i.e. it has to pass all the tests that the old code did.

You only ever want to have one test failing at a time.

Now, movies contains all the Movie instances that have been passed in through add(), so we should be able to change size() to return the number of objects in movies:

public int size() {
  return movies.size();
}

This is the critical change. Up to now our changes have been building parallel functionality in the background. The new code wasn't being relied on. This change uses the new code instead of the old. We compile and test. All of our previous tests still pass. Excellent! We can now clean up the old code. What do we do with the old code? Throw it out! Don't comment it out... trash it. You won't need it again, and if you ever do it's safely in your source control system.

When you replace old code with new, resulting in dead code (i.e. code that isn't used anymore), delete it. Also, make sure you are integrating continuously (well, at least as frequently as you can) so that you always have a fine granularity history of the project in case you do need to get something back.

Here's another plug for Eclipse: it keeps track of each file's local history... a snapshot of each version you've saved... and let's you roll back to any point (among other things). So you can revert to a version between your last check-in and the present.

After being cleaned up, here's MovieList:

public class MovieList {
  private Collection movies = new ArrayList();

  public int size() {
    return movies.size();
  }

  public void add(Movie aMovie) {
    movies.add(aMovie);
  }

  public boolean contains(Movie aMovie) {
    return false;
  }
}

Now we can turn our attention to the contains() method. Since we have all of the movies in a Collection this is easy, we can just delegate to movies for the containment check:

public boolean contains(Movie aMovie) {
  return movies.contains(aMovie);
}

Compile, test, green bar!

Are there any opportunities to refactor? Actually, yes. Have a look at our TestCase:

public class TestMovieList extends TestCase {
  private MovieList movieList = null;
  private Movie starWars = null;
  private Movie starTrek = null;
  private Movie stargate = null;

  protected void setUp() {
    movieList = new MovieList();
    starWars = new Movie("Star Wars");
    starTrek = new Movie("Star Trek");
    stargate = new Movie("StarGate");
  }

  public void testEmptyListSize() {
    assertEquals("Size of empty movie list should be 0.", 0, movieList.size());
  }

  public void testSizeAfterAddingOne() {
    movieList.add(starWars);
    assertEquals("Size of one item list should be 1.", 1, movieList.size());
  }

  public void testSizeAfterAddingTwo() {
    movieList.add(starWars);
    movieList.add(starTrek);
    assertEquals("Size of a two item list should be 2.", 2, movieList.size());
  }

  public void testContents() {
    movieList.add(starWars);
    movieList.add(starTrek);
    assertTrue("List should contain starWars.", movieList.contains(starWars));
    assertTrue("List should contain starTrek.", movieList.contains(starTrek));
    assertFalse("List should not contain stargate.", movieList.contains(stargate));
  }

  public static void main(String[] args) {
    junit.textui.TestRunner.run(TestMovieListWithEmptyList.class);
  }
}

Have a close look at testSizeAfterAddingTwo() and testContents(). Both start with the statements:

movieList.add(starWars);
movieList.add(starTrek);

Oh dear! Duplication. Yes, but it's more than that, though. Those two tests are using an extension of the fixture that setUp() is building. This is bad. The solution is simple, however: we just have to extract those two tests along with their fixture into a new TestCase. Here it is:

public class TestMovieListWithTwoMovies extends TestCase {
  private MovieList movieList = null;
  private Movie starWars = null;
  private Movie starTrek = null;
  private Movie stargate = null;

  protected void setUp() {
    starWars = new Movie("Star Wars");
    starTrek = new Movie("Star Trek");
    stargate = new Movie("StarGate");
    movieList = new MovieList();
    movieList.add(starWars);
    movieList.add(starTrek);
  }

  public void testSizeAfterAddingTwo() {
    assertEquals("Size of a two item list should be 2.", 2, movieList.size());
  }

  public void testContents() {
    assertTrue("List should contain starWars.", movieList.contains(starWars));
    assertTrue("List should contain starTrek.", movieList.contains(starTrek));
    assertFalse("List should not contain stargate.", movieList.contains(stargate));
  }

  public static void main(String[] args) {
    junit.textui.TestRunner.run(TestMovieListWithTwoMovies.class);
  }
}

One last change here is to rename testSizeAfterAddingTwo() to be clearer. Notice how it's current name includes a description of the fixture. This is a smell that indicates that a TestCase should be split. A better name would be testSize().

We can also see that the two tests that remain in TestMovieListWithEmptyList use slightly different fixtures: testEmptyListSize() uses an empty list while testSizeAfterAddingOne() uses a list with one item. Again, the test names describe the fixtures. Let's split them into separate classes and rename them accordingly. The two classes that result are:

public class TestEmptyMovieList extends TestCase {
  private MovieList movieList = null;

  protected void setUp() {
    movieList = new MovieList();
  }

  public void testSize() {
    assertEquals("Size of empty movie list should be 0.", 0, movieList.size());
  }

  public static void main(String[] args) {
    junit.textui.TestRunner.run(TestMovieListWithEmptyList.class);
  }
}

public class TestMovieListWithOneMovie extends TestCase {
  private MovieList movieList = null;
  private Movie starWars = null;

  protected void setUp() {
    starWars = new Movie("Star Wars");
    movieList = new MovieList();
    movieList.add(starWars);
  }

  public void testSize() {
    assertEquals("Size of one item list should be 1.", 1, movieList.size());
  }

  public static void main(String[] args) {
    junit.textui.TestRunner.run(TestMovieListWithOneMovie.class);
  }
}

There, much cleaner and clearer.

Now that we have more than one TestCase in the package, we should create a TestSuite containing them so that we can run all the tests together. As we create new TestCases we will add them to the suite. Here's the initial form of the suite:

public class AllTests extends TestSuite {
  
  public static void main(String[] args) {
    junit.textui.TestRunner.run(AllTests.class);
  }

  public static Test suite() {
    TestSuite suite = new TestSuite("Test for com.saorsa.nowplaying.tests");
    suite.addTest(new TestSuite(TestMovieListWithEmptyList.class));
    suite.addTest(new TestSuite(TestMovieListWithOneMovie.class));
    suite.addTest(new TestSuite(TestMovieListWithTwoMovies.class));
    return suite;
  }
}

Notice that Movie is an empty class. That's all we've needed so far. We know there will need to be some instance variables and methods in Movie, but we don't have any need of them yet. When we have a test that requires them, we'll add them. Until then it's just speculation, and we won't add anything on speculation.

7. In Closing

In this issue we've implemented the first task of an application. We've seen a variety of techniques used, including writing the test first, starting with the assertion, extracting a common fixture, splitting a TestCase when different fixtures emerge, naming tests descriptively, and using a TestSuite when we have multiple TestCodes.

In the next issue we will have a closer look at this business of fixtures: what they are and how they related to TestCase.


Server Response from: SC3