Discover TDD: Explained Through A Practical Example » The Bored Dev

Discover TDD: Explained through a practical example

If you’ve heard about the acronym TDD but you’re not sure what it means, or even if you know what TDD is but you don’t know how to put in into practice; then this article is for you. In this article you will find TDD explained in a very simple and pragmatic manner, guiding you through an exercise following TDD practices.

Let’s explain first what TDD is.

What is TDD

TDD stands for Test Driven Development, and as its name indicates is the practice where we write code using a test as a guidance of what to do next.
This leads us to what is one of my main principles when following a TDD approach:

Don’t write a single line of code if a test is not asking for it!

So what does it actually mean?

It’ll be easier to understand if we start explaining what the TDD process is like.

TDD Approach Process

The TDD process is quite simple:

  1. Write a failing test for your functionality
  2. Make the simplest change that makes that test pass
  3. Refactor code once the test is in place to protect the functionality
  4. Repeat

So that’s it? Is that simple?

Yes, that’s it. But TDD is a cycle that never ends; you write a failing test, make it pass and then refactor; then pick the next piece of functionality, and start over.

tdd process

What do we achieve by following this process? We achieve a few things:

  • Simplicity
    If we only write code when a test asks for it and we always write the simplest change that makes the test pass, the simplicity in our solution is guaranteed.
  • Coverage
    Guiding software development by tests and not writing any code that doesn’t make the current test pass, it’s absolutely impossible that any code that we write is not covered by our unit tests.
  • Less mental effort
    Another advantage of TDD is that we don’t have to overthink and think about the design beforehand, the solution most of times presents to ourselves after adding the right tests.

So how does it look in practice? Let’s do an exercise together!

tdd - do something great
Photo by Clark Tibbs on Unsplash

TDD in practice

We’re going to do an easy kata together to show how the process is. The kata in question is the Roman Numerals, I hope you enjoy it.

As shown in the link, some of the numbers we’ll have to convert are:

1 âž” I 
2 âž” II
3 âž” III
4 âž” IV
5 âž” V
9 âž” IX
21 âž” XXI
50 âž” L
100 âž” C
500 âž” D
1000 âž” M

Let’s write our first test!

@Test
public void convert_shouldReturnI() {
final String romanNumeral = RomanNumerals.convert(1);
assertThat(romanNumeral, is("I"));
}

To make that test compile first we had to create the simplest implementation of our RomanNumerals class.

public class RomanNumerals {
public static String convert(int number) {
return "";
}
}

If we run this test it actually FAILS, as expected.

Ok, so what do we do to make our first test pass? Remember that we have to make the simplest change to make the test pass; in this case, the simplest will be to return just “I”. We run our test and our first test is GREEN!

Now that our first test is green, we have to write our second test. For this particular example we have different choices on what test to pick next. It could be “2” for example, but that would be a double-digit in roman numerals; in this case, I prefer to pick the next single digit in roman numerals, so I pick 5 first.

@Test
public void convert_shouldReturnV() {
final String romanNumeral = RomanNumerals.convert(5);
assertThat(romanNumeral, is("V"));
}

If we run this test it FAILS as expected, as it returns “I”. Let’s change our implementation to make it pass, but now we have to keep in mind that the previous tests also has to pass after implementing our change. Again, let’s do the simplest!

public class RomanNumerals {
public static String convert(int number) {
if (number == 5) {
return "V";
}
return "I";
}
}

That’s it, simple! We just check the number and return the corresponding equivalent roman numeral.
I know what you’re thinking: that’s ugly! I know, but don’t worry about it; we’ll change it later.

We should avoid premature optimisations in TDD, that normally leads to overcomplicated solutions. We have to choose when to refactor our code; when we feel that our implementation is getting messy or lacking clarity we will tackle the refactoring.

To avoid making this post too long, I’m going to add the tests and the implementation after covering all the “single-digit” roman numerals, which are the simplest cases. Then we’ll be in the position to tackle the next challenges. Let’s see how it’d look like:

public class RomanNumeralsTest {
@Test
public void convert_shouldReturnI() {
final String romanNumeral = RomanNumerals.convert(1);
assertThat(romanNumeral, is("I"));
}
@Test
public void convert_shouldReturnV() {
final String romanNumeral = RomanNumerals.convert(5);
assertThat(romanNumeral, is("V"));
}
@Test
public void convert_shouldReturnX() {
final String romanNumeral = RomanNumerals.convert(10);
assertThat(romanNumeral, is("X"));
}
@Test
public void convert_shouldReturnL() {
final String romanNumeral = RomanNumerals.convert(50);
assertThat(romanNumeral, is("L"));
}
@Test
public void convert_shouldReturnC() {
final String romanNumeral = RomanNumerals.convert(100);
assertThat(romanNumeral, is("C"));
}
@Test
public void convert_shouldReturnD() {
final String romanNumeral = RomanNumerals.convert(500);
assertThat(romanNumeral, is("D"));
}
@Test
public void convert_shouldReturnM() {
final String romanNumeral = RomanNumerals.convert(1000);
assertThat(romanNumeral, is("M"));
}
}

All the new tests will FAIL if we run them now, so let’s see how we change the implementation in the simplest way to make them pass:

public class RomanNumerals {
public static String convert(int number) {
if (number == 5) {
return "V";
} else if (number ==10) {
return "X";
} else if (number == 50) {
return "L";
} else if (number == 100) {
return "C";
} else if (number == 500) {
return "D";
} else if (number == 1000) {
return "M";
}
return "I";
}
}

Great! Our tests are all GREEN! But wait a sec, our implementation is getting a bit messy, right?

Let’s refactor it! At this point it’s becoming a problem and we have all the single-digit cases covered; let’s change it!
The use of if conditions to determine the equivalence is not the best approach for this exercise, so we’re going to define a structure to hold these equivalences. For example, using a Java Map to store the equivalences would allow us to fetch each equivalence with O(1) every time. I’m using JDK 11, so I’ll be using Map.of to initialise this Map structure, which is available since JDK 9.

This new implementation ends up being like this:

import java.util.Map;
public class RomanNumerals {
private static final Map<Integer, String> ROMAN_NUMERALS = Map.of(
1, "I",
5, "V",
10, "X",
50, "L",
100, "C",
500, "D",
1000, "M"
);
public static String convert(int number) {
return ROMAN_NUMERALS.get(number);
}
}

Much cleaner now, right? I hope you like it.


After completing the refactoring, we run all our tests again to check that we haven’t broken anything. Good news, all GREEN!
Now that we have all the single-digit cases tested and we have refactored the implementation, it’s time to tackle the next challenge. Let’s write a test for the first double-digit roman numeral; I’ll pick 2 in this case.

@Test
public void convert_shouldReturnII() {
final String romanNumeral = RomanNumerals.convert(2);
assertThat(romanNumeral, is("II"));
}

If we run this test, it FAILS because we don’t have any equivalence for number 2 in our structure.
It’s time to make it pass, but that’s going to be more complicated than the first time. If we think about it, we have two choices: add an equivalence for number 2 in our structure or make a change to return two concatenated “I”.

We could go for the first option, but at some point we’d realise that we cannot store all the numbers in our structure; even in this exercise, where we are going to support numbers up to 1,000, it’s still too much.
So let’s try to do something so we process number 2 as 2 = 1 + 1.

If we think about it, what we have to do is to get the closest equivalence which is lower or equal than the number that we want to process.
So for number 2 we’ll get the closest key lower or equal, which happens to be 1; then we’ll get the remaining part and repeat.

Let’s see how our first approach looks like:

public class RomanNumerals {
private static final NavigableMap<Integer, String> ROMAN_NUMERALS = new TreeMap<>() {
{
put(1, "I");
put(5, "V");
put(10, "X");
put(50, "L");
put(100, "C");
put(500, "D");
put(1000, "M");
}
};
public static String convert(int number) {
final String result = ROMAN_NUMERALS.get(number);
return result != null ? result : ROMAN_NUMERALS.get(ROMAN_NUMERALS.lowerKey(number));
}
}

As you can see we’ve replaced our equivalences storage structure by a NavigableMap interface, which provides a lowerKey method that will do the work for us. However, although this implementation is capable of fetching the lower key for us, it still returns only single-digit roman numerals; unfortunately our test still FAILS.
We have to find a way to iterate again and find the equivalence for the remaining part. I think recursion will help us in this case, let’s implement it.

public class RomanNumerals {
private static final NavigableMap<Integer, String> ROMAN_NUMERALS = new TreeMap<>() {
{
put(1, "I");
put(5, "V");
put(10, "X");
put(50, "L");
put(100, "C");
put(500, "D");
put(1000, "M");
}
};
public static String convert(int number) {
final String result = ROMAN_NUMERALS.get(number);
return result != null ? result : calculateRomanNumeral(number);
}
private static String calculateRomanNumeral(int number) {
final Integer lowerKey = ROMAN_NUMERALS.lowerKey(number);
return ROMAN_NUMERALS.get(lowerKey) + convert(number - lowerKey);
}
}

If you’re new to recursion, it can be a bit mind-blowing at the beginning to clarify the concept in your head, but it’s simpler than what it looks like.
What we’ve done is to check an equivalence first; if there’s no equivalence then find the lower key, get the equivalence for that key and call convert method again for the remaining part (number – lowerKey).
If we run our tests now, they are all GREEN! That’s good news.

So what’s next? What about triple-digit roman numerals? Let’s try number 3.

@Test
public void convert_shouldReturnIII() {
final String romanNumeral = RomanNumerals.convert(3);
assertThat(romanNumeral, is("III"));
}

Surprise! It’s GREEN, we don’t have to do anything. But what about number 4? We have to keep in mind that this number is a special case, as it’s equivalence is “IV”.

@Test
public void convert_shouldReturnIV() {
final String romanNumeral = RomanNumerals.convert(4);
assertThat(romanNumeral, is("IV"));
}

As expected, it FAILS. How do we solve this? It’s a special case, because it subtracts “I” from “V”.
Sounds tricky, but let’s think about how many of these “special cases” do we have. We have IV (4), IX (9), XC (90) and CM (900) only, so it doesn’t really make sense making a big effort; let’s do the simplest, add equivalences to our structure for them.

So our new structure of equivalences would be:

private static final NavigableMap<Integer, String> ROMAN_NUMERALS = new TreeMap<>() {
{
put(1, "I");
put(4, "IV");
put(5, "V");
put(9, "IX");
put(10, "X");
put(50, "L");
put(90, "XC");
put(100, "C");
put(500, "D");
put(900, "CM");
put(1000, "M");
}
};

If we add tests for the numbers 4 and 9 they’re GREEN. What’s left? What about number 6? It’s different, as it’s equivalence is “VI”, which adds “I” to “V”. Let’s add a test:

@Test
public void convert_shouldReturnVI() {
final String romanNumeral = RomanNumerals.convert(6);
assertThat(romanNumeral, is("VI"));
}

If we run the test it’s GREEN as well! Our implementation has done the magic. I think it’s time to try a more complicated number, for example number 838. Let’s add the test:

@Test
public void convert_shouldReturnDCCCXXXVIII() {
final String romanNumeral = RomanNumerals.convert(838);
assertThat(romanNumeral, is("DCCCXXXVIII"));
}

Our test is GREEN! I think we’re done, we have a robust implementation that support roman numerals from 1 to 1,000.

Conclusion about TDD

One more thing that we could do at the end is refactor our tests if there are too many of them. In our case we could transform our JUnit 4 tests to use Parameterised, which would remove some redundancy in our tests. However, I will leave that as an exercise for you; we’ve covered enough in this post and doing that refactoring it’s out of the scope of showing what TDD is.

If you interested in reading Java articles, you can find more of our Java articles here.

So that’s it! I hope you’ve enjoyed this exercise and that you had gained a good understanding of what the TDD process is.

If you have enjoyed following this article, please subscribe to be notified when my new articles are published!

Thank you very much for reading!

One comment