Testing
is an essential part of software development. It helps developers verify the correctness of the functionality. JUnit and TestNG are two of the most popular testing libraries used in Java projects.
Test-driven development (TDD)
is a popular development practice where you write tests first and write just enough production code to pass the tests. You write various tests, such as unit, integration, and performance tests. Unit tests
focus on testing one component in isolation, whereas integration tests verify a feature's behavior, which can involve multiple components. While doing unit tests, you may have to mock the behavior of dependent components such as third-party web service classes and database method invocations. There are mocking libraries
, like Mockito
, PowerMock
, and jMock
, for mocking the object’s behavior.
The dependency injection (DI) design pattern
encourages programming to practice and write testable code. With dependency injection, you can inject mock implementations for testing and actual implementations for production. Spring
is a dependency injection container at its core, providing excellent support for testing various parts of an application.
This chapter will teach you how to test Spring components in Spring Boot applications. You will take a detailed look at how to test slices of applications, such as web components
(regular MVC controllers, REST API endpoints), Spring data repositories, and secured controller/service methods using the @WebMvcTest, @DataJpaTest, and the @JdbcTest annotations.
Testing Spring Boot Applications
One of the key reasons for the popularity of the Spring Framework
is its excellent support for testing. Spring provides
SpringExtension
, a custom JUnit extension helping to load the Spring ApplicationContext by using @ContextConfiguration(classes=AppConfig.class).
A typical
Spring unit/integration test
is shown in Listing
14-1.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes=AppConfig.class)
public class PostServiceTests
{
@Autowired
PostService userService;
@Test
public void should_load_all_posts()
{
List<PostDto> posts = postService.getAllPosts();
assertNotNull(posts);
assertEquals(10, posts.size());
}
}
Listing 14-1Typical Spring JUnit Test
A Spring Boot application is also nothing but a Spring application so you can use all of Spring’s testing features in your Spring Boot applications.
However, some
Spring Boot features
, like loading external properties and logging, are available only if you create
ApplicationContext using the
SpringApplication class, which you’ll typically use in your entry point class. These additional Spring Boot features won’t be available if you use
@ContextConfiguration.
@SpringBootApplication
public class SpringbootTestingDemoApplication
{
public static void main(String[] args)
{
SpringApplication.run(SpringbootTestingDemoApplication.class, args);
}
}
Spring Boot provides the
@SpringBootTest annotation
, which uses
SpringApplication behind the scenes to load
ApplicationContext so that all the Spring Boot features will be available. See Listing
14-2.
@SpringBootTest
public class SpringbootTestingDemoApplicationTests
{
@Autowired
PostService postService;
@Test
public void should_load_all_posts()
{
...
...
}
}
Listing 14-2Typical Spring Boot JUnit Test
For @SpringBootTest
, you can pass Spring configuration classes, Spring bean definition XML files, and more, but in Spring Boot applications, you typically use the entry point class.
The
Spring Boot Test Starter
spring-boot-starter-test pulls in the JUnit, Spring Test, and Spring Boot Test modules, along with the following most commonly used
mocking and asserting libraries
:
Now you’ll see how to create a simple Spring Boot web application with a simple REST
endpoint
.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
First, create an entry point class called
Application.java
, as follows:
@SpringBootApplication
public class Application
{
public static void main(String[] args)
{
SpringApplication.run(Application.class, args);
}
}
Listing
14-3 shows how to create a simple
REST endpoint
called
/ping.
@RestController
public class PingController
{
@GetMapping("/ping")
public String ping()
{
return "OK";
}
}
Listing 14-3Spring REST Controller
If you run the application, you can invoke the
REST endpoint
http://localhost:8080/ping, which gives the response
"OK". You can write a test for the
/ping endpoint. See Listing
14-4.
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
class PingControllerTests {
@Autowired
TestRestTemplate restTemplate;
@Test
void testPing() {
ResponseEntity<String> respEntity =
restTemplate.getForEntity("/ping", String.class);
assertThat(respEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(respEntity.getBody()).isEqualTo("OK");
}
}
Listing 14-4Testing Spring REST Endpoint
Using TestRestTemplate
Since you need to test the REST endpoint
, you start the embedded servlet container by specifying the webEnvironment attribute of
@SpringBootTest
.
The default webEnvironment value is
WebEnvironment.MOCK
, which doesn’t start an embedded servlet container.
You can use various
webEnvironment values
based on how you want to run the tests.
MOCK (default): Loads a WebApplicationContext and provides a mock servlet environment. It will not start an embedded servlet container. If servlet APIs are not on your classpath, this mode will fall back to creating a regular non-web ApplicationContext.
RANDOM_PORT: Loads a ServletWebServerApplicationContext and starts an embedded servlet container listening on a random available port
DEFINED_PORT: Loads a ServletWebServerApplicationContext and starts an embedded servlet container listening on a defined port (server.port)
NONE: Loads an ApplicationContext using SpringApplication but does not provide a servlet environment
The
TestRestTemplate
bean will be registered automatically only when @SpringBootTest is started with an embedded servlet container.
While running the integration tests that start the embedded servlet containers, it is better to use
WebEnvironment.RANDOM_PORT
so that it won’t conflict with other running applications, especially in continuous integration (CI) environments where multiple builds run in parallel.
You can specify which configuration classes to use to build ApplicationContext by using the classes attribute of the @SpringBootTest annotation. If you don’t determine any classes, it will automatically search for nested @Configuration classes and fall back to searching for @SpringBootConfiguration classes. The @SpringBootApplication is annotated with @SpringBootConfiguration so that @SpringBootTest will pick up the application’s entry point class.
Testing with Mock Implementations
While performing unit testing
, you may want to mock calls to external services like database interactions and web service invocations. You can create mock implementations for tests and actual implementations used in production.
Say you have an
EmployeeRepository file that talks to the database and gets employee data, as shown in Listing
14-5.
public interface EmployeeRepository
{
List<Employee> findAllEmployees();
}
Listing 14-5EmployeeRepository.java
Suppose you have
EmployeeService, which depends on
EmployeeRepository, with
getMaxSalariedEmployee() and a few other employee-related methods. See Listing
14-6.
@Service
public class EmployeeService
{
private EmployeeRepository employeeRepository;
public EmployeeService(EmployeeRepository employeeRepository)
{
this.employeeRepository = employeeRepository;
}
public Employee getMaxSalariedEmployee()
{
Employee emp = null;
List<Employee> emps = employeeRepository.findAllEmployees();
//loop through emps and find max salaried emp
return emp;
}
}
Listing 14-6EmployeeService.java
Now you can create a mock
EmployeeRepository file for testing, as shown in Listing
14-7.
@Repository
@Profile("test")
public class MockEmployeeRepository implements EmployeeRepository
{
public List<Employee> findAllEmployees()
{
return Arrays.asList(
new Employee(1, "A", 50000),
new Employee(2, "B", 75000),
new Employee(3, "C", 43000)
};
}
}
Listing 14-7MockEmployeeRepository.java
Now you’ll create a real implementation of
EmployeeRepository for production, as shown in Listing
14-8.
@Service
@Profile("production")
public class JdbcEmployeeRepository implements EmployeeRepository
{
private JdbcTemplate jdbcTemplate;
public JdbcEmployeeRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<Employee> findAllEmployees()
{
return jdbcTemplate.query(...);
}
}
Listing 14-8JdbcEmployeeRepository.java
You can use the
@ActiveProfiles annotation to specify which profiles to use so that Spring Boot will activate only the beans associated with those profiles. See Listing
14-9.
@ActiveProfiles("test")
@SpringBootTest
public class ApplicationTests
{
@Autowired
EmployeeService employeeService;
@Test
public void test_getMaxSalariedEmployee()
{
Employee emp = employeeService.getMaxSalariedEmployee();
assertNotNull(emp);
assertEquals(2, emp.getId());
assertEquals("B", emp.getName());
assertEquals(75000, emp.getSalary());
}
}
Listing 14-9Testing with Mock Implementation Using Profiles
Since you have enabled the
test profile,
MockEmployeeRepository will be injected into
EmployeeService. You can activate the
production profile while running the application in production as follows:
java -jar myapp.jar -Dspring.profiles.active=production
While running the main application, Spring Boot will use the production profile and JdbcEmployeeRepository will be injected into EmployeeService.
In addition, providing mock implementations for every use case can be tedious. You can use mocking libraries to create mock objects without actually creating classes. The next section looks into how to unit test using the popular mocking library Mockito.
Testing with Mockito
Mockito
is a popular Java mocking framework that can be used along with JUnit. Mockito lets you write tests by mocking the external dependencies with the desired behavior.
For example, assume you are invoking some external web service and want to retry the invocation three times when it fails due to communication failures. To test the retry behavior, that external web service should throw an exception that may not be in your control. You can use Mockito
to simulate this behavior to test the retry functionality.
Suppose you import user data from a third party using a web service, as shown in Listing
14-10.
@Service
public class UsersImporter
{
public List<User> importUsers() throws UserImportServiceCommunicationFailure
{
List<User> users = new ArrayList<>();
//get users by invoking some web service
//if any exception occurs throw UserImportServiceCommunicationFailure
//dummy data
users.add(new User());
users.add(new User());
users.add(new User());
return users;
}
}
Listing 14-10UsersImporter.java
UserService uses
UsersImporter to get user data and retries three times if a
UserImportServiceCommunicationFailure occurs. See Listing
14-11.
@Service
@Transactional
public class UsersImportService
{
private Logger logger = LoggerFactory.getLogger(UserService.class);
private UsersImporter usersImporter;
public UsersImportService(UsersImporter usersImporter)
{
this.usersImporter = usersImporter;
}
public UsersImportResponse importUsers()
{
int retryCount = 0;
int maxRetryCount = 3;
for (int i = 0; i < maxRetryCount; i++)
{
try
{
List<User> importUsers = usersImporter.importUsers();
logger.info("Import Users: "+importUsers);
break;
} catch (UserImportServiceCommunicationFailure e)
{
retryCount++;
logger.error("Error: "+e.getMessage());
}
}
if(retryCount >= maxRetryCount)
return new UsersImportResponse(retryCount, "FAILURE");
else
return new UsersImportResponse(0, "SUCCESS");
}
}
public class UsersImportResponse
{
private int retryCount;
private String status;
//setters & getters
}
Listing 14-11
UsersImportService.java
This code invokes the usersImporter.importUsers() method
and, if it throws UserImportServiceCommunicationFailure, it retries three times.
If you want to test if usersImporter.importUsers() returns the result without getting an exception, then UsersImportResponse(0, "SUCCESS") should be returned; otherwise, UsersImportResponse(3, "FAILURE") should be returned.
You can use
@Mock to create a mock object and
@InjectMocks to inject the dependencies with mocks. You can use
@ExtendWith(MockitoExtension.class) to initialize the mock objects or trigger the mock object initialization using
MockitoAnnotations.initMocks(this) in the JUnit
@Before method. See Listing
14-12.
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import com.apress.demo.model.UsersImportResponse;
@ExtendWith(MockitoExtension.class)
class UsersImportServiceMockitoTest {
@Mock
private UsersImporter usersImporter;
@InjectMocks
private UsersImportService usersImportService;
@Test
void should_import_users() {
UsersImportResponse response = usersImportService.importUsers();
assertThat(response.getRetryCount()).isEqualTo(0);
assertThat(response.getStatus()).isEqualTo("SUCCESS");
}
@Test
void should_retry_3times_when_UserImportServiceCommunicationFailure_occured() {
given(usersImporter.importUsers()).willThrow(new UserImportServiceCommunicationFailure());
UsersImportResponse response = usersImportService.importUsers();
assertThat(response.getRetryCount()).isEqualTo(3);
assertThat(response.getStatus()).isEqualTo("FAILURE");
}
}
Listing 14-12Testing Using Mockito Mock Objects
Here you are simulating the failure condition while importing users using the web service as follows:
given(usersImporter.importUsers()).willThrow(new UserImportServiceCommunicationFailure());
So, when you call
userService.importUsers()
and the mock usersImporter object throws UserImportServiceCommunicationFailure, it will retry three times. Similarly, you can use Mockito
to simulate any behavior to meet these test cases.
Spring Boot provides the
@MockBean annotation
that can be used to define a new Mockito mock bean or replace a Spring bean with a mock bean and inject that into their dependent beans. Mock beans will be automatically reset after each test method. See Listing
14-13.
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import com.apress.demo.exceptions.UserImportServiceCommunicationFailure;
import com.apress.demo.model.UsersImportResponse;
@ExtendWith(MockitoExtension.class)
class UsersImportServiceMockitoTest {
@MockBean
private UsersImporter usersImporter;
@Autowired
private UsersImportService usersImportService;
@Test
void should_import_users() {
UsersImportResponse response = usersImportService.importUsers();
assertThat(response.getRetryCount()).isEqualTo(0);
assertThat(response.getStatus()).isEqualTo("SUCCESS");
}
@Test
void should_retry_3times_when_UserImportServiceCommunicationFailure_occured() {
given(usersImporter.importUsers()).willThrow(new UserImportServiceCommunicationFailure());
UsersImportResponse response = usersImportService.importUsers();
assertThat(response.getRetryCount()).isEqualTo(3);
assertThat(response.getStatus()).isEqualTo("FAILURE");
}
}
Listing 14-13Testing Using Spring Boot’s @MockBean Mocks
Spring Boot will create a Mockito mock
object for UsersImporter and inject it into the UsersImportService bean.
Testing Slices of Application Using @*Test Annotations
While testing various application components, you may want to load a subset of the Spring ApplicationContext beans related to the subject under test (SUT)
. For example, when testing a SpringMVC controller, you may want to load only the MVC layer components and provide mock service-layer beans as dependencies.
Spring Boot provides annotations
like @WebMvcTest, @DataJpaTest, @DataMongoTest, @JdbcTest, and @JsonTest to test slices of the application.
Testing SpringMVC Controllers Using @WebMvcTest
Spring Boot provides the
@WebMvcTest annotation
, which will autoconfigure SpringMVC infrastructure components and load only @Controller, @ControllerAdvice, @JsonComponent, Filter, WebMvcConfigurer, and HandlerMethodArgumentResolver components. Other Spring beans (annotated with @Component, @Service, @Repository, etc.) will not be scanned using this annotation.
Let’s return to your Spring blog application and see how to write tests for your Spring MVC controllers. Listing
14-14 shows how to write a test for the
PostController class that uses Spring MVC and Thymeleaf using the
@WebMvcTest.
package com.apress.demo.springblog;
import com.apress.demo.springblog.domain.Post;
import com.apress.demo.springblog.service.PostService;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.hamcrest.Matchers.hasSize;
@WebMvcTest(controllers = PostControllerTest.class)
class PostControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private PostService postService;
@Test
public void testFindAllPosts() throws Exception {
Post post = new Post();
post.setId(1);
post.setTitle("Test");
post.setDescription("Test");
Post secondPost = new Post();
secondPost.setId(2);
secondPost.setTitle("Test 1");
secondPost.setDescription("Test 1");
BDDMockito.given(postService.findAllPosts()).willReturn(Arrays.asList(post, secondPost));
this.mvc.perform(MockMvcRequestBuilders.get("/posts")
.accept(MediaType.TEXT_HTML))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.view().name("post"))
.andExpect(MockMvcResultMatchers.model().attribute("posts", hasSize(2)));
}
}
Listing 14-14Testing SpringMVC Controller Using MockMvc
In this test, you annotate the test with @WebMvcTest(controllers = PostController.class) by explicitly specifying which controller you are testing. As @WebMvcTest doesn’t load other regular Spring beans and PostController depends on PostService, you provided a mock bean using the @MockBean annotation. The @WebMvcTest autoconfigures MockMvc, which helps to test controllers without starting an actual servlet container.
In this test method, you set the expected behavior on postService.findAllPosts() to return a list of two Post objects. Then you issue a GET request to the "/posts" endpoint and assert various things in response. The first assertion is to check whether the HTTP Response's status is OK.
In the following assertion, you check if the name of the view returned is “post”, and in the last assertion, you verify if the post Model Attribute has a size of 2, representing the number of objects you expect as the response.
Testing SpringMVC REST Controllers Using @WebMvcTest
Similar to how you can test SpringMVC controllers
, you can also test REST controllers. You can write assertions on response data using JsonPath or JSONAssert libraries.
Go ahead and write a test for your
PostController that serves the REST API, as shown in Listing
14-15.
package com.apress.demo.springblog;
import com.apress.demo.springblog.controller.PostController;
import com.apress.demo.springblog.dto.PostDto;
import com.apress.demo.springblog.service.PostService;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@WebMvcTest(controllers = PostController.class)
class PostRestControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private PostService postService;
@Test
public void testFindPostBySlug() throws Exception {
PostDto post = new PostDto();
post.setId(1L);
post.setTitle("Test");
post.setDescription("Test");
post.setSlug("Test");
BDDMockito.given(postService.findBySlug("Test")).willReturn(post);
this.mvc.perform(MockMvcRequestBuilders.get("/api/posts/Test")
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.is(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.title", Matchers.is("Test")))
.andExpect(MockMvcResultMatchers.jsonPath("$.description", Matchers.is("Test")));
}
}
Listing 14-15Testing SpringMVC REST Controller Using MockMvc
You test the PostController REST API endpoint "/api/posts/{slug}" in the same way you tested the findAllPosts() endpoint in PostController from Spring MVC.
The main difference is in the assertions. In this test class, you use the JSON Path assertions using the MockMvcResultMatchers.jsonPath method to verify the returned JSON response data.
Testing Persistence Layer Components Using @DataJpaTest and @JdbcTest
You might want to test the persistence layer components
of your application, which doesn’t require loading many components like controllers, security configuration, and so on. Spring Boot provides the @DataJpaTest and @JdbcTest annotations to test the Spring beans, which talk to relational databases.
Spring Boot provides the @DataJpaTest annotation
to test the persistence layer components that will autoconfigure in-memory embedded databases and scan for @Entity classes and Spring Data JPA repositories. The @DataJpaTest annotation doesn’t load other Spring beans (@Components, @Controller, @Service, and annotated beans) into ApplicationContext.
Now you’ll see how to test the Spring Data JPA repositories in your Spring blog application. Let’s create a test for the
PostRepository.java class using the
@DataJpaTest annotation
. See Listing
14-16.
package com.apress.demo.springblog;
import com.apress.demo.springblog.domain.Post;
import com.apress.demo.springblog.repository.PostRepository;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.TestInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.Optional;
@DataJpaTest
public class PostRepositoryTest {
@Autowired
private PostRepository postRepository;
@BeforeEach
public void setup() {
Post post = new Post();
post.setId(1L);
post.setTitle("Test");
post.setDescription("Test");
post.setSlug("test");
postRepository.save(post);
}
@Test
public void testPostBySlug() {
Optional<Post> postOptional = postRepository.findBySlug("test");
Assertions.assertTrue(postOptional.isPresent());
Assertions.assertEquals(1L, postOptional.get().getId());
Assertions.assertEquals("Test", postOptional.get().getTitle());
}
}
Listing 14-16Testing Spring Data JPA Repositories Using @DataJpaTest
When you run PostRepositoryTest, Spring Boot will autoconfigure the H2 in-memory embedded database (as you have the H2 database driver in the classpath) and run the tests.
You initialize the test data inside the setup() method of the Test class, which will be executed before each test.
If you want to run the tests against the actual registered database, you can annotate the test with @AutoConfigureTestDatabase(replace=Replace.NONE), which will use the registered DataSource instead of an in-memory data source. You can use Replace.AUTO_CONFIGURED to replace autoconfigured DataSource and use Replace.ANY (the default) to replace any datasource bean that’s autoconfigured or explicitly defined.
The
@DataJpaTest tests
are transactional and rolled back at the end of each test by default. You can disable this default rollback behavior for a single test or for an entire test class by annotating with
@Transactional(propagation = Propagation.NOT_SUPPORTED). See Listing
14-17.
@DataJpaTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class PostRepositoryTest {
@Autowired
private PostRepository postRepository;
@BeforeAll
public void setup() {
Post post = new Post();
post.setId(1L);
post.setTitle("Test");
post.setDescription("Test");
post.setSlug("test");
postRepository.save(post);
}
@Test
public void testPostBySlug() {
Optional<Post> postOptional = postRepository.findBySlug("test");
Assertions.assertTrue(postOptional.isPresent());
Assertions.assertEquals(1L, postOptional.get().getId());
Assertions.assertEquals("Test", postOptional.get().getTitle());
}
@Test
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void testCreatePost() {
Post post = new Post();
post.setId(2L);
post.setTitle("Test 1");
post.setDescription("Test 1");
post.setSlug("test 1");
post.setComments(null);
postRepository.save(post);
Optional<Post> byTest1Slug = postRepository.findBySlug("test 1");
Assertions.assertTrue(byTest1Slug.isPresent());
}
@Test
public void testUpdatePost() {
Post post = new Post();
post.setId(1L);
post.setTitle("Test Updated");
post.setDescription("Test Updated");
post.setSlug("test updated");
post.setComments(null);
postRepository.save(post);
Optional<Post> byTest1Slug = postRepository.findBySlug("test updated");
Assertions.assertTrue(byTest1Slug.isPresent());
}
Listing 14-17@DataJpaTest with Custom Transactional Behavior
When the testCreatePost() test method runs, the changes will not be rolled back, whereas the database changes made in testUpdateUser() will be automatically rolled back. In the above test, the @TestInstance(TestInstance.Lifecycle.PER_CLASS) ensures that the test instance is created only once per Test class, whereas by default the test instance will be created for each Test method in Junit Jupiter. This will lead to state sharing between the tests, which is bad and makes your tests flaky and unreliable, so use this option very rarely.
You may also observe that the above test fails when you add the Propogation.NOT_SUPPORTED option when the orphanRemoval = true attribute is added to the comments field inside the Post class. Just remove this attribute to make the test work normally.
The
@DataJpaTest annotation
also autoconfigures
TestEntityManager, which is an alternative to the JPA
EntityManager to be used in JPA tests. See Listing
14-18.
@DataJpaTest
public class PostRepositoryTestUsingTestEntityManager {
@Autowired
private PostRepository postRepository;
@Autowired
private TestEntityManager entityManager;
@Test
public void testPostBySlug() {
Post post = new Post();
post.setTitle("Test");
post.setDescription("Test");
post.setSlug("test");
post.setComments(null);
postRepository.save(post);
Long id = entityManager.persistAndGetId(post, Long.class);
Optional<Post> postOptional = postRepository.findById(id);
Assertions.assertTrue(postOptional.isPresent());
Assertions.assertEquals(1L, postOptional.get().getId());
Assertions.assertEquals("Test", postOptional.get().getTitle());
}
}
Listing 14-18
@DataJpaTest
Using TestEntityManager
The
TestEntityManager
provides some convenient methods like persistAndGetId(), persistAndFlush(), and persistFlushFind(), which are useful in tests.
Similar to the @DataJpaTest annotation
, you can use @JdbcTest as discussed in Chapter 5 to test plain JDBC-related methods using JdbcTemplate. The @JdbcTest annotation
also autoconfigures in-memory embedded databases and runs the tests in a transactional manner.
Like @DataJpaTest and @JdbcTest, Spring Boot provides other annotations like @DataMongoTest, @DataNeo4jTest, @JooqTest, @JsonTest, and @DataLdapTest to test slices of an application.
Testing Persistence Layer Using TestContainers
In the previous section, you learned how to test your persistence layer by using an in-memory database like H2. Ideally, you want to test your persistence logic against a real database like MySQL, PostgreSQL, or any other relational database of your choice.
But the challenge in accomplishing this is the availability of the database while running the tests. In a real-world project, your test suite will run on a CI server like Jenkins where it’s not so easy to install a real database each time you run your tests.
To address this challenge, you can use a Java library called TestContainers
. According to the TestContainers website, “Testcontainers
is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in Docker container.” See
www.testcontainers.org/
.
To work with TestContainers
, you need to have Docker installed on your machine as a prerequisite.
After that, add the following dependency to the
pom.xml file:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.17.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.17.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.17.2</version>
<scope>test</scope>
</dependency>
Please check the latest version available on Maven Central.
To avoid adding the same version number for multiple test container dependencies, you can add the Bill of Materials or a BOM dependency to the dependencyManagement section of the pom.xml file.
Now you can add the dependencies without specifying the version, like so:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>
After adding these dependencies, you can add the
@TestContainers annotation on top of the
UserRepositoryTest class to enable the support for
TestContainers
in your test. This annotation is coming from the junit-jupiter test containers dependency. Add the following code to the test class to initialize the MySQL docker container during the test start-up:
@Container
MySQLContainer mySQLContainer = new MySQLContainer("mysql:8.0.29")
.withDatabaseName("test-db")
.withUsername("testuser")
.withPassword("pass");
Listing
14-19 shows the test with the TestContainers
support
.
package com.apress.demo.springblog;
import com.apress.demo.springblog.domain.Post;
import com.apress.demo.springblog.repository.PostRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.util.Optional;
@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class PostRepositoryTCTest {
@Container
static MySQLContainer mySQLContainer = new MySQLContainer("mysql:8.0.29");
@Autowired
private PostRepository postRepository;
@DynamicPropertySource
static void overrideProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mySQLContainer::getJdbcUrl);
registry.add("spring.datasource.username", mySQLContainer::getUsername);
registry.add("spring.datasource.password", mySQLContainer::getPassword);
}
@Test
public void testPostBySlug() {
setup();
Optional<Post> postOptional = postRepository.findBySlug("test");
Assertions.assertTrue(postOptional.isPresent());
Assertions.assertEquals(1L, postOptional.get().getId());
Assertions.assertEquals("Test", postOptional.get().getTitle());
}
private void setup() {
Post post = new Post();
post.setId(1L);
post.setTitle("Test");
post.setDescription("Test");
post.setSlug("test");
postRepository.save(post);
}
}
Listing 14-19DataJpaTest using TestContainers Support
When you add the @
TestContainers
and @Container annotations, TestContainers will automatically start and stop the required containers during the startup and teardown phase of the tests.
The datasource properties like spring.datasource.url, spring.datasource.username and spring.datasource.password are passed during the initialization of the test dynamically by using the @DynamicPropertySource annotation.
You can also do the initialization and destruction of the containers manually by initializing the containers without the
@Container annotation, and then calling the
container.start() and
container.stop() methods manually. Here is an example of how to start and stop the containers manually:
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class PostRepositoryTCManualStartupTest {
static MySQLContainer<?> mysqlContainer =
new MySQLContainer<>(DockerImageName.parse("mysql:8.0.29"));
@Autowired
private PostRepository postRepository;
@BeforeAll
static void beforeAll() {
mysqlContainer.start();
}
@AfterAll
static void afterAll() {
mysqlContainer.stop();
}
@DynamicPropertySource
static void overrideProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysqlContainer::getJdbcUrl);
registry.add("spring.datasource.username", mysqlContainer::getUsername);
registry.add("spring.datasource.password", mysqlContainer::getPassword);
}
@Test
public void testPostBySlug() {
setup();
Optional<Post> postOptional = postRepository.findBySlug("test");
Assertions.assertTrue(postOptional.isPresent());
Assertions.assertEquals(1L, postOptional.get().getId());
Assertions.assertEquals("Test", postOptional.get().getTitle());
}
private void setup() {
Post post = new Post();
post.setId(1L);
post.setTitle("Test");
post.setDescription("Test");
post.setSlug("test");
postRepository.save(post);
}
}
Reusing the Containers
If you have multiple tests in your test suite, which are dependent on an external system like a database, your tests will start and stop the containers multiple times, leading to higher test execution time. To get over this issue, TestContainers
allows you to reuse the existing containers, which helps you with shorter test execution times.
To enable the reuse of containers, follow these steps:
Add the .withReuse(true) method while initializing the container.
Add the property testcontainers.reuse.enable=true to the testcontainer.properties file. You must create the file under the home directory of your system.
Singleton Container Pattern
The
Singleton Container Pattern
provides another way to reuse the containers. You can manually start a container once before the test execution and reuse the same container for subsequent test classes.
public abstract class BaseTest {
static final MySQLContainer<?> MY_SQL_CONTAINER;
static {
MY_SQL_CONTAINER = new MySQLContainer<>(DockerImageName.parse("mysql:8.0.29"));
MY_SQL_CONTAINER.start();
}
@DynamicPropertySource
static void overrideProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", MY_SQL_CONTAINER::getJdbcUrl);
registry.add("spring.datasource.username", MY_SQL_CONTAINER::getUsername);
registry.add("spring.datasource.password", MY_SQL_CONTAINER::getPassword);
}
}
Here you created an abstract class called
BaseTest
, which contains the logic to initialize the container inside a static initializer block manually. The container will be reused for all the tests which inherit the BaseTest class.
Summary
In this chapter, you learned various techniques to test Spring Boot applications. You looked at testing controllers, REST API endpoints, and service-layer methods. Using the Spring Security test module, you also learned how to test secured methods and REST endpoints. You learned how to test the persistence layer using an actual database with the help of the TestContainers library. In the next chapter, you will look at how to create your own Spring Boot Starter.