Understand the spring 5 mock test

Posted by jacinthe on Fri, 04 Mar 2022 20:18:13 +0100

preface

Many times, we developers are used to using postman to test the interface directly, but the disadvantage of using postman is that IT is only suitable for developers to test themselves, which is not convenient for the team to share, and IT is difficult to cover the logical branch methods of an interface involving all levels. When IT comes to the coverage of code logic, junit testing has a natural advantage in this regard. Generally, IT Internet companies require that the submitted code should have test cases, and there are certain requirements for the logical coverage of test cases, which is generally required to cover more than 70%.

In the absence of test cases, once someone in the project team leaves the team without leaving the interface document before leaving, the maintenance of the newly added and taken over employees will be more painful. The input parameters of each interface have to be found on the page one by one in the Network interface through the debugging mode. If the input parameters of an interface are relatively small, it's good to say that once the interface parameters are more than 50, the source code is thousands of lines of code for an interface, and it involves calling a third-party interface. At this time, it's really difficult to do without test cases.

The author recently took over a project called friends business travel in the company, which involves complex business needs such as ticket query and ticket ordering. Moreover, this demand does not need to be developed from scratch, but modified on the existing basis. The first step is to adjust the original interface related to ticket. Because there are no test cases, we can only rely on reading the source code and viewing the remarks of database fields to debug the interface step by step, and the efficiency can be said to be quite low. Fortunately, a detailed interface document was found later to speed up the progress. But it also made me realize that the developed interface has the advantage of complete test cases.

The purpose of this article is to take you to learn in springboot 2 Project x learns to complete test cases for the service classes and controller classes developed by itself, which not only facilitates project maintenance, but also meets the requirements of some companies that the submitted code must have test cases.

Introduction to spring boot starter test module

Spring Boot provides some tool classes and annotations to help developers Test their own functional modules. Spring Boot supports Test in two modules: Spring Boot Test, which contains core projects, and Spring Boot Test autoconfigure, which supports automatic configuration.

Most developers use spring boot starter test, which imports the spring boot test module and useful class libraries such as Jupiter, assertj and hamcrest.

Note: spring boot starter test start dependency introduces vintage engine, so you can run Junit4 and Junit5 tests at the same time. If you have upgraded your test class to JUnit 5, you can exclude JUnit 4 support from the dependency in the following way.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Several important annotations in spring 5 test class

@SpringBootTest

This annotation works on your test class. The @ SpringBootTest annotation can replace the @ ContextConfiguration in the standard Spring Test. Its function is to create an application context through the SpringBoot application in your test class

If you are using Junit4, don't forget to add @ RunWith(SpringRunner.class) annotation on your test class; If you use JUnit 5, you don't need to add the equivalent @ ExtendWith(SpringExtension.class) annotation. Because the @ ExtendWith(SpringExtension.class) annotation has been added to the @ SpringBootTest annotation@ The source code of SpringBootTest annotation is as follows:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class})
public @interface SpringBootTest {
    // Equivalent to the properties property, used to configure environment variable properties
    @AliasFor("properties")
    String[] value() default {};
    // Equivalent to the value attribute, used to configure the environment variable attribute
    @AliasFor("value")
    String[] properties() default {};
    // Test class application startup parameters
    
    String[] args() default {};
    // Configuration class
    Class<?>[] classes() default {};
    // Formulate the web environment, and use the mock Web environment by default
    SpringBootTest.WebEnvironment webEnvironment() default SpringBootTest.WebEnvironment.MOCK;
    // web environment enumeration
    public static enum WebEnvironment {
        //Using mock web Environment 
        MOCK(false),
        // Assign random port web Environment
        RANDOM_PORT(true),
        // Develop port wen environment
        DEFINED_PORT(true),
        // Do not use web Environment
        NONE(false);
        // Whether to use embedded container
        private final boolean embedded;

        private WebEnvironment(boolean embedded) {
            this.embedded = embedded;
        }

        public boolean isEmbedded() {
            return this.embedded;
        }
    }
}

From the source code, we can see that the test class marked with @ SpringBootTest uses the mock Web environment by default

By default, @ SpringBootTest will not start a server. You can customize how your test class starts by using the webEnvironment attribute in the @ SpringBootTest annotation

  • Mock (default): load an ApplicationContext and provide a Mock Web environment. When you use this enumeration value, the embedded service will not start; If there is no Web application environment in your classpath, this mode will create a non Web ApplicationContext (application context), which can be used in conjunction with @ AutoConfigureMockMvc or @ AutoConfigureWebTestClient annotations in mock based test classes
  • RANDOM_PORT: load a webserver ApplicationContext (Web service application context) and provide a real web environment, start the embedded web container (such as tomcat or Jetty) and listen to the randomly assigned ports
  • Load a webserver ApplicationContext and provide a real web environment, start the embedded web container and listen to your application The port defined in the properties configuration file listens to port 8080 by default
  • NONE: loads an ApplicationContext and uses spring application, but does not provide any Web environment

Note: if you add @ Transactional annotation to your test class, it will roll back the transaction after each test method is executed by default. However, if you use RANDOM_PORT or DEFINED_PORT opens the real servlet web environment. In this case, the http client and server run in an independent thread. At this time, any transaction executed in the test method will not be rolled back after the test method is executed

@MockBean and @ SpyBean annotation

@The MockBean annotation is generally used on the bean attributes injected in the test class. It represents a simulated bean. Its usage in the official document is as follows:

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.*;
import org.springframework.boot.test.context.*;
import org.springframework.boot.test.mock.mockito.*;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;

@SpringBootTest
class MyTests {

    @MockBean
    private RemoteService remoteService;

    @Autowired
    private Reverser reverser;
    
    @Test
    void exampleTest() {
        // RemoteService has been injected into the reverser bean
        given(this.remoteService.someCall()).willReturn("mock");
        String reverse = reverser.reverseSomeCall();
        assertThat(reverse).isEqualTo("kcom");
    }

}

This annotation can be added to the bean attributes in the test class and the test class at the same time. If you want to test the use of real beans, use @ Autowired or @ Resource and other automatic assembly annotations

@The SpyBean annotation is similar to the @ MockBean annotation and is also used to simulate a bean@ SpyBean annotation can also be used on classes and attributes. Its usage in official documents is as follows:

@RunWith(SpringRunner.class)
 public class ExampleTests {

     @SpyBean
     private ExampleService service;

     @Autowired
     private UserOfService userOfService;

     @Test
     public void testUserOfService() {
         String actual = this.userOfService.makeUse();
         assertEquals("Was: Hello", actual);
         verify(this.service).greet();
     }

     @Configuration
     @Import(UserOfService.class) // A @Component injected with ExampleService
     static class Config {
     }

 }

@AutoConfigureMockMvc annotation

This annotation is added to the test class to automatically assemble the MockMvc test controller. After adding this annotation to the test class, you can inject the MockMvc strength bean into the test method through the @ Autowired annotation. The demo usage on the official website is as follows:

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class MockMvcExampleTests {

    @Test
    void exampleTest(@Autowired MockMvc mvc) throws Exception {
        mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string("Hello World"));
    }

}

There is a perform(RequestBuilder requestBuilder) method in the MockMvc class, which can execute http requests including GET|POST|PUT|DELETE and return an object of type ResultActions. ResultActions is an interface class, which mainly includes the following three abstract methods to represent the execution of expectation matching, further actions and return results

public interface ResultActions {
    ResultActions andExpect(ResultMatcher var1) throws Exception;

    ResultActions andDo(ResultHandler var1) throws Exception;

    MvcResult andReturn();
}

The implementation class of the ResultActions interface class is implemented in the MockMvc#perform method. Here I attach this part of the source code:

return new ResultActions() {
            public ResultActions andExpect(ResultMatcher matcher) throws Exception {
                matcher.match(mvcResult);
                return this;
            }

            public ResultActions andDo(ResultHandler handler) throws Exception {
                handler.handle(mvcResult);
                return this;
            }

            public MvcResult andReturn() {
                return mvcResult;
            }
        };

The RequestBuilder type parameter in the MockMvc#perform method can be constructed through the static method in the abstract class mockmvcrequebuilders, and the return is the implementation class MockHttpServletRequestBuilder object of RequestBuilder.

Several important construction methods in the MockHttpServletRequestBuilder class are as follows:

/**
* Construct a GET type request through the url template parameter and the placeholder parameter variable in the url
* @param urlTemplate url Template, example: / contextpath / path? ket1={key1}&key2={key2}
* @param uriVars Parameter, example: [key1,key2]
*/
public static MockHttpServletRequestBuilder get(String urlTemplate, Object... uriVars) {
        return new MockHttpServletRequestBuilder(HttpMethod.GET, urlTemplate, uriVars);
    }

    /**
    * Construct a GET type request directly through the URI
    * @param URI The url of the request can be directly constructed by https or the url of the request, which must conform to the http protocol
    */
    public static MockHttpServletRequestBuilder get(URI uri) {
        return new MockHttpServletRequestBuilder(HttpMethod.GET, uri);
    }

    /**
    * Construct POST type request through url template and parameters
    * @param urlTemplate url Template. The example is the same as the parameter input method in the GET request
    * @param uriVars url Placeholder parameter variable in
    */
    public static MockHttpServletRequestBuilder post(String urlTemplate, Object... uriVars) {
        return new MockHttpServletRequestBuilder(HttpMethod.POST, urlTemplate, uriVars);
    }

    /**
    * POST type requests are constructed directly through URI parameters
    * param uri Request path URI type parameter
    */
    public static MockHttpServletRequestBuilder post(URI uri) {
        return new MockHttpServletRequestBuilder(HttpMethod.POST, uri);
    }

    /**
    * The PUT type request is constructed by url template and query parameter variable
    * @param urlTemplate url Template
    * @param uriVars url Template placeholder parameter variable
    */
    public static MockHttpServletRequestBuilder put(String urlTemplate, Object... uriVars) {
        return new MockHttpServletRequestBuilder(HttpMethod.PUT, urlTemplate, uriVars);
    }
    
    /**
    * Construct a PUT type request through URI parameters
    * @param uri url Wrapper type URI parameter
    */
    public static MockHttpServletRequestBuilder put(URI uri) {
        return new MockHttpServletRequestBuilder(HttpMethod.PUT, uri);
    }
    
    /**
    * Construct a DELETE type request through url template and placeholder parameter variables
    * @param urlTemplate Request url template
    */
    public static MockHttpServletRequestBuilder delete(String urlTemplate, Object... uriVars) {
        return new MockHttpServletRequestBuilder(HttpMethod.DELETE, urlTemplate, uriVars);
    }

    /**
    * Construct a DELETE type request through the request url wrapper class URI type parameter
    * @param uri Request url wrapper class URI type parameter
    */
    public static MockHttpServletRequestBuilder delete(URI uri) {
        return new MockHttpServletRequestBuilder(HttpMethod.DELETE, uri);
    }
    
    /**
    * Construct an OPTIONS type request through url template and placeholder parameter variables
    * @param urlTemplate url Template parameters
    * @param uriVars url Placeholder variable parameters in template parameters
    */
    public static MockHttpServletRequestBuilder options(String urlTemplate, Object... uriVars) {
        return new MockHttpServletRequestBuilder(HttpMethod.OPTIONS, urlTemplate, uriVars);
    }
    
    /**
    * Directly construct an OPTIONS type parameter through the uri parameter
    * @param URI Type parameter
    */
    public static MockHttpServletRequestBuilder options(URI uri) {
        return new MockHttpServletRequestBuilder(HttpMethod.OPTIONS, uri);
    }
    
    /**
    * Construct a request of a specified type through request type parameters, url template parameters and placeholder variable parameters
    * @param method Http Request type (enumeration value)
    * @param urlTemplate url Template
    * @param uriVars Placeholder variable
    */
    public static MockHttpServletRequestBuilder request(HttpMethod method, String urlTemplate, Object... uriVars) {
        return new MockHttpServletRequestBuilder(method, urlTemplate, uriVars);
    }
    
    /**
    * Construct a request of a specified type through the request type parameter and uri parameter
    * @param httpMethod http Request type (enumeration value)
    * @param uri Request path wrapper class URI type parameter
    */
    public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, URI uri) {
        return new MockHttpServletRequestBuilder(httpMethod, uri);
    }
    
    /**
    * Construct a request of a specified type through the request type and uri parameters
    * @param httpMethod Request type, example: GET|POST|PUT|DELETE
    */
    public static MockHttpServletRequestBuilder request(String httpMethod, URI uri) {
        return new MockHttpServletRequestBuilder(httpMethod, uri);
    }
    
    /**
    * Construct a file upload request through url template parameters and placeholder parameters
    * @param urlTemplate
    * @param uriVars 
    */
    public static MockMultipartHttpServletRequestBuilder multipart(String urlTemplate, Object... uriVars) {
        return new MockMultipartHttpServletRequestBuilder(urlTemplate, uriVars);
    }
    
    /**
    * Construct a file upload request directly through the uri parameter
    */
    public static MockMultipartHttpServletRequestBuilder multipart(URI uri) {
        return new MockMultipartHttpServletRequestBuilder(uri);
    }

@Mvwebtest annotation

This annotation is used on the test class to test a single controller class. It is generally used together with MockMvc. Its usage on the official document is as follows:

import com.gargoylesoftware.htmlunit.*;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.boot.test.autoconfigure.web.servlet.*;
import org.springframework.boot.test.mock.mockito.*;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;

@WebMvcTest(UserVehicleController.class)
class MyHtmlUnitTests {

    @Autowired
    private WebClient webClient;

    @MockBean
    private UserVehicleService userVehicleService;

    @Test
    void testExample() throws Exception {
        given(this.userVehicleService.getVehicleDetails("sboot"))
                .willReturn(new VehicleDetails("Honda", "Civic"));
        HtmlPage page = this.webClient.getPage("/sboot/vehicle.html");
        assertThat(page.getBody().getTextContent()).isEqualTo("Honda Civic");
    }

}

@WebFluxTest annotation

This annotation is generally used to test controllers in WebFlux mode (all non blocking IO and support Reactive Streams). Generally, the @ WebFluxTest annotation is used to test requests in a single controller and is used in combination with @ MockBean; Adding this annotation to the test class will automatically configure the WebTestClient class bean. If the test class decorated with the @ SpringBootTest annotation wants to use the WebTestClient bean, it needs to add the @ AutoConfigureWebTestClient annotation

@The WebFluxTest annotation is used to test the class. The example usage on the official document is as follows:

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;

@WebFluxTest(UserVehicleController.class)
class MyControllerTests {

    @Autowired
    private WebTestClient webClient;

    @MockBean
    private UserVehicleService userVehicleService;

    @Test
    void testExample() throws Exception {
        given(this.userVehicleService.getVehicleDetails("sboot"))
                .willReturn(new VehicleDetails("Honda", "Civic"));
        this.webClient.get().uri("/sboot/vehicle").accept(MediaType.TEXT_PLAIN)
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class).isEqualTo("Honda Civic");
    }

}

Mockito and BDDMockito classes for Mock testing

Mockito class inherits from ArgumentMatchers class, and BDDMockito class inherits from mockito

Common methods in ArgumentMatchers class

  • Static < T > t any(): construct parameters of any type
  • Static < T > t any (class < T > type): construct objects of any type
  • Static < T > List < T > anylist(): construct any array

Important methods in Mockito class

  • STAITC < T > t mock (class < T > classtomock): simulates an object of a class. Adding a MockBean annotation to the injection attribute will call this method;
  • static MockingDetails mockingDetails(Object toInspect): mock a specific object;
  • Static < T > t spy (class < T > classtospy): simulate the object of the class. Adding SpyBean annotation to the injection attribute will call this method;
  • Static < T > ongoingsubsbing < T > when (t methodcall): simulate calling method;
  • Static stub dothrow (throwable... Tobethrown): simulate throwing exceptions;
  • Static stub docallrealmethod() simulates calling a real method;
  • Static stub double Answer (Answer answer): simulate the Answer. The Answer method in Answer sets the proxy method processor InvocationOnMock;
  • Public static stub doreturn (object tobereturned): simulates the returned object;

Important methods in BDDMockito class

  • static <T> BDDMockito. Bddmyongoingstubbing < T > given (t methodcall): simulate calling method;
  • public static <T> BDDMockito. Then < T > then (t mock): start the next simulation object;
  • static BDDMockito.BDDStubber willThrow(Throwable... toBeThrown): simulate throwing multiple exceptions;
  • static BDDMockito. Bddstubber willthrow (class <? Extends throwable > tobethrown): simulate throwing an exception;
  • static BDDMockito. Bddstubber willreturn (object to be returned): simulates the returned object;
  • static BDDMockito. Bddstubber willanswer (answer <? > answer): simulate the answer and set the agent execution method;
  • static BDDMockito.BDDStubber willCallRealMethod(): simulate and call real methods;

Looking at the source code of the above Mock implementation classes, we find that the implementation of Mock test uses bytecode instrumentation technology. When the Mock class executes the method, it is actually the executing proxy method. The Answer type parameter passed when the specific proxy method executes the static < T > t Mock (class < T > classtomock, Answer defaultanswer) method is specified; Returns is used when an Answer type parameter is not passed_ DEFAULTS

The source code of Answer interface is as follows:

public interface Answer<T> {
    T answer(InvocationOnMock var1) throws Throwable;
}

Its implementation class is Answers, which is an enumeration class. The source code is as follows:

public enum Answers implements Answer<Object> {
    // Specify the Answer implementation class as globalyconfiguredanswer
    RETURNS_DEFAULTS(new GloballyConfiguredAnswer()),
    // Specify the Answer implementation class as returnsmartnulls
    RETURNS_SMART_NULLS(new ReturnsSmartNulls()),
    // Specify the Answer implementation class as ReturnsMocks
    RETURNS_MOCKS(new ReturnsMocks()),
    // Specify the Answer implementation class as returnsdeepstubs
    RETURNS_DEEP_STUBS(new ReturnsDeepStubs()),
    // Specify the Answer implementation class as CallsRealMethods
    CALLS_REAL_METHODS(new CallsRealMethods()),
    // Specify the Answer implementation class as TriesToReturnSelf
    RETURNS_SELF(new TriesToReturnSelf());

    private final Answer<Object> implementation;

    private Answers(Answer<Object> implementation) {
        this.implementation = implementation;
    }

    /** @deprecated */
    @Deprecated
    public Answer<Object> get() {
        return this;
    }

    public Object answer(InvocationOnMock invocation) throws Throwable {
        return this.implementation.answer(invocation);
    }
}

The real answer method is executed by the answer implementation class specified in the enumeration value, such as globalyconfiguredanswer #answer method:

public Object answer(InvocationOnMock invocation) throws Throwable {
        return (new GlobalConfiguration()).getDefaultAnswer().answer(invocation);
    }

The key to completing the Mock test class lies in several spring 5 JUnit test annotations and the common methods in the Mock classes of Mockito and BDDMockito

Write at the end

Limited to the length of the article, this article only explains the usage of Mock test to complete Junit unit test in Spring 5 on the Spring official website. Combined with the source code, it explains how to construct some important parameters in the specific use process, and lists in detail the common methods and parameter meanings when completing Mock test. I believe that after reading the explanation of this article, there is no pressure to use Mock test to complete Junit5 unit test in SpringBoot project. About the specific test cases used and successfully run, the author will give them in the next article. Readers who are interested can also try the following first.

It's not easy to be original. All the partners here move your fingers and click to see it. Encourage the following authors to continue to write high-quality original content. Thank you!