Test Driven Development (TDD): Example Walkthrough

medium_5927476303Test-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
  • Repeat

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.

Let’s start!

Requirements

  • 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.

[JAVA TEST]

package com.wordpress.technologyconversations.tddtest;

import org.junit.Test;
import com.wordpress.technologyconversations.tdd.StringCalculator;

public class StringCalculatorTest {
    @Test(expected = RuntimeException.class)
    public final void whenMoreThan2NumbersAreUsedThenExceptionIsThrown() {
        StringCalculator.add("1,2,3");
    }
    @Test
    public final void when2NumbersAreUsedThenNoExceptionIsThrown() {
        StringCalculator.add("1,2");
        Assert.assertTrue(true);
    }
    @Test(expected = RuntimeException.class)
    public final void whenNonNumberIsUsedThenExceptionIsThrown() {
        StringCalculator.add("1,X");
    }
}

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).

[JAVA IMPLEMENTATION]

public class StringCalculator1 {
    public static final void add(final String numbers) {
        String[] numbersArray = numbers.split(",");
        if (numbersArray.length > 2) {
            throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
        } else {
            for (String number : numbersArray) {
                Integer.parseInt(number); // If it is not a number, parseInt will throw an exception
            }
        }
    }
}

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

[JAVA TEST]

@Test
public final void whenEmptyStringIsUsedThenReturnValueIs0() {
    Assert.assertEquals(0, StringCalculator.add(""));
}

[JAVA IMPLEMENTATION]

    public static final int add(final String numbers) { // Changed void to int
        String[] numbersArray = numbers.split(",");
        if (numbersArray.length > 2) {
            throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
        } else {
            for (String number : numbersArray) {
                if (!number.isEmpty()) {
                    Integer.parseInt(number);
                }
            }
        }
        return 0; // Added return
    }

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

[JAVA TEST]

@Test
public final void whenOneNumberIsUsedThenReturnValueIsThatSameNumber() {
    Assert.assertEquals(3, StringCalculator.add("3"));
}

@Test
public final void whenTwoNumbersAreUsedThenReturnValueIsTheirSum() {
    Assert.assertEquals(3+6, StringCalculator.add("3,6"));
}

[JAVA IMPLEMENTATION]

public static int add(final String numbers) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(",");
    if (numbersArray.length > 2) {
        throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
    }
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) { // After refactoring
            returnValue += Integer.parseInt(number);
        }
    }
    return returnValue;
}

Here we added iteration through all numbers to create a sum.

Requirement 4: Allow the Add method to handle an unknown amount of numbers

[JAVA TEST]

//  @Test(expected = RuntimeException.class)
//  public final void whenMoreThan2NumbersAreUsedThenExceptionIsThrown() {
//      StringCalculator.add("1,2,3");
//  }
    @Test
    public final void whenAnyNumberOfNumbersIsUsedThenReturnValuesAreTheirSums() {
        Assert.assertEquals(3+6+15+18+46+33, StringCalculator.add("3,6,15,18,46,33"));
    }

[JAVA IMPLEMENTATION]

public static int add(final String numbers) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(",");
    // Removed after exception
    // if (numbersArray.length > 2) {
    // throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
    // }
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) { // After refactoring
            returnValue += Integer.parseInt(number);
        }
    }
    return returnValue;
}

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).

[JAVA TEST]

@Test
public final void whenNewLineIsUsedBetweenNumbersThenReturnValuesAreTheirSums() {
    Assert.assertEquals(3+6+15, StringCalculator.add("3,6n15"));
}

[JAVA IMPLEMENTATION]

public static int add(final String numbers) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(",|n"); // Added |n to the split regex
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) {
            returnValue += Integer.parseInt(number.trim());
        }
    }
    return returnValue;
}

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 ‘;’ .

[JAVA TEST]

@Test
public final void whenDelimiterIsSpecifiedThenItIsUsedToSeparateNumbers() {
    Assert.assertEquals(3+6+15, StringCalculator.add("//;n3;6;15"));
}

[JAVA IMPLEMENTATION]

public static int add(final String numbers) {
    String delimiter = ",|n";
    String numbersWithoutDelimiter = numbers;
    if (numbers.startsWith("//")) {
        int delimiterIndex = numbers.indexOf("//") + 2;
        delimiter = numbers.substring(delimiterIndex, delimiterIndex + 1);
        numbersWithoutDelimiter = numbers.substring(numbers.indexOf("n") + 1);
    }
    return add(numbersWithoutDelimiter, delimiter);
}

private static int add(final String numbers, final String delimiter) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(delimiter);
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) {
            returnValue += Integer.parseInt(number.trim());
        }
    }
    return returnValue;
}

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.

[JAVA TEST]

@Test(expected = RuntimeException.class)
public final void whenNegativeNumberIsUsedThenRuntimeExceptionIsThrown() {
    StringCalculator.add("3,-6,15,18,46,33");
}
@Test
public final void whenNegativeNumbersAreUsedThenRuntimeExceptionIsThrown() {
    RuntimeException exception = null;
    try {
        StringCalculator.add("3,-6,15,-18,46,33");
    } catch (RuntimeException e) {
        exception = e;
    }
    Assert.assertNotNull(exception);
    Assert.assertEquals("Negatives not allowed: [-6, -18]", exception.getMessage());
}

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.

[JAVA IMPLEMENTATION]

    private static int add(final String numbers, final String delimiter) {
        int returnValue = 0;
        String[] numbersArray = numbers.split(delimiter);
        List negativeNumbers = new ArrayList();
        for (String number : numbersArray) {
            if (!number.trim().isEmpty()) {
                int numberInt = Integer.parseInt(number.trim());
                if (numberInt < 0) {
                    negativeNumbers.add(numberInt);
                }
                returnValue += numberInt;
            }
        }
        if (negativeNumbers.size() > 0) {
            throw new RuntimeException("Negatives not allowed: " + negativeNumbers.toString());
        }
        return returnValue;     
    }

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

[JAVA TEST]

    @Test
    public final void whenOneOrMoreNumbersAreGreaterThan1000IsUsedThenItIsNotIncludedInSum() {
        Assert.assertEquals(3+1000+6, StringCalculator8.add("3,1000,1001,6,1234"));
    }

[JAVA IMPLEMENTATION]

        private static int add(final String numbers, final String delimiter) {
                int returnValue = 0;
                String[] numbersArray = numbers.split(delimiter);
                List negativeNumbers = new ArrayList();
                for (String number : numbersArray) {
                        if (!number.trim().isEmpty()) {
                                int numberInt = Integer.parseInt(number.trim());
                                if (numberInt < 0) {
                                        negativeNumbers.add(numberInt);
                                } else if (numberInt <= 1000) {
                                        returnValue += numberInt;
                                }
                        }
                }
                if (negativeNumbers.size() > 0) {
                        throw new RuntimeException("Negatives not allowed: " + negativeNumbers.toString());
                }
                return returnValue;                
        }

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.

Related articles:

photo credit: bałamut via photopin cc

About these ads

8 thoughts on “Test Driven Development (TDD): Example Walkthrough

  1. In the implementation for step #2 you did not mention that you also added the number.isEmpty() test. Could you discuss more about how TDD drove you to that implementation choice? More specifically, that change allows “” to give 0, but seemingly as a side-effect also allows “,” to give 0 and “,5″ to give 5. Are those two latter inputs supposed to be acceptable? If so, why aren’t those syntax forms tested, in order to have full documentation? If not, why wasn’t that error detected during TDD? What methods can be used to minimize whichever case from reoccurring?

    • The reason behind the isEmpty in the step #2 is requirement “For an empty string the method will return 0″. The idea behind this example is to do tests and implementations for each requirement (11 in total).

      There are many cases that are not covered. I’d say that the result presented above is far from bullet proof and would require additional tests, refactoring and features. One of them could be the option to use decimal numbers. At the moment it is assumed that only integers are used.

      Would you’d be willing to add more tests and extend the implementation? If yes, I can add it to this post or link to your repo.

      • I’m more interested in the process by which TDD helps select that implementation, vs., say, “if (numbers.isEmpty()) {return 0;}” at the start of the function. My version seems to be the more direct translation of the requirements specification, no?

        Of course only rarely can testing cover all possible inputs, and TDD doesn’t promise to provide comprehensive testing – it’s a design methodology. I just don’t see how the test drives the specific implementation choice any more than not using TDD.

        You write “It is enough to look at tests to know what each software unit should do.” At this point though, I don’t think I can do that from these tests. There are only a few specific test cases, and not enough to infer the full expected input grammar. Certainly there isn’t enough to know that “,” gives results equivalent to “0,0″. How then does one judge if there are enough tests so that looking at the tests is enough to understand what the code should do?

        I’m not a Java programmer, so can’t help with adding code.

      • Following up to myself, I’ve read over some of your other postings, which might help explain what I’m interested in. In the PrimeFactors Kata, the given possible solution (of many) makes the implicit assumption that the inputs will be “small”. That is, 2^31-1, which is a Mersenne prime and also max signed int, will do 2+ billion trial divisions before finally showing it’s a prime. This will take a while.

        The problem spec is “Compute the prime factors of a given natural number.” No one expects that the factors of 39^1581*2216193 − 1 (the largest known non-Mersenne prime) will quickly be found, but there’s some borderland region where my implementation might be too slow (eg, longer than the MTBF of the computer) while your implementation is fast enough to be part of the unit test suite.

        Obviously, if 2^31-1 or even higher numbers were part of the test cases, then your implementation would be different, so the choice of test cases reveals something about how the implementer understands the spec, and hence the design.

        This assumptions can lead and have lead to design errors. If TDD doesn’t naturally lead to improvements in this regard (and I think I’ve demonstrated that it doesn’t), then what processes can we use to minimize these errors, and make things “bullet-proof”?

        A way to approach this is to figure out why one design choice was selected over another, which is why I’m curious to know why you did what you did.

  2. Pingback: Java Important topics | techiesjunkyard

  3. This would be a good example of test driven damage, where TDD leads to a false sense of confidence that the design is right because your tests say the implementation works. TDD should only be used with a strong knowledge of patterns and refactoring to patterns.

    This is just a basic clean up of your current code:
    // I cant understand why you wouldnt use an integer var arg for this instead
    // of a String?
    // private static int add(final int numbers…)
    // also theres no need for static. It ends up causing more headaches then
    // its worth.
    private static int add(final String numbers, final String delimiter) {
    // You could add an assert to check for not null params. In a lot of
    // instances you would use the JSR330 annotations to test for valid
    // input.
    Assert.notNull(numbers);
    Assert.notNull(delimiter);
    // Add a guard clause to return early if it doesnt meet the preconditions.
    // When testing for an empty string test the length instead. These
    // should probably return a negative number or better throw an exception
    if (numbers.trim().length() < 1) { return -1; }
    // Also test the delimeter.
    if (delimiter.trim().length() < 1) { return -1; }

        String[] numbersArray = numbers.split(delimiter);
    
        int runningTotal = 0;
    
        for (String number : numbersArray) {
    
            int numberInt = Integer.parseInt(number.trim());
    
            if (numberInt  1000) { continue; }
    
            runningTotal += numberInt;
    
        }
        return runningTotal;
    }
    

    You can aid testibility by having good patterns in your code. Nesting is going to be a killer to your ability to test code. It also has effects on the code complexity (cycliomatic complexity). Also while I test for nulls in this I usually use the null object pattern in my code. If you implement it properly it eradicated the need for null checks completely (FTW! :) !! After a while you’ll start building you applications with these patterns in mind and as you can see from the code below its easy to mock and test from the get go. Also for the thrown exceptions I would normally create custom exceptions and catch them for logging using AOP. If I were to reimplement your application (sorry I use spring quite a bit, as opposed to POJO) it would look like the following:

    @Component
    public class Calculator {

        @Autowired
        List preProcessingChain;
    
        private Integer add(final Integer... numbers) {
            Assert.notNull(numbers);
            Integer runningCount = 0;
            for (Integer integer : numbers) {
    
                // This allows for a complete decoupling using Dependency injection
                // (in this case Spring but it could be any other framework). This
                // allows for better testing an allows the method to be open for
                // extension but closed for modification.
                for (CountProcessingChainHandler handler : preProcessingChain) {
                    handler.preProcess(integer);
                }
    
                runningCount += integer;
            }
            return 0;
        }
    }
    
    
    // Interface for the handlers
    public interface CountProcessingChainHandler {
        void preProcess(Integer integer);
    }
    
    // Handles the default ordering. This can be overridden if necessary.
    abstract class GenericProcessingHandler implements CountProcessingChainHandler, Ordered {
        @Override
        public int getOrder() {
            return Ordered.LOWEST_PRECEDENCE;
        }
    }
    
    // A Link in the chain to handle negative cases.
    @Component
    public class CheckForNegativeHandler extends GenericProcessingHandler {
    
        @Override
        public void preProcess(Integer integer) {
            if (integer  1000){
                throw new RuntimeException("Cannot be greater then 1000: " + integer);
            }
        }
    }
    

    Keep up the good work though. Its hard work writing tutorials. :)

    • Ive a few minor errors in here (the return value being one) but I can’t edit :( Anyway hope it shows the basic ideas that I was trying to get across.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s