Overview
This article mainly describes how to write unit test, integration test code for Spring-boot-based Web applications.
The architecture diagram for such an application is generally as follows:
The program of our project corresponds to the Web Application section in. This part is generally divided into controller layer, service layer, persistence layer. In addition, there are some data encapsulation classes in the application, which we call domain. The responsibilities of the above components are as follows:
- Controller layer/rest Interface layer: responsible for providing rest services externally, receiving rest requests, returning processing results.
- Service layer: The business logic layer, according to the needs of the controller layer, the implementation of specific logic.
- Persistent layer: Access to the database for data read and write. Supports database access requirements for the service layer upwards.
In the spring environment, we typically register these three layers with the spring container, using a light blue background to represent this.
In the follow-up to this article, we'll show you how to test your application for integration, including testing the request to start a Web container, using a simulated environment without launching the Web container, and how to unit test your app, including testing the controller layer, service layer, and persistence layer separately.
The difference between integration and unit testing is that integration testing usually only needs to test the top level, because the upper layer automatically calls the lower layers, so the complete process chain is tested, and each link in the process chain is real and concrete. Unit testing is a single loop in the process chain, which relies directly on downstream links to provide support using simulations, a technique known as a mock. In introducing unit tests, we'll show you how to mock dependent objects and simply introduce the principles of a mock.
Another topic of concern in this article is how to eliminate the side effects of modifying a database when you are testing the persistence layer.
Integration Testing
Integration testing is done after all components have been developed and assembled. There are two ways to test: Start a Web container for testing, and use a simulated environment test. The effect of these two Tests is no different, just using the simulated environment test, you can not start the Web container, there will be less overhead. In addition, the test APIs for both are different.
Start the Web container for testing
We implement integration testing by testing the top controller, and our test objectives are as follows:
@RestControllerpublic class CityController { @Autowired private CityService cityService; @GetMapping("/cities") public ResponseEntity<?> getAllCities() { List<City> cities = cityService.getAllCities(); return ResponseEntity.ok(cities); }}
This is a controller that provides a service to the outside /cities
and returns a list containing all the cities. The controller accomplishes his duty by invoking the next layer of cityservice.
The integration test scenario for this controller is as follows:
@RunWith(SpringRunner.class)@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)public class CityControllerWithRunningServer { @Autowired private TestRestTemplate restTemplate; @Test public void getAllCitiesTest() { String response = restTemplate.getForObject("/cities", String.class); Assertions.assertThat(response).contains("San Francisco"); }}
First, we use the @RunWith(SpringRunner.class)
declaration to perform unit tests in the spring environment so that the relevant annotations of spring are recognized and effective. Then we use @springboottest, which scans the application's spring configuration and builds the full spring Context. We assign a value of SpringBootTest.WebEnvironment.RANDOM_PORT to its parameter webenvironment, which launches the Web container and listens on a random port, For us to automatically assemble a testresttemplate type of bean to assist us in sending requests.
Using simulated environment testing
The goal of the test is the same, and the test scenario is as follows:
@RunWith(SpringRunner.class)@SpringBootTest@AutoConfigureMockMvcpublic class CityControllerWithMockEnvironment { @Autowired private MockMvc mockMvc; @Test public void getAllCities() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/cities")) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("San Francisco"))); }}
We still use it, but we do @SpringBootTest
not set its webEnvironment
properties so that the full spring Context will still be built, but the Web container will no longer be started. In order to test, we need to send the request using an MockMvc
instance, and we use it @AutoConfigureMockMvc
because we can get an automatically configured MockMvc
instance.
There are many new APIs in the code for specific tests, and the study of API details is beyond the scope of this article.
Unit Test
The same scenario for the two integration tests described above is that the entire spring Context will be built. This means that all declared beans, regardless of the way they are declared, will be constructed and can be relied upon. The implication here is that the code on the entire dependency chain has been implemented from top to bottom.
Mock technology
Testing in the development process does not meet the above conditions, and the mock technology allows us to mask the dependency of the falling layer, thus focusing on the current test target. The idea of mock technology is that when the underlying behavior of a test target is predictable, the behavior of the test target itself is predictable, and the test is to compare the actual results with the expected results of the test target, and the mock is to pre-set the behavior of the underlying dependency.
The process of mock
- Mock the dependent object of the test target to set its intended behavior.
- Test the test target.
- Detects the test results and checks to see if the results of the test targets meet expectations under the expected behavior of the dependent objects.
Usage Scenarios for mock
- When collaborating with multiple people, you can do a mock-up without waiting for a test.
- When a dependent object of a test target needs access to an external service, while an external service is not readily available, a mock can be used to simulate a service's availability.
- Mock the problem when you are troubleshooting a problem scenario that is not easy to reproduce.
Test the Web Tier
The goal of the test is the same, and the test scenario is as follows:
/** * Does not build the entire spring Context, only the specified controller is built for testing. The dependent dependencies need to be mock.<br> * Created by lijinlong9 on 2018/8/22. */@RunWith (Springrunner.class) @WebMvcTest (citycontroller.class) public class Citycontrollerweblayer {@Autowired pri vate MOCKMVC MVC; @MockBean Private Cityservice Service; @Test public void Getallcities () throws Exception {City city = new City (); City.setid (1L); City.setname ("Hangzhou"); City.setstate ("Zhejiang"); City.setcountry ("China"); Mockito.when (Service.getallcities ()). Thenreturn (Collections.singletonlist (city)); Mvc.perform (Mockmvcrequestbuilders.get ("/cities")). Anddo (Mockmvcresulthandlers.print ()). An Dexpect (Mockmvcresultmatchers.content (). String (matchers.containsstring ("Hangzhou"))); }}
This is no longer used @SpringBootTest
, and instead @WebMvcTest
, it will only build the web layer or the specified bean for one or more controllers. @WebMvcTest
It is also possible for us to automatically configure MockMvc
the type of bean, which we can use to simulate sending requests.
@MockBean
is a new contact annotation that indicates that the corresponding bean is a simulated bean. Because we want to test CityController
and rely on it CityService
, we need to mock its expected behavioral performance. In the specific test method, the Mockito API is used to mock the behavior of sercive, which indicates that when the service's getallcities is invoked, a pre-set list of city objects is returned.
After that, the request is initiated and the result is predicted.
Mockito is a mock test framework for the Java language, and spring integrates it in its own way.
Test persistence layer
The test scheme for the persistence layer is related to the specific persistence layer technology. Here we introduce the test of persistence layer based on mybatis.
The test objectives are:
@Mapperpublic interface CityMapper { City selectCityById(int id); List<City> selectAllCities(); int insert(City city);}
The test scenarios are:
@RunWith (Springrunner.class) @MybatisTest @fixmethodorder (value = methodsorters.name_ascending)//@Transactional ( propagation = propagation.not_supported) public class Citymappertest {@Autowired private citymapper citymapper; @Test public void/*selectcitybyid*/test1 () throws Exception {City city = Citymapper.selectcitybyid (1); Assertions.assertthat (City.getid ()). Isequalto (long.valueof (1)); Assertions.assertthat (City.getname ()). Isequalto ("San Francisco"); Assertions.assertthat (City.getstate ()). Isequalto ("CA"); Assertions.assertthat (City.getcountry ()). Isequalto ("US"); } @Test public void/*insertcity*/test2 () throws Exception {City city = new City (); City.setid (2L); City.setname ("Hangzhou"); City.setstate ("Zhejiang"); City.setcountry ("CN"); int result = Citymapper.insert (city); Assertions.assertthat (Result). Isequalto (1); } @Test public void/*selectnewinsertedcity*/test3 () Throws Exception {City city = Citymapper.selectcitybyid (2); Assertions.assertthat (city). IsNull (); }}
Used here @MybatisTest
, it is responsible for building the bean of the mybatis-mapper layer, just like the @WebMvcTest
Bean responsible for building the Web layer used earlier. It is worth mentioning that @MybatisTest
it comes from the mybatis-spring-boot-starter-test
project, which is implemented by the MyBatis team according to Spring's habits. The two persistent layer test scenarios supported by spring are the @DataJpaTest
and @JdbcTest
, respectively, corresponding to the JPA persistence scheme and the JDBC Persistence scheme.
@FixMethodOrder
From JUnit to allow multiple test scenarios in a test class to be executed in a set order. In general, this is not required, and I would like to confirm that the data inserted in the Test2 method is still present in the TEST3, so the order of execution needs to be guaranteed.
We inject CityMapper
, because there is no underlying dependency, so we don't need to mock.
@MybatisTest
In addition to instantiating mapper-related beans, it detects embedded databases in dependencies and then uses inline databases when testing. If there is no embedded database in the dependency, it will fail. Of course, using an inline database is the default behavior and can be modified using configuration.
@MybatisTest
It also ensures that every test method is a transaction rollback, so in the test case above, Test2 inserts the data, and the inserted data is still not available in test3. Of course, this is also the default behavior that can be changed.
To test any bean
The service layer does not act as a special layer, so there is no annotation that can represent the concept of "just building a service layer of beans."
Here's another generic test scenario, and I'm going to test for a normal bean with no special roles, such as a controller that is not a special handler, or a DAO component that is responsible for persistence, and what we're testing is just a normal bean.
The default mechanism we used above is @SpringBootTest
to find @SpringBootApplication
the configuration that builds the spring context accordingly. Check out @SpringBootTest
the doc, which has one sentence:
Automatically searches for a @SpringBootConfiguration when nested @Configuration was not used, and no explicit classes was Specified.
This means that we can use the Classes property to specify the configuration class, or to define an inline config class to alter the default configurations.
Here we implement through the embedded configuration class, first look at the test target-Cityservice:
@Servicepublic class CityService { @Autowired private CityMapper cityMapper; public List<City> getAllCities() { return cityMapper.selectAllCities(); }}
Test scenario:
@RunWith(SpringRunner.class)@SpringBootTestpublic class CityServiceTest { @Configuration static class CityServiceConfig { @Bean public CityService cityService() { return new CityService(); } } @Autowired private CityService cityService; @MockBean private CityMapper cityMapper; @Test public void getAllCities() { City city = new City(); city.setId(1L); city.setName("杭州"); city.setState("浙江"); city.setCountry("CN"); Mockito.when(cityMapper.selectAllCities()) .thenReturn(Collections.singletonList(city)); List<City> result = cityService.getAllCities(); Assertions.assertThat(result.size()).isEqualTo(1); Assertions.assertThat(result.get(0).getName()).isEqualTo("杭州"); }}
Similarly, for the purpose of testing a dependency, we need to mock.
Mock operation
In unit tests, it is necessary to mock the dependencies of the test targets, which are needed to introduce the details of the mock. The logic, process, and usage scenarios of the mock are described in the Unit test section above, which focuses on a practical level of explanation.
Set expected behavior based on method parameters
The general mock is a mock of the method level, in which case the method's behavior may be related to the method's specific parameter value. For example, a method of division, passed the parameter 4, 2 results 2, passed the parameter 8, 2 results in 4, passed the parameter 2, 0 is abnormal.
A mock can set different expectations for different parameter values, as follows:
@RunWith (springrunner.class) @SpringBootTestpublic class Mathservicetest {@Configuration St Atic class Configtest {} @MockBean private mathservice mathservice; @Test public void Testdivide () {Mockito.when (Mathservice.divide (4, 2)). Thenreturn (2); Mockito.when (Mathservice.divide (8, 2)). Thenreturn (4); Mockito.when (Mathservice.divide (Argumentmatchers.anyint (), Argumentmatchers.eq (0))//must also use the matchers syntax. the Nthrow (New RuntimeException ("error")); Assertions.assertthat (Mathservice.divide (4, 2)). Isequalto (2); Assertions.assertthat (Mathservice.divide (8, 2)). Isequalto (4); Assertions.assertthatexceptionoftype (Runtimeexception.class). Isthrownby (()-{Math Service.divide (3, 0); }). withmessagecontaining ("error"); }}
The above test may be a bit strange, and the mock object is also the target of the test. This is because our goal is to introduce mock, which simplifies the testing process.
As can be seen from the above test cases, we can specify the behavior when the parameters meet certain matching rules in addition to the specific arguments.
There is a way to return
For methods that have a return, the behavior that can be set for a mock is:
Returns the result of the setting, such as:
when(taskService.findResourcePool(any())) .thenReturn(resourcePool);
Throws an exception directly, such as:
when(taskService.createTask(any(), any(), any())) .thenThrow(new RuntimeException("zz"));
Actually call the real method, such as:
when(taskService.createTask(any(), any(), any())) .thenCallRealMethod();
Note that invoking the real method is contrary to the original meaning of the mock and should be avoided as much as possible. If other dependencies are called in the method being called, you need to inject additional dependencies yourself, otherwise you will have null pointers.
No way to return
For methods that do not return, the behavior that can be set on a mock is:
Throws an exception directly, such as:
doThrow(new RuntimeException("test")) .when(taskService).saveToDBAndSubmitToQueue(any());
The actual invocation (the example given in doc in the Mockito class, I did not encounter this requirement), such as:
doAnswer(new Answer() { public Object answer(InvocationOnMock invocation) { Object[] args = invocation.getArguments(); Mock mock = invocation.getMock(); return null; }}).when(mock).someMethod();
Summary of appendix related annotations
@RunWith
:
JUnit annotations, which are used by this annotation SpringRunner.class
, are able to integrate JUnit and spring. Subsequent spring-related annotations will be effective.
@SpringBootTest
:
Spring annotations that build a spring context for testing by scanning the configuration in the application.
@AutoConfigureMockMvc
:
Spring annotations, which enable automatic configuration of MockMvc
object instances to send HTTP requests in a simulated test environment.
@WebMvcTest
:
Spring annotations, one of the slicing tests. Replacing it @SpringBootTest
with the ability to limit the build bean to the Web tier, but the underlying dependency of the bean on the web layer needs to be simulated by a mock. You can also specify that only one or more controllers of the Web tier are instantiated by using parameters. Refer to auto-configured Spring MVC Tests for details.
@RestClientTest
:
Spring annotations, one of the slicing tests. If your application accesses other rest services as a client, you can use this annotation to test the functionality of the client. Refer to auto-configured REST clients for details.
@MybatisTest
:
MyBatis the annotations developed according to Spring's custom, one of the slicing tests. Replace to @SpringBootTest
limit the return of the build bean to the mybatis-mapper layer. Refer to Mybatis-spring-boot-test-autoconfigure for details.
@JdbcTest
:
Spring annotations, one of the slicing tests. If you use JDBC as the persistence layer (spring) in your application JdbcTemplate
, you can use that annotation instead @SpringBootTest
to qualify the bean's build scope. Official reference materials are limited and can be found online.
@DataJpaTest
:
Spring annotations, one of the slicing tests. If you use JPA as a persistence layer technology, you can use this annotation to refer to auto-configured Data JPA Tests.
@DataRedisTest
:
Spring annotations, one of the slicing tests. Refer to auto-configured Data Redis Tests for details.
Setting up a test database
Add annotations to the persistence layer test class @AutoConfigureTestDatabase(replace = Replace.NONE)
you can use the configured database as the test database. At the same time, you need to configure the data source in the configuration file as follows:
spring: datasource: url: jdbc:mysql://127.0.0.1/test username: root password: root driver-class-name: com.mysql.jdbc.Driver
Transaction does not roll back
You can add it on a test method @Rollback(false)
to set it up without rolling back, or you can add it at the level of the test class to indicate that all of the test methods for that class are not rolled back.
Reference
- Spring Boot Testing
- Spring Boot Test Blog
- Mybatis Spring Boot Test official profile