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:
- Make a container for movies with a way to add to it. The
container doesn't have to be ordered or sorted.
- 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.
- 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
-
assertEquals, and
- 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:
-
MovieList doesn't contain anything, we've
been ignoring the the objects passed into the add()
method
- 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.
Connect with Us