preface

Springboot provides sprng-boot-starter-test for developers to use for unit testing, after introducing the spring-boot-starter-test dependency:

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
</dependency>
Copy the code

It contains the following libraries:

  • Junit – Commonly used unit test library
  • Spring Test & Spring Boot Test — Integration testing support for Spring applications
  • AssertJ – An assertion library
  • Hamcrest – a library of matching objects
  • Mockito – A Java simulation framework
  • JSONassert – a library of assertions for JSON
  • JsonPath — XPath for JSON

Let’s take a brief look at unit testing from the Service and Controller layers

Service unit Testing

In SpringBoot 2.0, create a unit test of a Service as follows:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceImplTest {
    @Autowired
    private UserService userService;
    @Test
    public void insertUser(a) {
        User user = new User();
        user.setUsername("li ning");
        user.setPassword("123456"); userService.insertUser(user); }}Copy the code

The above test is very simple, with two main annotations to note: @runwith and @SpringbooTtest

  • @RunWith: This annotation tag is provided by Junit and identifies the operator of this test class, used hereSpringRunnerIt actually inheritsSpringJUnit4ClassRunnerClass, andSpringJUnit4ClassRunnerThis class is a custom extension to the Junit runtime environment to standardize Junit4.x test cases in a Springboot environment
  • @SpringBoottest creates a context for the springApplication and supports the SpringBoot feature

Define the runtime environment using the webEnvironment attribute of @SpringBoottest:

  • Mock(default): Loads the WebApplicationContext and provides a Mock Web environment Servlet environment. When this annotation is used, the embedded server is not started
  • RANDOM_PORT: load WebServerApplicationContext and provide real web environment, embedded server, listener port is random
  • DEFINED_PORT: load WebServerApplicationContext and provide real Web environment, embedded server is up and listening defined port (8080) from the application. The properties or the default port
  • NONE: Loads the ApplicationContext with SpringApplication but does not provide any Web environment

Unit test of Controller

First create a Controller as follows:

@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @PostMapping("/user")
    public String userMapping(@RequestBody User user){
        userService.insertUser(user);
        return "ok"; }}Copy the code

Unit tests for controllers are then created, usually in one of two ways.

The first uses a simulated environment for testing

By default, @SpringBooTtest does not start a server. If you need to test a Web endpoint against this simulated environment, you can configure MockMvc as follows:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    public void userMapping(a) throws Exception {
        String content = "{\"username\":\"pj_mike\",\"password\":\"123456\"}";
        mockMvc.perform(MockMvcRequestBuilders.request(HttpMethod.POST, "/user")
                        .contentType("application/json").content(content))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string("ok")); }}Copy the code

There is an @AutoConfiguRemockMVC annotation that indicates that MockMvc is automatically injected when the test is started, and that MockMvc has the following basic methods:

  • perform: Executes a RequestBuilder request, which automatically executes the SpringMVC process and maps it to the appropriate controller for processing.
  • andExpect: Adds a RequsetMatcher verification rule to verify whether the result is correct after the controller is executed
  • andDo: adds a ResultHandler ResultHandler, such as printing results to the console during debugging
  • andReturn: Finally returns the corresponding MvcResult, then custom validation/next asynchronous processing

Here’s a little trick. Generally speaking, if you have more than one Request to test in a controller, knocking MockMvcRequestBuilders and MockMvcResultMatchers can be tedious. An easy way to do this is to import the methods of both classes as import static, and then use the static methods of both classes directly. The code then looks like this:

.import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    public void userMapping(a) throws Exception {
        String content = "{\"username\":\"pj_mike\",\"password\":\"123456\"}";
        mockMvc.perform(request(HttpMethod.POST, "/user")
                        .contentType("application/json").content(content))
                .andExpect(status().isOk())
                .andExpect(content().string("ok")); }}Copy the code

Also, if you only want to focus on the Web layer and not launch the full ApplicationContext, consider using the @webMvcTest annotation. This annotation cannot be used with @SpringbooTtest, and it only focuses on the Web layer. You need to introduce dependencies, see the official documentation for more information about this annotation: docs.spring. IO /spring-boot…

Build the MockMvc object using MockMvcBuilder

In addition to injecting MockMvc directly and automatically with the @AutoConfigURemockMVC annotation above, we can use MockMvcBuilder to build a MockMvc object, as shown in the following example:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest4 {
    @Autowired
    private WebApplicationContext web;
    private MockMvc mockMvc;

    @Before
    public void setupMockMvc(a) {
        mockMvc = MockMvcBuilders.webAppContextSetup(web).build();
    }
    @Test
    public void userMapping(a) throws Exception {
        String content = "{\"username\":\"pj_m\",\"password\":\"123456\"}";
        mockMvc.perform(request(HttpMethod.POST, "/user")
                        .contentType("application/json").content(content))
                .andExpect(status().isOk())
                .andExpect(content().string("ok")); }}Copy the code

The second uses a real Web environment for testing

Set the attribute webEnvironment = webEnvironment. RANDOM_PORT in the @SpringBooTtest annotation and randomly select one available port each time you run it. We can also use the @loalServerPort annotation for local port numbers. Here is the test code:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerTest3 {
    @Autowired
    private TestRestTemplate testRestTemplate;
    @Test
    public void userMapping(a) throws Exception {
        User user = new User();
        user.setUsername("pj_pj");
        user.setPassword("123456");
        ResponseEntity<String> responseEntity = testRestTemplate.postForEntity("/user", user, String.class);
        System.out.println("Result: "+responseEntity.getBody());
        System.out.println("Status code:+responseEntity.getStatusCodeValue()); }}Copy the code

The above code has a key class called TestRestTemplate. TestRestTemplate is an alternative to Spring’s RestTemplate and can be used for integration testing. This class is generally used in real Web environment testing. For more details about how to use this class, refer to the official documentation: docs.spring. IO /spring-boot…

Unit test rollback

You can use the @Transactional annotation in the header of a method or class to enable transactions if you don’t want to generate garbage data.

If your test is @Transactional, it rolls back the transaction at the end of each test method by default. However, as using this arrangement with either RANDOM_PORT or DEFINED_PORT implicitly provides a real servlet environment, the HTTP client and server run in separate threads and, thus, in separate transactions. Any transaction initiated on the server does not roll back in this case

To paraphrase, using the @Transactional annotation in a unit test rolls back the transaction at the end of the test method by default. However, there are a few special cases to note when we use RANDOM_PORT or DEFINED_PORT. This arrangement implicitly provides a true Servlet environment, so the HTTP client and server will run in different threads, separating transactions. In this case, Any transactions started on the server are not rolled back.

If you want to turn off the Rollback, add the @rollback (false) annotation. @rollback indicates that the transaction is rolled back. You can pass in a value.

Another thing to be aware of is that if you are using MySQL and sometimes find that @transactionl annotations do not roll back, you should check if your default engine is InnoDB, and if not change to InnoDB.

MyISAM and InnoDB are two database engines commonly used by mysql at present. The main difference between MyISAM and InnoDB lies in performance and transaction control. Here are the differences and conversion methods of MyISAM and InnoDB:

  • MyISAM: MyISAM is the default database storage engine prior to MySQL5.5. MyISAM provides high-speed storage and retrieval, as well as full-text search capabilities, suitable for data warehouse and other query-intensive applications, but does not support transactions and foreign keys, and cannot recover data after table corruption
  • InnoDB: InnoDB is the default database storage engine for MySQL5.5. InnoDB has transaction security with commit, rollback and crash recovery capabilities, supports transactions and foreign keys,InnoDB writes less efficiently than MyISAM and takes up more disk space to retain data and indexes.

If your data table is the MyISAM engine, since it does not support transactions, add transaction annotations to the unit test and the test method will not be rolled back.

Modifying the Default Engine

  • View the current default storage engine of MySQL
mysql> show variables like '%storage_engine%';
Copy the code
  • See what engine the user table uses.
mysql> show create table user;
Copy the code
  • Alter table user as InnoDB storage engine
mysql> ALTER TABLE user ENGINE=INNODB;

Copy the code

Pay attention to

Another point to note here is that when we use Spring Data JPA, if we do not specify the storage engine for MySQL to build tables, MySQL MyISAM is used by default. This is also a pitfall. In this case, you use the @Transactional annotation in your unit tests. Rollback won’t work.

The solution is to hibernate. The dialect attributes configured to hibernate. The dialect = org. Hibernate. The dialect. MySQL5InnoDBDialect, use the InnoDB engine built specified MySQL table, The following is an example configuration file:

spring:
  jpa:
    # database type
    database: mysql
    # output log
    show-sql: true
    properties:
      hibernate:
        # JPA configuration
        hbm2ddl.auto: update
        Mysql storage type configuration
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect
Copy the code

summary

This is a brief summary of how to use unit testing under Springboot. For more details on unit testing, please refer to the official documentation: docs.spring. IO /spring-boot…

References & acknowledgements

  • Spring Boot uses unit testing
  • Official springBoot document