Test driven development is an important and valued part of agile practices. In test driven development (in short TDD) developers first write tests for new functionality (e.g. a class) and later they implement it. The order is crucial and I will explain later why.
Let’s assume that the developer needs to implement new class PathCalculator which calculates distance between 2 points. The developer has a general idea about the class, that he wants to implement, and could start implementation of it method by method. If he is smart and the problem is not very complicated, he will do it without big problem. Then the class can be integrated into the existing application structure and the new version of the system can be tested. This was easy but many issues can arise if the problem is more complicated:
- the developer may have a vague idea about how this class should be accessed from outside (not to mention how to implement it!)
- after the code for the class is finished, the developer could find out that one or more important methods are missing or the existing methods miss parameters (or something else) to integrate it successfully into existing application structure
- after integration, the new functionality misbehaves or even breaks other functionality for some unknown and hard to trace reason
All of these problems could have be more-or-less avoided (actually TDD is not a silver bullet which solves all problems) if TDD was used.
Basics of TDD
In TDD we first write one or more tests for the new class to find out the how it should be accessed from outside and how it should behave in certain situations. We should specify the usage in typical situation and in certain border cases. Of course, it is impossible to catch all such cases but we should at least try. Let’s look at the implementation of it using Java and JUnit.
package com.example; import org.junit.Test; import static org.junit.Assert.*; public class PathCalculatorTestCase { private static final double DELTA = 0.001; @Test public void test1() { PathCalculator pathCalc = new PathCalculator(PathCalculator.Type.EUCLIDAN); double distance; distance = pathCalc.getDistance(1, 1, 6, 7); assertEquals(distance, Math.sqrt(5 * 5 + 6 * 6), DELTA); distance = pathCalc.getDistance(1, 1, 1, 2); assertEquals(distance, 1.0, DELTA); } @Test public void test2() { PathCalculator pathCalc = new PathCalculator(PathCalculator.Type.EUCLIDAN); double distance = pathCalc.getDistance(7, 8, 7, 8); assertEquals(distance, 0, DELTA); } }
At the moment our code does not compile at all because we are missing class PathCalculator. The most important thing is that we defined how the new class should behave and we decided on its interface. Let’s add a stub for PathCalculator class with empty methods:
package com.example; public class PathCalculator { enum Type { EUCLIDAN, MANHATTAN } public PathCalculator(Type type) { // TODO: } public double getDistance(int x1, int y1, int x2, int y2) { // TODO: return 0; } }
Running tests with Maven
In order to run the tests just create own project directory, put PathCalculatorTestCase class in project directory under src/test/java/com/example/PathCalculatorTestCase.java, put PathCalculator class in project directory under src/main/java/com/example/PathCalculator.java and finally add pom.xml file in the project directory with following contents:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>javatest</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>javatest</name> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.13</version> </plugin> </plugins> </build> </project>
Once it is done the tests can be run like this:
robert@epsilon blogt]$ mvn test (...) ------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.example.PathCalculatorTestCase Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.069 sec <<< FAILURE! test1(com.example.PathCalculatorTestCase) Time elapsed: 0.01 sec <<< FAILURE! java.lang.AssertionError: expected:<0.0> but was:<7.810249675906654> (...)
The code should compile and all the tests will be run but they will fail because the methods are just empty. It is completely OK for now. The last step is to implement the empty methods and run the tests again to check if the tests finish successfully. In practice it is necessary to run the tests many times to implement the code right.
Additionally, tests limit the number of issues during integration of the class into existing application because many of the bugs were found by tests during implementation step. They also allow us to assume certain behaviour from the class and easier spot the rest of the bugs.
How to write the tests
The general idea is to have each class method called at least once in tests with typical arguments (e.g. list with few elements) and also call it few times with border cases (e.g. null value, empty list, list with one element). The reason should be obvious. There is no need to test getters and setters, if the only thing they do is just calling return or setting value. If they do some calculations or propagate the value further, we should test them.
The other important thing is that each test should be concise and should test only one thing. Don’t try to write a big master test which tests it all but split it into many specific tests. Remember, that you can always create many methods in *TestCase class and when it becomes too big, you can create additional *TestCase class with new tests.
Maintenance
The importance of tests does not finish after implementing the class and integrating it into existing application. In real world the requirements are often changing so they may be a need to adapt the class to new requirements after a month or half a year. In this case modify the existing tests or add new ones so they match new requirements. The tests will be failing again but the next step is obvious – modify the implementation of the class so that the tests succeed again.
The tests are also important when changing the implementation of the class. It may be fixing a bug, improving performance or refactoring. Before committing code into SCM, run the tests again to make sure they succeed (in which case we can more-or-less assume that we did not break something else). Of course, it is advised to run the tests more often. Additionally, if we are fixing a bug we should add additional test (called regression test) which verifies if the bug was resolved and will not appear in the future (you would be surprised to see how often bugs fixed long ago reappear).
Sum up
I would like to sum up the article by listing the advantages and disadvantages of TDD.
Advantages:
- Encourage designing interface and defining usage and behaviour before coding the implementation
- Gives general overview how the class should be used
- Automatically verify if new modification to class does not break some other functionality
- Automatically verify if refactoring step does not change the visible behaviour of the application
Disadvantages:
- Require additional time to write the tests and maintain them
- Require additional time to run the tests
- Require additional time to change the tests if the interface of tested class changes
In my opinion additional time spent for tests is an investment which will return quickly in increased testability and quality of the software. Always remember that the TDD is not solver bullet – it does not solve all problems and even though your tests are passing, it does not mean that there are no bugs.
“…Often tested class contains so many methods and have so many interactions that the number of necessary tests may be overwhelming. … Although it may seem OK, people often prevent these tests from running because they take a lot of time to finish. There is also a problem with maintaining test changes. Therefore, we should keep sane limit on the number of tests even though we know that we would not test some methods….”
I think that what you are describing here is some kind of a code smell.
If the class has so many methods, and the test is very complicated, then perhaps the design is bad.
I guess that in such a case, the tested class is not well cohesive.
Perhaps it does more than one thing?
In my experience, the test classes are usually bigger than the production class.
This is totally fine.
As a rule of thumb, unit tests should not take so much time to be executed.
Developers should run all the tests all the time.
So if a test takes too much time, perhaps it’s not isolated? Mayne it calls actual DB instead of mocking it?
And as for your conclusion of limiting the number of tests. I couldn’t disagree more.
In TDD you should never have untested code.
The reason is that you always write the code AFTER you write a test for it.
If you have so many tests that it has become ‘insane number’, check your design, but don’t cut corners in testing.
As for maintaining the test code.You should it the same as production code: refactor and make it clean.
Thank you for pointing this out. I will update the article soon.