2
头图

[Note] This article is translated from: Testing with Spring Boot and @SpringBootTest-Reflectoring

Using the @SpringBootTest annotation, Spring Boot provides a convenient way to start the application context to be used in the test. In this tutorial, we will discuss when to use @SpringBootTest and when it is better to use other tools for testing. We will also look at different ways to customize the application context and how to reduce test run time.

 Code example

This article attached on GitHub working code examples.

"Testing with Spring Boot" series

This tutorial is part of a series:

  1. Use Spring Boot for unit testing
  2. Use Spring Boot and @WebMvcTest to test MVC Web Controller
  3. Use Spring Boot and @DataJpaTest to test JPA query
  4. Use Spring Boot and @SpringBootTest for testing

Integration test and unit test

Before starting to use Spring Boot for integration testing, let us define the difference between integration testing and unit testing.
Unit testing covers a single "unit", where a unit is usually a single class, but it can also be a cluster within a group of combined tests.
The integration test can be any of the following:

  • Covers multiple "units" of testing. It tests the interaction between two or more intra-cluster clusters.
  • Test covering multiple layers. This is actually a specialization of the first case, for example, it may cover the interaction between the business service and the persistence layer.
  • Tests covering the entire application path. In these tests, we sent a request to the application and checked whether it responded correctly and changed the database state according to our expectations.

Spring Boot provides the @SpringBootTest annotation, we can use it to create an application context, which contains all the objects we need for all the above test types. But please note that excessive use of @SpringBootTest may cause the test suite to run very long .
Therefore, for a simple test covering multiple units, we should create a simple test, which unit test . In the unit test, we manually create the object graph required for the test and simulate the rest. In this way, Spring will not start the entire application context at the beginning of each test.

Test slice

We can test our Spring Boot application as a whole, unit by unit, or layer by layer. Using Spring Boot's test slice annotation , we can test each layer separately.
Before we study the @SpringBootTest annotations in detail, let's explore the test slice annotations to check @SpringBootTest is really what you want.
@SpringBootTest annotation loads the complete Spring application context. In contrast, test slice annotations only load the beans needed to test a specific layer. Because of this, we can avoid unnecessary simulations and side effects.

@WebMvcTest

Our web controller takes on many responsibilities, such as listening for HTTP requests, validating input, invoking business logic, serializing output, and converting exceptions into correct responses. We should write tests to verify all these functions.
@WebMvcTest test slice annotation will use just enough components and configurations to set up our application context to test our web controller layer. For example, it will set our @Controller , @ControllerAdvice , a MockMvc bean and some other automatically configure .
To read more about @WebMvcTest more information and to learn how we verify each role, please read my About article uses Spring Boot and @WebMvcTest test MVC Web controller .

@WebFluxTest

@WebFluxTest used to test the WebFlux controller. @WebFluxTest works similarly to the @WebMvcTest annotation, the difference is that it is not a Web MVC component and configuration, but a WebFlux component and configuration. One of the beans is WebTestClient, which we can use to test our WebFlux endpoint.

@DataJpaTest

Just like @WebMvcTest allows us to test our web layer, @DataJpaTest used to test the persistence layer.
It configures our entities, repositories and sets up embedded databases. Now, all this is fine, but what does it mean to test our persistence layer? What are we testing? If query, then what kind of query? To find out the answers to all these questions, read my article on the use of Spring Boot and @DataJpaTest test JPA query .

@DataJdbcTest

Spring Data JDBC is another member of the Spring Data family. If we are using this project and want to test the persistence layer, then we can use the @DataJdbcTest annotation. @DataJdbcTest will automatically configure the embedded test database and JDBC repository defined in our project for us.
Another similar project is Spring JDBC, which provides us with JdbcTemplate objects to perform direct queries. @JdbcTest DataSource object needed to test our JDBC query. rely
The code examples in this article only need to rely on Spring Boot's test starter and JUnit Jupiter:

dependencies {
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.junit.jupiter:junit-jupiter:5.4.0')
}

Use @SpringBootTest to create ApplicationContext

@SpringBootTest starts to search in the current package of the test class by default, and then searches upward in the package structure, looking for @SpringBootConfiguration , and then reads the configuration from it to create the application context. This class is usually our main application class, because the @SpringBootApplication annotation includes the @SpringBootConfiguration annotation. Then, it creates an application context that is very similar to the application context launched in the production environment.
We can customize this application context in many different ways, as described in the next section.
Because we have a complete application context, including web controllers, Spring data repositories, and data sources, @SpringBootTest is very convenient for integration testing throughout all layers of the application:

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {

  @Autowired
  private MockMvc mockMvc;

  @Autowired
  private ObjectMapper objectMapper;

  @Autowired
  private UserRepository userRepository;

  @Test
  void registrationWorksThroughAllLayers() throws Exception {
    UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");

    mockMvc.perform(post("/forums/{forumId}/register", 42L)
            .contentType("application/json")
            .param("sendWelcomeMail", "true")
            .content(objectMapper.writeValueAsString(user)))
            .andExpect(status().isOk());

    UserEntity userEntity = userRepository.findByName("Zaphod");
    assertThat(userEntity.getEmail()).isEqualTo("zaphod@galaxy.net");
  }
}
@ExtendWith
The code examples in this tutorial use the @ExtendWith annotation to tell JUnit 5 to enable Spring support. Starting from Spring Boot 2.1 , we no longer need to load SpringExtension because it is included as a meta-annotation in Spring Boot test annotations, such as @DataJpaTest , @WebMvcTest and @SpringBootTest .

Here, we additionally use @AutoConfigureMockMvc add the MockMvc instance to the application context.
We use this MockMvc object to perform a POST request to our application and verify that it responds as expected.
Then, we use UserRepository in the application context to verify whether the request caused the expected change in the database state.

Custom application context

There are many ways to customize the application context created by @SpringBootTest Let us see what options we have.

Precautions when customizing the application context
Each customization of the application context is another thing that makes it different from the "real" application context launched in a production setting. Therefore, in order to make our tests as close to production as possible, we should only customize what is really needed to make the tests run!

Add automatic configuration

In the above, we have seen the role of automatic configuration:

@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {
  ...
}

There are many other auto-configuration , each of which can add other beans to the application context. Here are some other useful contents in the document:

  • @AutoConfigureWebTestClient : Add WebTestClient to the test application context. It allows us to test server endpoints.
  • @AutoConfigureTestDatabase : This allows us to run tests against real databases instead of embedded databases.
  • @RestClientTest : It will come in handy when we want to test our RestTemplate It automatically configures the required components and a MockRestServiceServer object, which helps us simulate the response to the request RestTemplate
  • @JsonTest : Automatically configure JSON mappers and classes, such as JacksonTester or GsonTester . Using these we can verify whether our JSON serialization/deserialization is working properly.

Set custom configuration properties

Usually, some configuration properties need to be set to different values from the values in the production settings during testing:

@SpringBootTest(properties = "foo=bar")
class SpringBootPropertiesTest {

  @Value("${foo}")
  String foo;

  @Test
  void test(){
    assertThat(foo).isEqualTo("bar");
  }
}

If the attribute foo present in the default setting, it will be overwritten bar

Use @ActiveProfiles to externalize properties

If many of our tests require the same set of attributes, we can create a configuration file application-<profile>.propertie or application-<profile>.yml and load the attributes from that file by activating a certain configuration file:

# application-test.yml
foo: bar
@SpringBootTest
@ActiveProfiles("test")
class SpringBootProfileTest {

  @Value("${foo}")
  String foo;

  @Test
  void test(){
    assertThat(foo).isEqualTo("bar");
  }
}

Use @TestPropertySource to set custom properties

Another way to customize the entire set of attributes is to use the @TestPropertySource annotation:

# src/test/resources/foo.properties
foo=bar
@SpringBootTest
@TestPropertySource(locations = "/foo.properties")
class SpringBootPropertySourceTest {

  @Value("${foo}")
  String foo;

  @Test
  void test(){
    assertThat(foo).isEqualTo("bar");
  }
}

foo.properties file are loaded into the application context. @TestPropertySource can also be configured more.

Use @MockBean to inject mocks

If we only want to test a certain part of the application instead of the entire path from the incoming request to the database, we can use @MockBean replace some beans in the application context:

@SpringBootTest
class MockBeanTest {

  @MockBean
  private UserRepository userRepository;

  @Autowired
  private RegisterUseCase registerUseCase;

  @Test
  void testRegister(){
    // given
    User user = new User("Zaphod", "zaphod@galaxy.net");
    boolean sendWelcomeMail = true;
    given(userRepository.save(any(UserEntity.class))).willReturn(userEntity(1L));

    // when
    Long userId = registerUseCase.registerUser(user, sendWelcomeMail);

    // then
    assertThat(userId).isEqualTo(1L);
  }
 
}

In this case, we replaced UserRepository bean with analog. Use Mockito of given method, we specify the expected behavior of this simulation to test the use of this repository class.
You can me about the simulation article read about in @MockBean more information annotations.

Use @Import to add Bean

If some beans are not included in the default application context, but we need them in the test, we can import them @Import

package other.namespace;

@Component
public class Foo {
}

@SpringBootTest
@Import(other.namespace.Foo.class)
class SpringBootImportTest {

  @Autowired
  Foo foo;

  @Test
  void test() {
    assertThat(foo).isNotNull();
  }
}

By default, a Spring Boot application includes all the components it finds in its packages and subpackages, so we usually only need to do this if we want to include beans in other packages.

Use @TestConfiguration to overwrite Bean

Using @TestConfiguration , we can not only include other beans needed for testing, but also overwrite beans that have been defined in the application. Read more in our article about 16184d680b01fb using @TestConfiguration to test

Create custom @SpringBootApplication

We can even create a complete custom Spring Boot application to start the test. If this application class is in the same package as the real application class, but in the test source instead of the production source, @SpringBootTest will find it before the actual application class and load the application context from this application.
Alternatively, we can tell Spring Boot which application class to use to create the application context:

@SpringBootTest(classes = CustomApplication.class)
class CustomApplicationTest {
}

However, when doing this, that may be completely different from the production environment, so this should be the last resort only when the production application cannot be launched in the test environment. However, there are usually better ways, such as making the real application context configurable to exclude beans that will not be launched in the test environment. Let us look at an example.
Suppose we use the @EnableScheduling annotation on the application class. Every time the application context is launched (even during testing), all @Scheduled jobs will be launched and may conflict with our testing. We usually don't want the job to run in the test, so we can create a second @EnabledScheduling annotation and use it in the test. However, a better solution is to create a configuration class that can use attribute switching:

@Configuration
@EnableScheduling
@ConditionalOnProperty(
        name = "io.reflectoring.scheduling.enabled",
        havingValue = "true",
        matchIfMissing = true)
public class SchedulingConfiguration {
}

We have moved the @EnableScheduling annotation from our application class to this special configuration class. Setting the property io.reflectoring.scheduling.enabled to false will cause this class not to be loaded as part of the application context:

@SpringBootTest(properties = "io.reflectoring.scheduling.enabled=false")
class SchedulingTest {

  @Autowired(required = false)
  private SchedulingConfiguration schedulingConfiguration;

  @Test
  void test() {
    assertThat(schedulingConfiguration).isNull();
  }
}

We have now successfully disabled scheduled assignments in the test. The attribute io.reflectoring.scheduling.enabled can be specified in any of the above-mentioned ways.

Why is my integration test so slow?

A code base containing a large number of @SpringBootTest annotation tests may take a considerable amount of time to run. Spring's test support is smart enough to to create an application context once and reuse it in subsequent tests, but if different tests require different application contexts, it will still create a separate context for each test, which takes some time To complete each test.
All the customization options described above will cause Spring to create a new application context . Therefore, we may want to create a configuration and use it for all tests so that the application context can be reused.
If you are interested in the time the test spends on setup and Spring application context, you may want to check out JUnit Insights , which can be included in a Gradle or Maven build to generate a good report on how JUnit 5 spends time .

in conclusion

@SpringBootTest is a very convenient way to set up the application context for testing, it is very close to the context we will use in production. There are many options to customize this application context, but they should be used with caution because we want our tests to run as close to production as possible.
If we want to test in the entire application, @SpringBootTest will bring the most value. In order to test only certain slices or layers of the application, we have other options available.
The sample code used in this article can be found on github at .


信码由缰
65 声望8 粉丝

“码”界老兵,分享程序人生。