======================= 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 :doc:`/junit/main` before attempting it! .. _XP: http://en.wikipedia.org/wiki/Extreme_programming 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. Preparation =========== #. Run IntelliJ and create a new Java project called ``TDD``. Right-click on the ``src`` directory, choose :menuselection:`New --> Package` and specify a package name of ``comp2931.money``. Create a source directory for your tests by right-clicking on the :file:`TDD` directory in the Project tool window and choosing :menuselection:`New --> Directory`. Call your new directory :file:`tests`. Right-click on it and choose :menuselection:`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 :file:`src` directory. #. Right-click on :file:`TDD` and choose :menuselection:`New --> Directory`. Call the new directory :file:`lib`. Download the JAR files :download:`junit-4.12.jar` and :download:`hamcrest-all-1.3.jar` and copy them into the :file:`lib` directory. Right-click on :file:`lib` and choose :guilabel:`Add as Library...` from the menu. Leave the settings on the resulting dialog unchanged and click :guilabel:`OK`. 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. #. Begin by creating a class to host all of your tests. Right-click on the ``comp2931.money`` package underneath ``tests`` and choose :menuselection:`New --> Java Class`. Specify ``MoneyTest`` as the class name then edit ``MoneyTest.java`` so that it looks like this: .. code-block:: java 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 { } #. 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``: .. code-block:: java @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 [#]_, as required in TDD; it is this failure that motivates creation of the ``Money`` class. #. 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 :file:`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``: .. figure:: comperror.png :align: center 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). #. 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: .. code-block:: java 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. #. 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: .. code-block:: java 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**. #. Go to the ``Money`` class and add the following implementation of ``getEuros``: .. code-block:: java 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. #. The current implementation of ``Money`` has a 'bad smell' to it [#]_. 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 [#]_. A suitable test would be this: .. code-block:: java @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. .. figure:: redbar.png :scale: 65 % :align: center 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! Further Steps ============= #. 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``: .. code-block:: java @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: .. code-block:: java 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. #. 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 :menuselection:`&Refactor --> E&xtract --> &Field`, or press :kbd:`Ctrl+Alt+F`. .. figure:: field.png :align: center 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. #. 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: .. code-block:: java public Money plus(Money other) { int sumEuros = getEuros() + other.getEuros(); int sumCents = getCents() + other.getCents(); return new Money(sumEuros, sumCents); } #. 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 [#]_: .. code-block:: java public Money plus(Money other) { int sumEuros = getEuros() + other.getEuros(); int sumCents = getCents() + other.getCents(); return new Money(sumEuros + sumCents / 100, sumCents % 100); } #. Take a moment to compare the sizes of the ``Money`` and ``MoneyTest`` classes. What do you notice? 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. ---- .. [#] Failure to compile counts as a failed test. .. [#] 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. .. [#] In case you didn't know, 'EUR' is the ISO 4217 currency code for the Euro. .. [#] If you don't see why, remind yourself of the purpose and behaviour of the ``%`` operator... .. _code smell: http://en.wikipedia.org/wiki/Code_smell