Within this blog post, I am going to talk about Test-Driven Development (TDD). And I will walk through a concrete example that I hope you will be able to follow and implement yourself.
But before I begin with the implementation, let us start with what TDD is. Test-driven development (TDD) is a software development process that relies on the repetition of a very short development cycle: requirements are turned into very specific test cases, then the software is improved so that the tests pass. This is opposed to software development that allows software to be added that is not proven to meet requirements.
And Kent Beck defines TDD in his Test-Driven Development By Example as follows: Test-driven development is a set of techniques that any software engineer can follow, which encourage simple design and test suites that inspire confidence.
Let’s start with an example. Assume that we have been asked to design a bank account application as an API(Application Programming Interface) that should have the following functionalities:
- I would like to be able to withdraw money from my account when there is sufficient balance
- I would like to be able to see the transactions of my account
Let’s start implementing the first requirement which is “I would like to be able to withdraw money from my account”
The first step is we write a failing test. But to be able to do that we need to think about the requirements and design of the API. Of course, we don’t need to design all the classes at once. As we will have tests we will be able to refactor easily and confidently.
For our API I think we need a BankAccountManager class that exposes the required functions. And we start with its test class which is BankAccountManagerTest. Name convention for test classes is [ClassName]Test.
We assume that ‘BankAccountManager’ class has a ‘withdraw’ method that returns ‘OK’ if there is sufficient balance in the account. And there is a ‘balance’ method that returns the balance.
package tdd;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class BankAccountManagerTest {
@Test
public void thereIsSufficientBalance() {
BankAccountManager bankAccountManager = new BankAccountManager(400);
Assertions.assertEquals("OK", bankAccountManager.withdraw(50), "There is no sufficient balance");
Assertions.assertEquals(350, bankAccountManager.balance(), "The balance is not 350");
}
}
If you are using an IDE(Integrated Development Environment) that will help you to create required classes and methods from the test. What I am using as IDE for this demonstration is IntelliJ.
Once we create the BanckAccountManager class by selecting “Create class ‘BanckAccountManager’“ now we need to create the ‘withdraw’ method.
BankAccountManager class has been created automatically by the IDE as follows. Now we have a test method and actual method which compile.
package tdd;
public class BankAccountManager {
public BankAccountManager(int balance) {
}
public Object withdraw(int amount) {
return null;
}
public int balance() {
return 0;
}
}
Let’s run the first test to see if it fails then we will make it pass. To run the test method we just click the play button next to it.
The first test is failing as we expected.So it is time to make it green! We just change the return values of the two methods to make the test green.
package tdd;
public class BankAccountManager {
public BankAccountManager(int balance) {
}
public Object withdraw(int amount) {
return "OK";
}
public int balance() {
return 350;
}
}
Obviously this is not what we want. Withdraw and balance methods should not return hardcoded values. So let’s add one more failing test.
package tdd;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class BankAccountManagerTest {
@Test
public void thereIsSufficientBalance() {
BankAccountManager bankAccountManager = new BankAccountManager(400);
Assertions.assertEquals("OK", bankAccountManager.withdraw(50), "There is no sufficient balance");
Assertions.assertEquals(350, bankAccountManager.balance(), "The balance is not 350");
}
@Test
public void thereIsNoSufficientBalance() {
BankAccountManager bankAccountManager = new BankAccountManager(400);
Assertions.assertEquals("NO_SUFFICIENT_BALANCE", bankAccountManager.withdraw(450), "There is sufficient balance");
}
}
We just make the following changes to make both tests green:
package tdd;
public class BankAccountManager {
private int balance;
public BankAccountManager(int balance) {
this.balance = balance;
}
public Object withdraw(int amount) {
if (amount < balance) {
balance = balance - amount;
return "OK";
}
return "NO_SUFFICIENT_BALANCE";
}
public int balance() {
return balance;
}
}
Now both tests are green. But I think we missed one more case which is withdrawing all balance. So let’s add a test for it as well.
@Test
public void thereIsSufficientBalance() {
BankAccountManager bankAccountManager = new BankAccountManager(400);
Assertions.assertEquals("OK", bankAccountManager.withdraw(50), "There is no sufficient balance");
Assertions.assertEquals(350, bankAccountManager.balance(), "The balance is not 350");
Assertions.assertEquals("OK", bankAccountManager.withdraw(350), "There is no sufficient balance");
Assertions.assertEquals(0, bankAccountManager.balance(), "The balance is not 0");
}
We changed if statement in the 'withdraw' method to make test green:
public Object withdraw(int amount) {
if (amount <= balance) {
balance = balance - amount;
return "OK";
}
return "NO_SUFFICIENT_BALANCE";
}
It is time to start implementing the second requirement which is “I would like to be able to see the transactions of my account”
For this, we add the following test and assume that BankAccountManager will have a ‘transactions’ method that returns the transactions of the account as a collection.
@Test
public void transactions() {
BankAccountManager bankAccountManager = new BankAccountManager(400);
Assertions.assertEquals(Collections.EMPTY_LIST, bankAccountManager.transactions(), "The transactions are not empty");
}
Then we created the ‘transactions’ method via Intellij:
public Object transactions() {
return null;
}
Of course, the test is failing! To make it green we do this:
public Object transactions() {
return Collections.EMPTY_LIST;
}
Now we need another failing test which is:
@Test
public void transactions() {
Assertions.assertEquals(Collections.EMPTY_LIST, bankAccountManager.transactions(), "The transactions are not empty");
bankAccountManager.withdraw(10);
bankAccountManager.withdraw(15);
bankAccountManager.withdraw(20);
Assertions.assertEquals(Arrays.asList(10, 15, 20), bankAccountManager.transactions());
}
And it is time to implement the ‘transactions’ logic:
public class BankAccountManager {
private int balance;
private List<Integer> transactions = new ArrayList<Integer>();
public BankAccountManager(int balance) {
this.balance = balance;
}
public Object withdraw(int amount) {
if (amount <= balance) {
balance = balance - amount;
transactions.add(amount);
return "OK";
}
return "NO_SUFFICIENT_BALANCE";
}
public int balance() {
return balance;
}
public Object transactions() {
return transactions;
}
}
Now we have implemented all the requirements. But if you noticed that there is some code duplication in tests. Can we refactor them? Of course we can do that confidently as all code is covered by the tests. Let’s do that then!
Creation of BankAccountManager is moved to the ‘setUp’ method that will be run before each test.
public class BankAccountManagerTest {
BankAccountManager bankAccountManager;
@BeforeEach
public void setUp() {
bankAccountManager = new BankAccountManager(400);
}
@Test
public void thereIsSufficientBalance() {
Assertions.assertEquals("OK", bankAccountManager.withdraw(50), "There is no sufficient balance");
Assertions.assertEquals(350, bankAccountManager.balance(), "The balance is not 350");
Assertions.assertEquals("OK", bankAccountManager.withdraw(350), "There is no sufficient balance");
Assertions.assertEquals(0, bankAccountManager.balance(), "The balance is not 0");
}
@Test
public void thereIsNoSufficientBalance() {
Assertions.assertEquals("NO_SUFFICIENT_BALANCE", bankAccountManager.withdraw(450), "There is sufficient balance");
}
@Test
public void transactions() {
Assertions.assertEquals(Collections.EMPTY_LIST, bankAccountManager.transactions(), "The transactions are not empty");
bankAccountManager.withdraw(10);
bankAccountManager.withdraw(15);
bankAccountManager.withdraw(20);
Assertions.assertEquals(Arrays.asList(10, 15, 20), bankAccountManager.transactions());
}
}
That’s the end of the exercise. I hope you enjoyed it and were able to learn something new. The most important take-away from this exercise is to take small steps! The source code of this project can be found on my github page.