一文搞懂Spring5 Mock测试

2022-03-04 16:23:21 浏览数 (1)

前言

很多时候我们开发人员测试接口时习惯使用postman去直接测,但是使用postman测试有个缺点就是只适合开发人员自己测试,不太方便团队共享,而且测试的时候很难覆盖到一个接口涉及到各个层面的逻辑分支方法。说到对代码逻辑的覆盖,这方面junit测试就有天然的优势。一般规范一点IT互联网公司都会要求提交的代码都要有测试用例,而且对测试用例的逻辑覆盖率有一定的要求,一般要求覆盖率70%以上。

在没有测试用例的情况下,一旦项目团队成员中有人离开团队,走之前也没有留下接口文档,那么新加入的并接手的员工维护起来就会比较蛋疼了,一个个接口的入参还得去页面上通过调试模式在Network界面一个一个地去找。如果一个接口的入参数量比较少还好说,一旦接口参数动不动就50个以上那种,源码一个接口上千行代码,而且涉及到调用第三方接口时,这时如果没有测试用例就真的非常难搞了。

笔者最近在公司就接手了一个叫友商旅的项目涉及到机票航班查询、机票下单等业务比较复杂的需求,而且这个需求不需要我从头开始开发,而是在已有的基础上修改,第一步要做的就是调通涉及机票相关的原有接口。因为没有测试用例,只能依靠阅读源码以及查看数据库字段备注信息等一步一步调试接口,效率可以说是相当的低。还好后面找到一份详细的接口文档资料才加快了进度。但通过这件事情也让我意识到开发的接口有完整的测试用例的好处。

本文的目的就是带大家学会在SpringBoot2.X项目中学会给自己开发的服务类和控制器类完成测试用例,方便项目维护的同时也可以满足一些公司对提交的代码必须有测试用例的要求。

spring-boot-starter-test模块简介

Spring Boot提供了一些工具类和注解用户帮助开发人员测试自己开发出来的功能模块。Spring Boot对Test的支持是两个模块提供的:包含核心项目的spring-boot-test和Test支持自动配置的spring-boot-test-autoconfigure

多数开发人员使用spring-boot-starter-test,它导入了spring-boot-test模块以及Jupiter, AssertJ, Hamcrest等有用的类库。

注意:spring-boot-starter-test起步依赖引入了vintage engine,所以可以同时跑Junit4Junit5测试。如果你已经把你的测试类升级到Junit5,那么你可以按下面这种方式在依赖中排除对Junit4的支持。

代码语言:javascript复制
<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>

Spring5测试类中几个重要的注解

@SpringBootTest

这个注解作用在你的测试类上,@SpringBootTest注解可以替代标准的Spring Test中的@ContextConfiguration,它的作用是在你的测试类中通过SpringBoot应用创建应用上下文(ApplicationContext

如果你使用的是Junit4,不要忘了在你的测试类上添加@RunWith(SpringRunner.class)注解;如果你使用的是Junit5,那么你无需添加等价的@ExtendWith(SpringExtension.class)注解。因为@SpringBootTest注解上已经添加了@ExtendWith(SpringExtension.class)注解。@SpringBootTest注解的源码如下:

代码语言:javascript复制
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class})
public @interface SpringBootTest {
    // 等价于properties属性,用于配置环境变量属性
    @AliasFor("properties")
    String[] value() default {};
    // 等价于value属性,用于配置环境变量属性
    @AliasFor("value")
    String[] properties() default {};
    // 测试类应用启动参数
    
    String[] args() default {};
    // 配置类
    Class<?>[] classes() default {};
    // 制定web环境,默认使用mock Web环境
    SpringBootTest.WebEnvironment webEnvironment() default SpringBootTest.WebEnvironment.MOCK;
    // web环境枚举
    public static enum WebEnvironment {
        //使用mock web环境 
        MOCK(false),
        // 分配随机端口web环境
        RANDOM_PORT(true),
        // 制定端口wen环境
        DEFINED_PORT(true),
        // 不使用web环境
        NONE(false);
        // 是否使用内嵌容器
        private final boolean embedded;

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

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

从源码里我们可以看到@SpringBootTest标注的测试类默认使用mock Web环境

默认情况下@SpringBootTest不会启动一个服务器,你可以使用@SpringBootTest注解中的webEnvironment属性自定义你的测试类如何启动

  • Mock(默认): 加载一个ApplcationContext并提供一个Mock Web环境。当你使用这个枚举值的时候内嵌的服务不会启动;如果你的类路径中没有Web应用环境,这种模式会创建一个非Web的ApplicationContext(应用上下文), 它可以和@AutoConfigureMockMvc@AutoConfigureWebTestClient两个注解在基于Mock的测试类中联合使用
  • RANDOM_PORT: 加载一个WebServerApplicationContext(Web服务应用上下文)并提供一个真实的Web环境,启动内嵌的Web容器(如tomcat或Jetty等)并监听随机分配的端口
  • 加载一个WebServerApplicationContext并提供一个真实的Web环境,启动内嵌的web容器并监听你在application.properties配置文件中定义好的端口,默认监听8080端口
  • NONE: 加载一个ApplicationContext并使用SpringApplication,但并不提供任何Web环境

注意:如果你的测试类上加上了@Transactional注解,默认情况下它会在每一个测试方法执行完之后回滚事务。然而如果你使用RANDOM_PORT或者DEFINED_PORT开启了真实的servlet web 环境,这种情况下http客户端和服务器运行在一个独立的线程中,这时候任何在test方法中执行完的事务在测试方法执行完之后都不会回滚

@MockBean@SpyBean注解

@MockBean注解一般作用在测试类中注入的bean属性上,它表示一个模拟的bean,其在官方文档上的用法如下:

代码语言:javascript复制
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");
    }

}

这个注解可以同时添加在测试类和测试类中的bean属性上,如若要测试使用真实的bean则使用@Autowired@Resource等自动装配注解

@SpyBean注解和@MockBean注解的作用类似,也是用来模拟一个bean;@SpyBean注解同样可以用在类和属性上,其官方文档上的用法如下:

代码语言:javascript复制
@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注解

这个注解加在测试类上用来自动装配MockMvc测试控制器的,在测试类上加上这个注解之后就可以在测试方法中通过@Autowired注解注入MockMvc实力bean了,官网上的demo用法如下:

代码语言:javascript复制
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"));
    }

}

MockMvc类中有个perform(RequestBuilder requestBuilder)方法,可以执行包括GET|POST|PUT|DELETE等类型http请求,该方法返回一个ResultActions类型对象。ResultActions是一个接口类,主要包括下面三个抽象方法分别表示执行期待匹配、进一步的动作以及返回结果等

代码语言:javascript复制
public interface ResultActions {
    ResultActions andExpect(ResultMatcher var1) throws Exception;

    ResultActions andDo(ResultHandler var1) throws Exception;

    MvcResult andReturn();
}

ResultActions接口类的实现类在MockMvc#perform方法中得到实现,这里笔者附上这部分源码:

代码语言:javascript复制
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;
            }
        };

MockMvc#perform方法中的RequestBuilder类型参数可通过抽象类MockMvcRequestBuilders中的静态方法构造,返回的是RequestBuilder的实现类MockHttpServletRequestBuilder对象。

MockHttpServletRequestBuilder类中的几个重要的构造方法如下:

代码语言:javascript复制
/**
* 通过url模板参数与url中占位符参数变量构造一个GET类型请求
* @param urlTemplate url模板,示例:/contextPath/path?ket1={key1}&key2={key2}
* @param uriVars 参数,示例:[key1,key2]
*/
public static MockHttpServletRequestBuilder get(String urlTemplate, Object... uriVars) {
        return new MockHttpServletRequestBuilder(HttpMethod.GET, urlTemplate, uriVars);
    }

    /**
    * 直接通过URI构造一个GET类型请求
    * @param URI 请求路径url的包装对象,可通过new URI(url)直接构造,url必须符合http或https协议请求路径语法
    */
    public static MockHttpServletRequestBuilder get(URI uri) {
        return new MockHttpServletRequestBuilder(HttpMethod.GET, uri);
    }

    /**
    * 通过url模板与参数参数构造POST类型请求
    * @param urlTemplate url模板,示例同GET请求中相同入参方法
    * @param uriVars url中占位符参数变量
    */
    public static MockHttpServletRequestBuilder post(String urlTemplate, Object... uriVars) {
        return new MockHttpServletRequestBuilder(HttpMethod.POST, urlTemplate, uriVars);
    }

    /**
    * 通过URI参数直接构造POST类型请求
    * param uri 请求路径URI类型参数
    */
    public static MockHttpServletRequestBuilder post(URI uri) {
        return new MockHttpServletRequestBuilder(HttpMethod.POST, uri);
    }

    /**
    * 通过url模板与查询参数变量构造PUT类型请求
    * @param urlTemplate url模板
    * @param uriVars url模板占位符参数变量
    */
    public static MockHttpServletRequestBuilder put(String urlTemplate, Object... uriVars) {
        return new MockHttpServletRequestBuilder(HttpMethod.PUT, urlTemplate, uriVars);
    }
    
    /**
    * 通过URI参数构造一个PUT类型请求
    * @param uri url包装类型URI参数
    */
    public static MockHttpServletRequestBuilder put(URI uri) {
        return new MockHttpServletRequestBuilder(HttpMethod.PUT, uri);
    }
    
    /**
    * 通过url模板和占位符参数变量构造一个DELETE类型请求
    * @param urlTemplate 请求url模板
    */
    public static MockHttpServletRequestBuilder delete(String urlTemplate, Object... uriVars) {
        return new MockHttpServletRequestBuilder(HttpMethod.DELETE, urlTemplate, uriVars);
    }

    /**
    * 通过请求url包装类URI类型参数构造一个DELETE类型请求
    * @param uri 请求url包装类URI类型参数
    */
    public static MockHttpServletRequestBuilder delete(URI uri) {
        return new MockHttpServletRequestBuilder(HttpMethod.DELETE, uri);
    }
    
    /**
    * 通过url模板和占位符参数变量构造一个OPTIONS类型请求
    * @param urlTemplate url模板参数
    * @param uriVars url模板参数中的占位符变量参数
    */
    public static MockHttpServletRequestBuilder options(String urlTemplate, Object... uriVars) {
        return new MockHttpServletRequestBuilder(HttpMethod.OPTIONS, urlTemplate, uriVars);
    }
    
    /**
    * 通过uri参数直接构造一个OPTIONS类型参数
    * @param URI类型参数
    */
    public static MockHttpServletRequestBuilder options(URI uri) {
        return new MockHttpServletRequestBuilder(HttpMethod.OPTIONS, uri);
    }
    
    /**
    * 通过请求类型参数、url模板参数和占位符变量参数构造一个指定类型的请求
    * @param method Http请求类型(枚举值)
    * @param urlTemplate url模板
    * @param uriVars 占位符变量
    */
    public static MockHttpServletRequestBuilder request(HttpMethod method, String urlTemplate, Object... uriVars) {
        return new MockHttpServletRequestBuilder(method, urlTemplate, uriVars);
    }
    
    /**
    * 通过请求类型参数和uri参数构造一个指定类型的请求
    * @param httpMethod http请求类型(枚举值)
    * @param uri 请求路径包装类URI类型参数
    */
    public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, URI uri) {
        return new MockHttpServletRequestBuilder(httpMethod, uri);
    }
    
    /**
    * 通过请求类型和uri参数构造一个指定类型的请求
    * @param httpMethod 请求类型,示例:GET|POST|PUT|DELETE
    */
    public static MockHttpServletRequestBuilder request(String httpMethod, URI uri) {
        return new MockHttpServletRequestBuilder(httpMethod, uri);
    }
    
    /**
    * 通过url模板参数和占位符参数构造一个文件上传请求
    * @param urlTemplate
    * @param uriVars 
    */
    public static MockMultipartHttpServletRequestBuilder multipart(String urlTemplate, Object... uriVars) {
        return new MockMultipartHttpServletRequestBuilder(urlTemplate, uriVars);
    }
    
    /**
    * 直接通过uri参数构造一个文件上传请求
    */
    public static MockMultipartHttpServletRequestBuilder multipart(URI uri) {
        return new MockMultipartHttpServletRequestBuilder(uri);
    }

@WebMvcTest注解

这个注解作用在测试类上用于测试单个的控制器类,一般和MockMvc一起使用,其在官方文档上的用法如下:

代码语言:javascript复制
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注解

这个注解一般用于测试WebFlux模式下(全部非阻塞IO、支持 Reactive Streams)的控制器,通常@WebFluxTest注解用于测试单个控制器中的请求并与@MockBean联合使用;测试类上加上这个注解就会自动配置WebTestClient类bean, 而如果使用@SpringBootTest注解装饰的测试类要使用WebTestClient的bean时则需要加上@AutoConfigureWebTestClient注解

@WebFluxTest注解用于测试类在官方文档上的示例用法如下:

代码语言:javascript复制
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");
    }

}

用于Mock测试的MockitoBDDMockito

Mockito类继承自ArgumentMatchers类,BDDMockito类又继承自Mockito

ArgumentMatchers类中的常用方法

  • static <T> T any(): 构造任意类型的参数
  • static <T> T any(Class<T> type): 构造任意类型对象
  • static <T> List<T> anyList(): 构造任意数组

Mockito类中的重要方法

  • staitc <T> T mock(Class<T> classToMock): 模拟一个类的对象, 注入属性上加入MockBean注解会调用此方法;
  • static MockingDetails mockingDetails(Object toInspect): mock一个具体对象;
  • static <T> T spy(Class<T> classToSpy):模拟类的对象,注入属性上加入SpyBean注解会调用此方法;
  • static <T> OngoingStubbing<T> when(T methodCall): 模拟调用方法;
  • static Stubber doThrow(Throwable... toBeThrown): 模拟抛异常;
  • static Stubber doCallRealMethod()模拟调用真实方法;
  • static Stubber doAnswer(Answer answer):模拟回答, Answer中的answer方法设置了代理方法处理器InvocationOnMock
  • public static Stubber doReturn(Object toBeReturned): 模拟返回对象;

BDDMockito类中的重要方法

  • static <T> BDDMockito.BDDMyOngoingStubbing<T> given(T methodCall): 模拟调用方法;
  • public static <T> BDDMockito.Then<T> then(T mock): 开始下一个模拟对象;
  • static BDDMockito.BDDStubber willThrow(Throwable... toBeThrown): 模拟抛多个异常;
  • static BDDMockito.BDDStubber willThrow(Class<? extends Throwable> toBeThrown): 模拟抛一个异常;
  • static BDDMockito.BDDStubber willReturn(Object toBeReturned): 模拟返回对象;
  • static BDDMockito.BDDStubber willAnswer(Answer<?> answer): 模拟回答,设置代理执行方法;
  • static BDDMockito.BDDStubber willCallRealMethod():模拟调用真实方法;

查看以上几个Mock实现类的源码,我们发现Mock测试的实现使用了字节码插桩技术,Mock类执行方法时实际上是执行的代理方法,具体代理方法的执行 static <T> T mock(Class<T> classToMock, Answer defaultAnswer)方法时传递的Answer类型参数指定;不传递Answer类型参数时使用RETURNS_DEFAULTS

Answer接口的源码如下:

代码语言:javascript复制
public interface Answer<T> {
    T answer(InvocationOnMock var1) throws Throwable;
}

它的实现类为Answers,是一个枚举类,源码如下:

代码语言:javascript复制
public enum Answers implements Answer<Object> {
    // 指定Answer实现类为GloballyConfiguredAnswer
    RETURNS_DEFAULTS(new GloballyConfiguredAnswer()),
    // 指定Answer实现类为ReturnsSmartNulls
    RETURNS_SMART_NULLS(new ReturnsSmartNulls()),
    // 指定Answer实现类为ReturnsMocks
    RETURNS_MOCKS(new ReturnsMocks()),
    // 指定Answer实现类为ReturnsDeepStubs(深度插桩)
    RETURNS_DEEP_STUBS(new ReturnsDeepStubs()),
    // 指定Answer实现类为CallsRealMethods
    CALLS_REAL_METHODS(new CallsRealMethods()),
    // 指定Answer实现类为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);
    }
}

真正的answer方法由枚举值中具体指定的Answer实现类执行,如GloballyConfiguredAnswer#answer方法:

代码语言:javascript复制
public Object answer(InvocationOnMock invocation) throws Throwable {
        return (new GlobalConfiguration()).getDefaultAnswer().answer(invocation);
    }

完成Mock测试类的关键就在于几个Spring5 Junit测试注解以及MockitoBDDMockito两个Mock类中的常用方法

写在最后

限于文章篇幅,本文就只讲解了Spring官网中关于Spring5 中Mock测试完成Junit单元测试的用法。并结合源码讲了具体使用过程中一些重要参数如何构造,并详细列出了完成Mock测试时常用方法及参数含义。相信看完本文的讲解后,对于在SpringBoot项目中使用Mock测试完成Junit5单元测试已经毫无压力了。关于具体的使用并成功跑起来的测试用例笔者会在下一篇文章中给出,读者有兴趣也可自己先尝试以下。

原创不易,看到这里的小伙伴们都动动你们的手指点个在看吧,鼓励以下笔者继续写出优质的原创内容,谢谢!

0 人点赞