Test-Driven Development By Example

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

Concrete Example

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: 

  1. I would like to be able to withdraw money from my account when there is sufficient balance
  2. I would like to be able to see the transactions of my account

The First Requirement

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";
}

The Second Requirement

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;
   }
}

Refactoring

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());
   }
}

Final Thoughts

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.

Fatih is a software craftsman, clean code & TDD lover, Scrum & Kanban practitioner. He is a passionate technologist that has been in the industry for over 15 years. He has contributed to critical projects in finance, automotive, retail, telecommunications, and healthcare sector. Currently, he is interested in cloud technologies and distributed systems.

1 COMMENT

LEAVE A REPLY

Please enter your comment!
Please enter your name here