Test-driven development (TDD) is a software development process that relies on the repetition of a very short development cycle: first the developer writes an (initially failing) automated test case that defines a desired improvement or new function, then produces the minimum amount of code to pass that test, and finally refactors the new code to acceptable standards.
The following sequence of steps is generally followed:
- Add a test
- Run all tests and see if the new one fails
- Write some code
- Run tests
- Refactor code
There's a plenty of articles written on TDD and Wikipedia is always a good start. This article will focus on the actual test and implementation using variation of one of the Roy Osherove Katas. Do not click the link until you're finished with this article. This excercise is best done when not all requirements are known in advance.
Below you will find the test code related to each requirement and afterwards the actual implementation. Try to read only one requirement, write the tests and the implementation yourself and compare it with the results from this article. Remember that there are many different ways to write tests and implementation. This article is only one out of many possible solutions.
- Create a simple String calculator with a method int Add(string numbers)
- The method can take 0, 1 or 2 numbers, and will return their sum (for an empty string it will return 0) for example "" or "1" or "1,2"
- Allow the Add method to handle an unknown amount of numbers
- Allow the Add method to handle new lines between numbers (instead of commas).
- The following input is ok: "1\n2,3" (will equal 6)
- Support different delimiters
- To change a delimiter, the beginning of the string will contain a separate line that looks like this: "//[delimiter]\n[numbers…]" for example "//;\n1;2" should return three where the default delimiter is ';' .
- The first line is optional. All existing scenarios should still be supported
- Calling Add with a negative number will throw an exception "negatives not allowed" - and the negative that was passed. If there are multiple negatives, show all of them in the exception message stop here if you are a beginner.
- Numbers bigger than 1000 should be ignored, so adding 2 + 1001 = 2
- Delimiters can be of any length with the following format: "//[delimiter]\n" for example: "//[---]\n1---2---3" should return 6
- Allow multiple delimiters like this: "//[delim1][delim2]\n" for example "//[-][%]\n1-2%3" should return 6.
- Make sure you can also handle multiple delimiters with length longer than one char
Even though this is a very simple program, just looking at those requirements can be overwhelming. Let's take a different approach. Forget what you just read and let us go through the requirements one by one.
Create a simple String calculator
Requirement 1: The method can take 0, 1 or 2 numbers separated by comma (,).
Let's write our first set of tests.
It's a good practice to name test methods in a way that it is easy to understand what is being tested. I prefer a variation of BDD with When [ACTION] Then [VERIFICATION]. In this case the name of one of the test methods is whenMoreThan2NumbersAreUsedThenExceptionIsThrown. Our first set of tests verifies that up to two numbers can be passed to the calculator's add method. If there's more than two or if one of them is not a number, exception should be thrown. Putting "expected" inside the @Test annotation tells the JUnit runner that the expected outcome is to throw the specified exception. From here on, for brevity reasons, only modified parts of the code will be displayed. Whole code divided into requirements can be obtained from the GitHub repository (tests and implementation).
Keep in mind that the idea behind TDD is to do the necessary minimum to make the tests pass and repeat the process until the whole functionality is implemented. At this moment we're only interested in making sure that "the method can take 0, 1 or 2 numbers". Run all the tests again and see them pass.
Requirement 2: For an empty string the method will return 0
All there was to do to make this test pass was to change the return method from void to int and end it with returning zero.
Requirement 3: Method will return their sum of numbers
Here we added iteration through all numbers to create a sum.
Requirement 4: Allow the Add method to handle an unknown amount of numbers
All we had to do to accomplish this requirement was to remove part of the code that throws an exception if there are more than 2 numbers. However, once tests are executed, the first test failed. In order to fulfill this requirement, the test whenMoreThan2NumbersAreUsedThenExceptionIsThrown needed to be removed.
Requirement 5: Allow the Add method to handle new lines between numbers (instead of commas).
All we had to do to was to extend the split regex by adding |\n.
Requirement 6: Support different delimiters
To change a delimiter, the beginning of the string will contain a separate line that looks like this: "//[delimiter]\n[numbers…]" for example "//;\n1;2" should take 1 and 2 as parameters and return 3 where the default delimiter is ';' .
This time there was quite a lot of refactoring. We split the code into 2 methods. Initial method parses the input looking for the delimiter and later on calls the new one that does the actual sum. Since we already have tests that cover all existing functionality, it was safe to do the refactoring. If anything went wrong, one of the tests would find the problem.
Requirement 7: Negative numbers will throw an exception
Calling Add with a negative number will throw an exception “negatives not allowed” - and the negative that was passed. If there are multiple negatives, show all of them in the exception message.
There are two new tests. First one checks whether exception is thrown when there are negative numbers. The second one verifies whether the exception message is correct.
This time code was added that collects negative numbers in a List and throws an exception if there was any.
Requirement 8: Numbers bigger than 1000 should be ignored
Example: adding 2 + 1001 = 2
This one was simple. We moved "returnValue += numberInt;" inside an "else if (numberInt <= 1000)".
There are 3 more requirements left. I encourage you to try them by yourself.
Requirement 9: Delimiters can be of any length
Following format should be used: "//[delimiter]\n". Example: "//[---]\n1---2---3" should return 6
Requirement 10: Allow multiple delimiters
Following format should be used: "//[delim1][delim2]\n". Example "//[-][%]\n1-2%3" should return 6.
Requirement 11: Make sure you can also handle multiple delimiters with length longer than one char
Give TDD a chance
This whole process often looks overwhelming to TDD beginners. One of the common complains is that TDD slows down the development process. It is true that at first it takes time to get into speed. However, after a bit of practice development using TDD process saves time, produces better design, allows easy and safe refactoring, increases quality and test coverage and, last but not least, makes sure that software is always tested. Another great benefit of TDD is that tests serve as a living documentation. It is enough to look at tests to know what each software unit should do. That documentation is always up to date as long as all tests are passing. Unit tests produced with TDD should provide "code coverage" for most of the code and they should be used together with Acceptance Test Driven Development (ATDD) or Behavior Driven Development (BDD). Together they are covering both unit and functional tests, serving as full documentation and requirements.
TDD makes you focus on your task, code exactly what you need, think from outside and, ultimately, a better programmer.
Test-Driven Java Development
Test-Driven Java Development book wrote by Alex Garcia and me has been published by Packt Publishing. It was a long, demanding, but very rewarding journey that resulted in a very comprehensive hands-on material for all Java developers interested in learning or improving their TDD skills.
If you liked this article I am sure that you'll find this book very useful. It contains extensive tutorials, guidelines and exercises for all Java developers eager to learn how to successfully apply TDD practices.