It is meant to support you by focusing on “what” before “how”.
A lot of programmers find the idea of writing tests before writing code absurd. They think that it is not useful and slows down the development process. In a sense, that is correct and it does impact the speed of development. But the speed of development would hardly matter if your system is not resilient.
A developer's job is to deliver code that is not only functional but readable and maintainable. And Test-driven development helps you do that.
According to Martin Fowler, TDD is:
- Writing a test for the next bit of functionality you want to add.
- Writing the functional code until the test passes.
- Refactoring both new and old code to make it well structured.
One of the most important aspects of TDD is that it shifts the focus to WHAT of a problem from HOW.
Let’s Code
We will apply TDD to a problem that is generally asked in interviews and try to come up with a solution. The problem is called Reverse Polish Notation.
In reverse Polish notation, the operators follow their operands; for instance, to add 3 and 4, one would write _3 4 +_
rather than _3 + 4_
. If there are multiple operations, operators are given immediately after their second operands; so the expression is written _3 − 4 + 5_
in conventional notation would be written _3 4 − 5 +_
in reverse Polish notation: 4 is first subtracted from 3, then 5 is added to it.
We will divide the problem into smaller steps and think about “what” each step expects us to do. We will consider “how” later. The language used to solve is Java.
Test 1
Our starting state is an input string: 3 4 +
And what we want to achieve is to have control over each char so that we can operate on that. So we want a string array from a string.
Our test will look like this:
public void removeSpacesFromTheString() {
ReversePolishNotation rpn = new ReversePolishNotation();
String[] result = rpn.getStringArray("3 4 +");
assertEquals("3", result[0]);
assertEquals("4", result[1]);
assertEquals("+", result[2]);
}
Once we have the test that defines what we need, we will think about how to do that. To convert a string into a string array is pretty straight forward.
public String[] getStringArray(String s) {
return s.split(" ");
}
Test 2
Our first test and its implementation are ready. Let’s look into what we want next. We want to apply the operator to the numbers that we encounter.
So in this case, we want to add the two numbers.
public void addNumbersWhenPlusOperatorIsFound() {
ReversePolishNotation rpn = new ReversePolishNotation();
double result = rpn.evaluate("3 4 +");
assertEquals(7.0, result, 0.1);
}
So we want a method that will apply the operator to our numbers. Since we only have two numbers we can just take them and add them.
public double evaluate(String s) {
String[] elements = getStringArray(s);
double result = 0.0;
for (int i = 0; i < elements.length; i++) {
String c = elements[i];
if (c.equals("+") && i >= 2) {
result = parseDouble(elements[i - 1]) + parseDouble(elements[i - 2]);
}
}
return result;
}
We iterate over our array and add the numbers we encounter +
.
Test 3
Now let's make it interesting by adding multiple +
operators in the string. Our test would fail because it is hardcoded. For the input 5 5 2 + +
, our implementation will fail.
We want our function to return the result of this string 5 + (5 + 2)
. This is how our code should evaluate this.
public void evaluateWhenMultiplePlusOperatorsAreFound() {
ReversePolishNotation rpn = new ReversePolishNotation();
double result = rpn.evaluate("5 5 2 + +");
assertEquals(12.0, result, 0.1);
}
To do that we need to come up with a way to deal with operators and numbers using some data structure. In this case, we can use a Stack
to push the numbers until we find an operator and apply that by popping the last two values.
public double evaluate(String s) {
String[] elements = getStringArray(s);
Stack<Double> numbers = new Stack();
double result = 0.0;
for (int i = 0; i < elements.length; i++) {
String c = elements[i];
if (c.equals("+") && i >= 2) {
result = numbers.push(numbers.pop() + numbers.pop());
} else {
numbers.push(Double.parseDouble(c));
}
}
return result;
}
Test 4
The next step is to make sure that we are able to apply all the four operators, which is quite simple now. We just need to check for different operators and apply them accordingly.
public void addNumbersWhenMinusOperatorIsFound() {
ReversePolishNotation rpn = new ReversePolishNotation();
double result = rpn.evaluate("5 5 2 + -");
assertEquals(2.0, result, 0.1);
}
public void multiplyNumbersWhenMultiplyOperatorIsFound() {
ReversePolishNotation rpn = new ReversePolishNotation();
double result = rpn.evaluate("5 5 2 + *");
assertEquals(35.0, result, 0.1);
}
We can jump a step and implement the method for all the operators. It will take care of multiply and divide as well.
public double evaluate(String s) {
String[] elements = getStringArray(s);
Stack<Double> numbers = new Stack();
double result = 0.0;
for (String c : elements) {
switch (c) {
case "+":
result = numbers.push(numbers.pop() + numbers.pop());
break;
case "-":
result = numbers.push(numbers.pop() - numbers.pop());
break;
case "*":
result = numbers.push(numbers.pop() * numbers.pop());
break;
case "/":
result = numbers.push(numbers.pop() / numbers.pop());
break;
default:
numbers.push(Double.parseDouble(c));
break;
}
}
return result;
}
Final Test
Everything works and it seems that we have reached our end result. Let’s test it for large input 10 6 9 3 + -11 * / * 17 + 5 +
. The input contains multiple operators and also negative numbers.
public void evaluateWhenAllOperatorAreFound() {
ReversePolishNotation rpn = new ReversePolishNotation();
double result = rpn.evaluate("10 6 9 3 + -11 * / * 17 + 5 +");
assertEquals(21.5, result, 0.1);
}
Our implementation at this point fails. It is a little surprising because it should have worked, but it seems that the operators —
and \
work differently than +
and *
. Let’s change the implementation to handle this.
public double evaluate(String s) {
String[] elements = getStringArray(s);
Stack<Double> n = new Stack();
double result = 0.0;
for (String c : elements) {
switch (c) {
case "+": {
result = n.push(n.pop() + n.pop());
break;
}
case "-": {
double fNumber = n.pop();
double sNumber = n.pop();
result = n.push(sNumber - fNumber);
break;
}
case "*": {
result = n.push(n.pop() * n.pop());
break;
}
case "/": {
double fNumber = n.pop();
double sNumber = n.pop();
result = n.push(sNumber / fNumber);
break;
}
default:
n.push(Double._parseDouble_(c));
break;
}
}
return result;
}
In the next blog, we will see how to get rid of the switch case.
Advantages
We have reached our final solution. Take some time to understand this. The focus was always on what we expect from our algorithm before we start implementing it. The advantages of TDD are real:
- You write better software.
- You avoid over-engineering.
- You have protection when introducing new features.
- Your software is self-documented.
Final Thought
I have tried to explain why we should use Test-Driven development. There are better articles out there that can help you understand how to do it. One of them is here. TDD is really a technique for design. The foundations of TDD are focused on using small tests to design systems from the ground up in an emergent manner.
It might seem slow at first, but with practice, you will gain momentum.
I hope this helped you understand the reasoning behind TDD.