Test-Driven Development in Spring Boot: Replacing Components and Configurations for Maximum Flexibility

Iacopo Palazzi
7 min readJan 3, 2023
Unit Testing with Spring Boot

Purpose of this post is to provide an example of how to implement unit tests in a Spring Boot application. It shows how to configure the application to replace components and configurations as needed in order to design a software architecture that adapts automatically between runtime and test phase.

Test Driven Development (TDD) is an important pattern to follow in order to develop high-quality software. It helps you think about robust design and eases the process of shaping the architecture using a modular approach, which will result in a more maintainable codebase. Of course, it will also anticipate and reduce bugs.

There are many ways to do that, according to the language/framework of your choice. However, it can be challenging to find a clear and simple example on how to do that using Java with Spring Boot. That’s why I’m writing down my findings in this post.

TL;DR

For you busy people in need of fast answers, here some tips:

  • Clone the project source from here.
  • Run the project with mvn spring-boot:run
  • Run tests with mvn test
  • Check DemoApplicationTests and DemoConfigurationTest to get an idea at first sight.

But I suggest you to keep reading, it’s healthy :)

Context

I will assume that you know what TDD is as well as how to manage Spring Boot applications, including the lifecycle of components, so I will not spend words on describing those. Instead, it’s important to provide some context on what kind of setup I needed at the time of writing this example.

When you write software with Spring Boot, you usually need to create basic elements such as configurations and components, which define the building blocks of the application. To implement TDD with Spring Boot, you typically need to write test cases that mock configurations and classes (beans) and provide test-oriented implementations of those elements, targeting the testing context.

In my example, what I did is creating a command-line tool that retrieves the current date from an external endpoint and prints it to the screen. The test unit for this application simply checks that the data model used as output of the Spring Boot service, which implements this logic, is respected, without calling the real endpoint but instead creating a synthetic sample.

This kind of test is very simple and not necessarily useful in general, but it demonstrates the following points:

  • how to configure the Spring Boot project to handle the definition of the unit test;
  • how to mock configurations and beans (services) without altering the structure of the software;
  • integrating the test suite in a seamless way.

For this examples, I have:

  • Created a standard, plain Spring Boot v2.7.5 project using Spring Initializr.
  • Used Maven as building and dependency manager tool.
  • Used Java 11 to run the example.

Project

You can find the GitHub repository containing the project here: Unit Testing with Spring Boot.

File Structure

.
├── HELP.md
├── LICENSE
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
│ └── java
│ └── com
│ └── example
│ └── demo
│ ├── DateResult.java
│ ├── DemoApplication.java
│ ├── DemoApplicationCommandLineRunner.java
│ ├── DemoConfiguration.java
│ ├── MyService.java
│ └── MyServiceImpl.java
└── test
└── java
└── com
└── example
└── demo
├── DemoApplicationTests.java
├── DemoConfigurationTest.java
└── MyTestServiceImpl.java

It’s a very plain straightforward setup: src/main contains the real application implementation, the src/test contains unit tests.

Dependencies

The only dependencies I’ve used are:

  • spring-boot-starter: which is the main Spring Boot one.
  • spring-boot-starter-test: which enables tests.
  • spring-boot-starter-webflux: to provide the WebClient used to retrieve the current date at runtime.
  • lombok: because I’m lazy.

Check the pom.xml for more details.

Main Components

Let’s split component between runtime and tests:

  • runtime are the ones containing the actual implementation of the application’s logic.
  • tests contain the definition of test cases.

Runtime

The runtime part of the project contains the following elements:

MyService is the Java interface used to decouple the implementation with the reference to be used transparently between runtime and unit testing:

package com.example.demo;

public interface MyService {
DateResult getDate();
}

MyServiceImpl is the actual implementation of the logic:

package com.example.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;

@Slf4j
public class MyServiceImpl implements MyService
{
@PostConstruct
private void init ()
{
log.info("Creating MyServiceImpl");
}

@Override
public DateResult getDate ()
{
WebClient webClient = WebClient.create("http://date.jsontest.com/");

Mono<DateResult> dateResultMono = webClient
.get()
.retrieve()
.bodyToMono(DateResult.class);

return dateResultMono.block();
}
}

The instance of the MyServiceImpl component is provided by the DemoConfiguration element which is the following:

package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DemoConfiguration {

@Bean
public MyService getService()
{
return new MyServiceImpl();
}

}

That’s where Spring Boot knows how to create the instance of the service to be used at runtime!

Finally, the DemoApplicationCommandLineRunner component is the one really using MyServiceImpl instance, which is auto-wired by Spring Boot as a private variable declared as its own interface. Take a look at the code:

package com.example.demo;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class DemoApplicationCommandLineRunner implements CommandLineRunner
{
@Autowired
private MyService service;

@Override
public void run (String... args) throws Exception
{
log.info("That's the time: {}", service.getDate());
}
}

So, if you clone the repository and place a terminal in the root folder, running the following command you’ll get the current date.

Execute:

mvn spring-boot:run

Output:

[...]

2023-01-03 17:41:49.304 INFO 60489 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 11.0.17 on <host> with PID <pid> (<path_to_project>/target/classes started by <user> in <path_to_project>)
2023-01-03 17:41:49.306 INFO 60489 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
2023-01-03 17:41:49.684 INFO 60489 --- [ main] com.example.demo.MyServiceImpl : Creating MyServiceImpl
2023-01-03 17:41:49.806 INFO 60489 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.839 seconds (JVM running for 1.131)
2023-01-03 17:41:50.610 INFO 60489 --- [ main] c.e.d.DemoApplicationCommandLineRunner : That's the time: DateResult(date=01-03-2023, millisecondsSinceEpoch=1672764110559, time=04:41:50 PM)

[...]

You will notice that you are getting the time from the runtime version of MyServiceImpl component from two main aspects:

  • The date changes every time you run it.
  • There is a log line saying “com.example.demo.MyServiceImpl: Creating MyServiceImpl” which is provided by the post-construct method defined into the MyServiceImpl class.

Tests

Ok, great, but how to implement and run tests replacing the implementation of MyServiceImpl, without changing the structure of the previous code?

That’s where the DemoApplicationTests and DemoConfigurationTests come into place. They are both placed into the testing part of the repository under src/test.

DemoApplicationTests is component which defines real unit tests to be run during testing phase. I will not report the entire code, just the relevant part of it.

Just consider the annotations at class level:

@SpringBootTest(classes = DemoConfigurationTest.class)
@ActiveProfiles("test")
class DemoApplicationTests
{
...
}

The first one, @SpringBootTest, is telling Spring Boot that this class contains the definition of unit tests and also to use the DemoConfigurationTest component class as part of the testing suite.

The second one, the @ActiveProfiles, instructs Spring Boot to activate the profile named “test”, which will be used in the DemoConfigurationTest component.

This part is telling this component to get a reference to an instance of MyService:

@Autowired
private MyService service;

And finally, you can find the implementation of test cases called positiveTest and negativeTest, annotated with @Test, which is the way you instruct Spring Boot to thread them as unit tests:

@Test
void positiveTest ()
{
log.info("### Performing positive test");
DateResult dateResult = service.getDate();
// Test definition
}

@Test
void negativeTest ()
{
log.info("### Performing negative test");
DateResult dateResult = service.getDate();
// Test definition
}

Ok, so now let’s describe the DemoConfigurationTest component:

package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("test")
public class DemoConfigurationTest
{
@Bean
public MyService getService()
{
return new MyTestServiceImpl();
}
}

As you can see, this class is still a Spring Boot configuration component (see the @Configuration annotation) exactly as the DemoConfiguration one. The main difference here is that it also contains the @Profile(“test”) annotation which is the way you tell Spring Boot to use it in replacement of the other one when the active profile is “test”, that is, during test phase, not runtime!

The getService() method of the DemoConfigurationTest class will be used to define the MyService instance in replacement of the DemoConfiguration class, providing the testing version of MyService which is called MyTestServiceImpl.

MyTestServiceImpl is another component implementing the same application interface MyService, but it’s declared into the test context of the Spring Boot project (under src/test folder) which is defined like this:

package com.example.demo;

import lombok.extern.slf4j.Slf4j;

import javax.annotation.PostConstruct;

@Slf4j
public class MyTestServiceImpl implements MyService
{
@PostConstruct
private void init ()
{
log.info("Creating MyTestServiceImpl");
}

@Override
public DateResult getDate()
{
DateResult dateResult = new DateResult();

dateResult.setDate("01-01-1970");
dateResult.setTime("00:00:00 AM");
dateResult.setMillisecondsSinceEpoch("0");

return dateResult;
}
}

As you can see, this time the result is a mocked, static DateResult element containing always the same values, just for testing purposes.

To see how the test works in action, place a terminal in the root folder of the repository and run the following command:

mvn test

The output will be something like this:

[...]

2023-01-03 18:17:40.088 INFO 63410 --- [ main] com.example.demo.DemoApplicationTests : Starting DemoApplicationTests using Java 11.0.17 on <host> with PID <pid> (started by <user> in <path_to_project>)
2023-01-03 18:17:40.090 INFO 63410 --- [ main] com.example.demo.DemoApplicationTests : The following 1 profile is active: "test"
2023-01-03 18:17:40.366 INFO 63410 --- [ main] com.example.demo.MyTestServiceImpl : Creating MyTestServiceImpl
2023-01-03 18:17:40.376 INFO 63410 --- [ main] com.example.demo.DemoApplicationTests : Started DemoApplicationTests in 0.598 seconds (JVM running for 1.443)
2023-01-03 18:17:40.711 INFO 63410 --- [ main] com.example.demo.DemoApplicationTests : ### Performing positive test
2023-01-03 18:17:40.725 INFO 63410 --- [ main] com.example.demo.DemoApplicationTests : ### Performing negative test
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.272 s - in com.example.demo.DemoApplicationTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

[...]

You will notice that you are getting the time from the testing version of MyService because the following aspects:

  • There is no date output on the logs.
  • There is a log line saying “com.example.demo.MyTestServiceImpl: Creating MyTestServiceImpl” which is provided by the post-construct method defined into the MyTestServiceImpl class.
  • There is a summary of the tests run with their status.

Conclusions

I hope that this example will be helpful in speeding up the process of setting up a Spring Boot project that follows the TDD approach. By providing a straightforward way to mock application configurations and components without modifying the main software architecture, I hope that this example will make it easier for you to use TDD in your own projects.

Let me know in the comments :)

--

--

Iacopo Palazzi

IoT Engineer working in AWS Professional Services, passionate about Software Development and DevOps.