Test-Driven Development With Java Spring Boot and Junit5

2024-08-18
By: Daham Navinda
Unit test flow
Unit test work flow

What is TDD?

In layman’s terms, Test Driven Development (TDD) is a software development practice that focuses on creating unit test cases before developing the actual code.

Why Do we need TDD?

When following TDD approach developers are focused on one feature at a time. Therefore when following such a phase code naturally becomes clean and developers can easily modularize the code. Clean and modularized code is easy to maintain and expand.

When you first try to implement TDD practices in your teams you might start to think that it consumes more time and it leads to an increase in the project cost. But if you are working on complex projects this will benefit you a lot in the long run.

Bugs are a nightmare for developers. If you follow a test-driven approach that will lead to reducing bugs in your system.

Our favorite Uncle Bob defines TDD with 3 rules.

  1. You are not allowed to write any production code unless it is to make a failing unit test to pass
  2. You are not allowed to write any more of a unit test than sufficient to fail
  3. You are not allowed to write any more production code than sufficient to pass the failing unit test.

Enough of the theory, not lets get our hands dirty with real test-driven development examples. In this example, I am following the below approach.

i. First I write a failing unit test (red).

ii. Then I write the required code or the business logic to make the unit test pass (Green).

iii. Refactor the code (Yellow)

iv. Then Repeat the cycle.

The cycle of TDD goes like Red -> Green -> Refactor -> Repeat

Unit test flow
Unit test work flow

When we write unit tests we need to consider the readability of the code. Many developers are encouraged to write dirty unit tests to deliver the product fast, but this may lead to inefficiencies in the long run as the code expansions are heavily dependent on unit tests. Therefore always keep in mind that we need to write clean unit test codes.

For this example I am using java8, springboot to create a simple rest api and junit5 and mockito to create unit tests for the application. As for the demonstration purposes I have neglected best coding practices in the following code and assume that you already have a basic understanding of Rest api, java and springboot. Therefore I won't be going to depth into those topics.

Requirement

We need to create an application that takes users total income and required months as user input and returns the total tax user needs to pay.

I have created a simple spring boot project with a single controller. you can use this project to continue on the tutorial.

The controller In the beginning, we have only a simple controller which returns null when we send a get request to the http://localhost:8087/getTax?income=1234&months=3 .

we need to implement the business logic here to take the income and months as user inputs and process the tax.

java
package com.application.tdd;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TaxController {
    @GetMapping("/getTax")
    public ResponseEntity calculateTax(@RequestParam int income , @RequestParam int months){
         return ResponseEntity.ok().body(null);
    }
}

Starting with TDD

Now we need to implement the business logic. We start implementing this with a unit test. In Java, all the test classes need to be created under the test directory. You can create packages and structure your code accordingly. I have created ProcessTaxTest class to implement my unit test for tax processing.

Unit test flow
Unit test work flow

After creating the class we can start writing unit tests. Here I have created processTax class which will be used to implement the tax calculation code. But this class is not existing in our codebase (red sign) therefore we need to create it now.

java
package com.application.tdd;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
public class ProcessTaxTest {
    @InjectMocks
    ProcessTax processTax;
    @BeforeEach
    void init() {
        MockitoAnnotations.openMocks(this);
    }
}

After creating this class you need to import this class to the ProcessTaxTest class.

Unit test flow
Unit test work flow

Now let's start writing unit tests related to the tax calculation. we will be using following equation to calculate tax totalTax = income * tax percentage * number of months as an example if income = 10, tax percentage=0.3 and number of months = 5 if should return 15 as the totaltax. The next step is to write uni tests to calculate tax. testTaxCalcualtion method is used for that. As we haven’t implemented any code inside the ProcessTax class our IDE will indicate an error. Therefore, we need to fix that before proceeding with the next step. we need to implement the business logic. We start implementing this with a unit test. In Java, all the test classes need to be created under test directory. You can create packages and structure your code accordingly. I have created ProcessTaxTest class to implement my unit test for tax processing. After creating the class we can start writing unit tests. Here I have created processTax class which will be used to implement the tax calculation code. But this class is not existing in our codebase (red sign) therefore we need to create it now.

java

package com.application.tdd;
import com.application.tdd.service.ProcessTax;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
public class ProcessTaxTest {
    @InjectMocks
    ProcessTax processTax;
    @BeforeEach
    void init() {
        MockitoAnnotations.openMocks(this);
    }
    @Test
    public void testTaxCalcualtion(){
        int income =10;
        int months =5;
        double totalTax=processTax.calculate( income, months);
    }
}

Now go to the ProcessTax class and implement the calculate method with relevant parameters. Note that we haven't implemented any tax calculation logic here.

java
package com.application.tdd.service;
@Service
public class ProcessTax {
    public double calculate(int income, int months){
        return 0;
    }
}

Now let's go back to our ProcessTaxTest class. we need to write a unit test related to tax calculation. As an example, we need to verify that our calculator class is doing the job correctly. Junit assertions can be used for this.

java
package com.application.tdd;
import com.application.tdd.service.ProcessTax;
import junit.framework.Assert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
public class ProcessTaxTest {
    @InjectMocks
    ProcessTax processTax;
    @BeforeEach
    void init() {
        MockitoAnnotations.openMocks(this);
    }
    @Test
    public void testTaxCalcualtion(){
        int income =10;
        int months =5;
        double totalTax=processTax.calculate( income, months);
        Assert.assertEquals(15.0,totalTax);
    }
}

when we run this unit test it will indicate an error. Even though we expect 15 to be the output it will return 0. Now it's time to correct this unit test failure by writing the correct logic in the calculator class.

java
package com.application.tdd.service;
@Service
public class ProcessTax {
    private double taxPercentage=0.3;
    public double calculate(int income, int months){
        return income*months*taxPercentage;
    }
}

Now we have successfully written the code required to calculate total tax using the test-driven approach.

Additional Step

as an additional step, we can integrate our ProcessTax class into the controller class to calculate the tax when the user sends a request.

java

package com.application.tdd;
import com.application.tdd.service.ProcessTax;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TaxController {
    @Autowired
    ProcessTax processTax;
    @GetMapping("/getTax")
    public ResponseEntity calculateTax(@RequestParam int income , @RequestParam int months){
         return ResponseEntity.ok().body(processTax.calculate(income,months));
    }
}

Congratulations now you have successfully developed a TDD bases application from scratch.