© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
K. S. P. Reddy, S. UpadhyayulaBeginning Spring Boot 3https://doi.org/10.1007/978-1-4842-8792-7_14

14. Testing Spring Boot Applications

Siva Prasad Reddy Katamreddy1   and Sai Subramanyam Upadhyayula2
(1)
Hyderabad, India
(2)
Rotterdam, The Netherlands
 

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-1

Typical 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-2

Typical 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-3

Spring 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-4

Testing 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-5

EmployeeRepository.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-6

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

MockEmployeeRepository.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-8

JdbcEmployeeRepository.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-9

Testing 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-10

UsersImporter.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-12

Testing 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-13

Testing 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 java.util.Arrays;
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-14

Testing 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-15

Testing 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-16

Testing 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-19

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

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.130.201