Native Integration Testing in Spring Boot
Learn everything you need to know to successfully conduct integration testing for applications on the Spring framework.
Join the DZone community and get the full member experience.
Join For FreeOverview
This article focuses on leveraging issues during integration testing of a Spring Boot application and provides examples how to achieve efficient tests, which will prevent your application from possible integration issues in production.
Theory
Before we start, we need to understand what integration testing means. There are tons of explanations on the internet about what integration tests should actually verify.
In my opinion, one of the most accurate definitions is:
"Integration testing is a software testing methodology used to test individual software components or units of code to verify interaction between various software components and detect interface defects." - Techopedia
Cases
Now we need to determine what parts should be covered by integration tests. Going by the definition, it should be units of code in services which interact with external systems.
Let's define the most common interaction units in the diagram below:
Best Practices
Theory is good, but what is theory worth without applying it in practice?
Since our main platform is Spring Boot, let's see what Spring provides for us in order to simplify the testing of integration units split into types from the diagram above.
Here is a common dependency for integration tests:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
1. REST Controller
The most common way of interaction, not requiring an introduction. Go ahead and test it!
Controller to test:
@RestController
public class HelloWorldController {
private final HelloWorldService helloWorldService;
public HelloWorldController(final HelloWorldService helloWorldService) {
this.helloWorldService = helloWorldService;
}
@GetMapping("hello")
public String hello() {
return helloWorldService.hello();
}
}
Service interface:
public interface HelloWorldService {
String hello();
}
Integration test itself:
@WebMvcTest(HelloWorldController.class) //1
@RunWith(SpringRunner.class)
public class HelloWorldControllerTest {
@Autowired //2
private MockMvc mockMvc;
@MockBean //3
private HelloWorldService service;
@Test
public void shouldReturnHello() throws Exception {
when(service.hello()).thenReturn("Hello world"); //4
mockMvc.perform( //5
get("/hello").contentType(MediaType.TEXT_PLAIN))
.andExpect(status().isOk())
.andExpect(content().string("Hello world"));
}
}
Here is the simplest test we can make on our rest integration unit. Let's discuss what we have here:
@WebMvcTest
auto-configures the Spring MVC infrastructure and limits scanned beans to only needed ones, significantly decreasing starting time.The
MockMvc
bean provided by Spring context, needed to verify our calls to the controller.Mockito is well-integrated into Spring, so we don't need to introduce additional configuration where we mock our service, just annotate it with
@MockBean
and get mocking in context for free.Adding expectations as in casual unit tests.
Perform a request and return a type that allows chaining further actions, such as asserting expectations on the result.
Things which should be asserted in such tests: path, method, name and value of parameters, body content type and content itself, response code, redirection rules, etc...
2. External Service Client
There are thousands of ways to communicate with external services, but as an example, we will take the most common one — a REST client. Spring has a built-in client called RestTemplate.
Client component:
@Component
public class ExternalServiceClient {
private final RestTemplate restTemplate;
private final String externalServiceUrl;
public ExternalServiceClient(final RestTemplateBuilder restTemplateBuilder,
@Value("${external.service.url}") final String externalServiceUrl) {
this.restTemplate = restTemplateBuilder.build();
this.externalServiceUrl = externalServiceUrl;
}
public String callExternal() {
return restTemplate.getForObject(externalServiceUrl + "/external", String.class);
}
}
External service client integration test:
@RunWith(SpringRunner.class)
@RestClientTest(ExternalServiceClient.class) //1
@TestPropertySource(properties = "external.service.url=http://external.service.com/") //2
public class ExternalServiceClientTest {
@Autowired
private MockRestServiceServer server; //3
@Autowired
private ExternalServiceClient client;
@Test
public void shouldCallExternal() {
this.server.expect(requestTo("http://external.service.com/external"))
.andRespond(withSuccess("Hello from external Service", MediaType.TEXT_PLAIN)); //4
String externalServiceResponse = client.callExternal();
assertThat(externalServiceResponse).isEqualTo("Hello from external Service");
}
}
@RestClientTest
— an annotation which tells Spring to load needed beans for testing clients. Can only be used on beans that use onlyRestTemplateBuilder
. If you want to useRestTemplate
directly, just add@AutoConfigureWebClient(registerRestTemplate=true)
.@TestPropertySource
— used for adding properties to the test context. In our case, we need to add a URL for the external service.MockRestServiceServer
— a bean which is used for mocking responses ofRestTemplate
.Setting up an expectation for our request.
In external service client tests, we need to assert pretty much the same things as we did in our controller test, just from the client perspective. This example covers only classes which use Spring's RestTemplate
; if you use a different client like Apache HttpClient, OkHttp, Jersey, or something else, you may need to stub your responses with some HTTP server mock like MockServer or WireMock.
3. DAO Layer
To persist state services, you can use various database solutions. The most common ones are relational DB and NoSQL DB. To simplify data manipulation operation, Spring has an awesome Spring Data project, which adds a generalized interface for popular DB clients, allowing swithcing of DB providers without the pain of rewriting of all the calls. For our example, we will use relational DB, and it actually doesn't matter which one service is going to be used in production since we will use Spring Data JPA.
Additional pom dependencies:
<dependency> <!-- spring data -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency> <!-- in-memory database -->
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.197</version>
<scope>test</scope>
</dependency>
Entity:
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@Column
private String name;
public User(final String name) {
this.name = name;
}
...
//getters, setters, equals, hashCode implementations
...
}
JPA repository:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select user from User user where length(user.name) = :length")
Collection<User> findByNameLength(@Param("length") int length);
}
Data JPA test:
@DataJpaTest //1
@RunWith(SpringRunner.class)
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
public void shouldFindByNameLength() {
userRepository.save(new User("Tom")); //2
userRepository.save(new User("Jerry"));
userRepository.save(new User("Joe"));
Collection<User> users = userRepository.findByNameLength(3);
assertThat(users.size()).isEqualTo(2);
}
}
@DataJpaTest
— this annotation disables full auto-configuration and instead applies only configuration relevant to JPA tests. It scans for@Entity
classes and configures Spring Data JPA repositories. Regular@Component
beans are not loaded into the ApplicationContext. This annotation also detects and configures an embedded DB if such is provided in the project dependedcies. In our test, we use H2, but Apache Derby and HSQL also could be used as an embedded DB for integration tests.In our example, we are injecting data via repository, the simplest way of doing it, but for some cases, it's better to inject data directly to the DB without repository bean interaction. In order to do this, you may use the DbUnit library, which has a Spring support module. If you are not okay with DbUnit, take a look on Liquibase, a library not designed for tests, but which can suit your needs for injecting data into the DB before testing.
If a relational DB or Spring data repositories are not your case, don't worry — Spring provides a bunch of other autoconfigure annotations:
@JdbcTest
@JooqTest
@DataMongoTest
@DataNeo4jTest
@DataRedisTest
@DataLdapTest
etc...
DAO layer tests should assert the way your service interacts with the DB, that queries are OK, entities mappings are correct, etc...
4. Message Queue Producers/Consumers
I intentionally highlighted this integration unit since the event-driven programming paradigm is becoming more and more popular among microservices architectures, and it requires a separate paragraph.
Spring has its own adapter for interaction with message brokers called Spring Integration, which also provides testing utils to simplify the testing of integration flows.
Another library is Apache Camel, a framework for message-oriented middleware. Behind all the features Camel provides, it comes with great Spring Boot support both for production and testing aspects.
Bad Practices
Do not mix this with functional/component testing, which includes business logic verification. Integration testing should test only the integration points of your service. If for some reason it cannot be achieved, you should consider rewriting the logic; otherwise, functional/component tests should be added instead.
Avoid using the @SpringBootTest
annotation, which will scan for @SpringBootApplication
and load the whole service in order to test just a small piece of your application. It increases test execution time significantly. Consider using one of the provided annotations from the examples or using @ContextConfiguration
to define only the needed slice of your application. Although Spring can cache test context along test suites, you should remember that it caches only identical ones, and @MockBean
, for example, will force SpringRunner to create a new context with the mocked bean instead of an already loaded context. Another drawback is a case when integration testing fails and fixing it may require a lot of test runs — it can be a pain if the app contains a lot of beans and logic which should be loaded despite only a small slice of the app being needed.
Location
Unlike functional/component tests, integration tests can go along with unit tests, if they are written well — execution time should not be that high to divide them from unit tests.
Opinions expressed by DZone contributors are their own.
Comments