3. Test-Driven Development

Author:Nick Efford
Contact:N.D.Efford@leeds.ac.uk
Status:Final
Revised:2017-09-13

This worksheet provides a basic introduction to the practice of test-driven development (TDD), essential to XP and frequently adopted by other agile methods.

Note

This worksheet requires that you understand how to use JUnit.

Make sure that you have completed Unit Testing With JUnit before attempting it!

3.1. Basic Concepts

A developer might normally consider writing a unit test for each method of a class immediately after implementing that method. TDD advocates a reversal of these tasks, with the test being implemented first.

Why might you choose to do this? One reason is that it forces you to implement the test. With a ‘test last’ approach, there is always the possibility that a programmer will skip one or more tests. (When you are under pressure to deliver working code quickly, it is surprisingly easy to delude yourself that a method is “bound to be correct”, and therefore doesn’t require a test.)

But there is much more to TDD than that. By writing a test first, you are forced to think carefully about how a method should be called and what the precise outcome of calling it should be; in essence, TDD clarifies the detailed requirements for your code. TDD also helps to define a clear endpoint for implementation of a method; when the tests for that method all pass, you’ve finished. Advocates of TDD argue that this approach leads to cleaner, simpler code that does no more than what is required.

3.2. Preparation

  1. Run IntelliJ and create a new Java project called TDD. Right-click on the src directory, choose New ‣ Package and specify a package name of comp2931.money.

    Create a source directory for your tests by right-clicking on the TDD directory in the Project tool window and choosing New ‣ Directory. Call your new directory tests. Right-click on it and choose Mark Directory as ‣ Test Sources Root. Then right-click on it again and create a new package called comp2931.money, as you did for the src directory.

  2. Right-click on TDD and choose New ‣ Directory. Call the new directory lib. Download the JAR files junit-4.12.jar and hamcrest-all-1.3.jar and copy them into the lib directory.

    Right-click on lib and choose Add as Library… from the menu. Leave the settings on the resulting dialog unchanged and click OK.

3.3. The TDD Cycle

In this example, you will use TDD to create a class named Money, representing an amount of money as euros and cents.

  1. Begin by creating a class to host all of your tests. Right-click on the comp2931.money package underneath tests and choose New ‣ Java Class. Specify MoneyTest as the class name then edit MoneyTest.java so that it looks like this:

    package comp2931.money;
    
    import org.junit.Before;
    import org.junit.Test;
    
    import static org.hamcrest.CoreMatchers.*;
    import static org.junit.Assert.*;
    
    /**
     * Unit tests for the Money class.
     */
    public class MoneyTest {
    
    }
    
  2. The starting point of the TDD cycle is to write a test that fails, so let’s do that now. Write the following test inside MoneyTest:

    @Test
    public void createAmount() {
      Money m = new Money(1, 50);
    }
    

    This code contains two design decisions: one is that there should be a class called Money; the other is that two integer values (representing number of euros and number of cents) must be supplied in order to create a Money object.

    Notice that Money is highlighted in red. The associated error is “Cannot resolve symbol ‘Money’”. You have written a test that fails [1], as required in TDD; it is this failure that motivates creation of the Money class.

  3. Click on the offending code in the test, then click on the red ‘light bulb’ icon. Choose the option to ‘Create class Money’. The class will be created under the src directory. Modify the doc comment to something more useful like “Class to represent an amount of money”.

    Notice that the test still fails; there is a new compilation error in MoneyTest:

    ../_images/comperror.png

    This tells you there is no constructor in Money that matches how you are trying to create a Money object. So this time, test failure motivates the creation of a matching constructor (one with two integer parameters).

  4. Click on the red ‘light bulb’ icon again and choose the option to ‘Create constructor’. In the Money class, use the Tab key to step through the different parts of the constructor parameter list, changing the parameter names to be euros and cents. You should end up with something like this:

    public class Money {
      public Money(int euros, int cents) {
      }
    }
    

    Don’t make any further changes at this point; a central principle of TDD is that you write only the code that is necessary to get a test to pass.

    Of course, you still don’t know whether the test passes or not - so check this now by running the tests. You should see a green bar.

  5. Note that you haven’t really tested yet whether a Money object is created correctly or not; to do so, you need to verify that the euros and cents components of a Money object have the correct values. To do this, go to the createAmount test in MoneyTest and add the following assertions:

    assertThat(m.getEuros(), is(1));
    assertThat(m.getCents(), is(50));
    

    Notice how writing these assertions has forced you into a design decision; namely, that there are getters called getEuros and getCents in Money. The test fails to compile because the getters do not exist. In effect, the test has identified the need for these methods.

  6. Go to the Money class and add the following implementation of getEuros:

    public int getEuros() {
      return 1;
    }
    

    Don’t be tempted to do anything else at this stage. Remember that, in TDD, it is the tests that drive implementation; as yet, our test requires only that getEuros returns a value of 1.

    Do the same for getCents, defining it to return a value of 50, then rerun the tests. You should see a green bar again.

    Note

    Looking ahead, it is obvious that these getters aren’t correct; they shouldn’t return hard-coded values. But you need to bear in mind that TDD encourages implementation of the simplest possible solution that makes the tests pass. Only when we have a test that identifies the need for something more complicated should we add that complexity. This is a powerful way of preventing ‘over-engineering’ of the code.

  7. The current implementation of Money has a ‘bad smell’ to it [2]. In such cases, the TDD approach dictates that you write another test to explore the issue and suggest changes to the implementation.

    Add another test to check that you can create a Money object representing a different sum of money: EUR 2.99, say [3]. A suitable test would be this:

    @Test
    public void createOtherAmount() {
      Money m = new Money(2, 99);
      assertThat(m.getEuros(), is(2));
      assertThat(m.getCents(), is(99));
    }
    

    On rerunning the tests, you should see a red bar.

    ../_images/redbar.png

    The only sensible way of getting this failed test to pass as well as the previous one is to store the values for euros and cents in fields and have the getters return the values of these fields. Implement this now, and rerun the tests. When you get a green bar, you’re done!

3.4. Further Steps

  1. Now suppose that you need to be able to add together two sums of money. You should start by writing a test to check that euros are added together correctly. Add the following code to MoneyTest:

    @Test
    public void addOneEuro() {
      Money oneFifty = new Money(1, 50);
      Money oneEuro = new Money(1, 0);
      Money sum = oneFifty.plus(oneEuro);
      assertThat(sum.getEuros(), is(2));
      assertThat(sum.getCents(), is(50));
    }
    

    This fails to compile because the method plus doesn’t exist in the Money class.

    Click on the offending code (highlighted in red) to bring up the red ‘light bulb’ icon. Click on this and choose the option to “Create method ‘plus’”. A method template will be created in Money. Use the Tab key to move through the different elements of the template, editing it so that it looks like this:

    public Money plus(Money amount) {
      return null;
    }
    

    Run the tests and you’ll get a red bar, because the plus method should not be returning null. Change the return statement so that it returns a new Money(2, 50). This will be sufficient, for the moment, to give you a green bar.

  2. Before proceeding any further, take a look at the createAmount and addOneEuro tests. Do you spot any duplication?

    Both methods create a Money object representing the amount of EUR 1.50. Remove this duplication now by making that amount of money part of the test fixture available to all tests. Click on the variable oneFifty defined in addOneEuro. Then right-click and choose Refactor ‣ Extract ‣ Field, or press Ctrl+Alt+F.

    ../_images/field.png

    In the pop-up dialog, choose the option to ‘Initialize in setUp’ and press the Tab key. the local variable oneFifty will be turned into a field and a setUp method that initializes the field will be added.

    Finally, modify createAmount so that it uses the oneFifty field, instead of creating its own local variable with the same value. Check that the refactoring has worked by rerunning the tests.

  3. Write a test named addTwoEuros that adds EUR 2.00 to EUR 1.50 and checks the result. Use this test to help you refactor the plus method. Again, don’t be tempted to implement more than is necessary to get a green bar. Write another test named addOneCent that adds EUR 0.01 to EUR 1.50. If you are following TDD to the letter, this should drive you to an implementation of plus similar to this:

    public Money plus(Money other)
    {
      int sumEuros = getEuros() + other.getEuros();
      int sumCents = getCents() + other.getCents();
      return new Money(sumEuros, sumCents);
    }
    
  4. Write a test named addWithCarry that adds EUR 0.01 to EUR 2.99. Take this opportunity to refactor the tests and enlarge the test fixture, creating fields twoNinetyNine and oneCent that are initialised in the setUp method and used where appropriate. Your addWithCarry test should drive refactoring of plus into a final, correct version similar to this [4]:

    public Money plus(Money other)
    {
      int sumEuros = getEuros() + other.getEuros();
      int sumCents = getCents() + other.getCents();
      return new Money(sumEuros + sumCents / 100, sumCents % 100);
    }
    
  5. Take a moment to compare the sizes of the Money and MoneyTest classes. What do you notice?

3.5. Final Thoughts on TDD

You may still be unconvinced by TDD. It is not uncommon for newcomers to feel that it is slow and unnecessarily cautious, forcing you to take small steps towards the desired implementation when the nature of that implementation is ‘obvious’ - but this is missing the point.

Take a close look at the tests you have written in MoneyTest. Effectively, these document the required behaviour of the Money class. There are two tests that document the requirement that euros and cents values are stored in, and can be retrieved from, a Money object. There are four tests that collectively encode the rules governing how two sums of money should be added together. Use of TDD has resulted in the development of a complete and executable requirements specification for the Money class. Without using TDD, we might not have arrived at such a complete specification.

Because the requirements are specified so comprehensively by tests, you can have more confidence in the correctness of your implementation. You can also be more confident about refactoring the class in future development work, knowing that unintended changes in its behaviour will be flagged by failing tests.

Finally, TDD’s emphasis on not implementing more code than is necessary to make the tests pass means that you haven’t wasted any time over-engineering the Money class by adding features that aren’t yet needed.


[1]Failure to compile counts as a failed test.
[2]Proponents of TDD (and of the methodology with which it is most commonly associated, extreme programming or XP) frequently talk about code smell, the sense that a particular piece of code isn’t quite right.
[3]In case you didn’t know, ‘EUR’ is the ISO 4217 currency code for the Euro.
[4]If you don’t see why, remind yourself of the purpose and behaviour of the % operator…