0


SpringBoot Test详解

目录

spring-boot-starter-test

1、概述

SpringBoot对单元测试的支持在于提供了一系列注解和工具的集成,它们是通过两个项目提供的:

  • spring-boot-test项目:包含核心功能
  • spring-boot-test-autoconfigure项目:支持自动配置

通常情况下,我们通过spring-boot-starter-test的Starter来引入SpringBoot的核心支持项目以及单元测试项目以及单元测试库。

spring-boot-starter-test包含的类库如下:

  • JUnit:一个Java语言的单元测试框架
  • Spring Test & Spring Boot Test:为SpringBoot应用提供集成测试和工具支持
  • AssertJ::支持流式断言的Java测试框架
  • Hamcrest:一个匹配器库
  • Mockito:一个Java Mock框架
  • JSONassert:一个针对JSON的断言库
  • JsonPath:一个JSON XPath库

如果SpringBoot提供的基础类无法满足业务需求,我们也可以自行添加依赖。依赖注入的优点之一就是可以轻松使用单元测试。这种方式可以直接通过new来创建对象,而不需要涉及Spring。当然,也可以通过模拟对象来替换真实依赖。

如果需要集成测试,比如使用Spring的ApplicationContext,Spring同样能够提供无须部署应用程序或连接到其它基础环境的集成测试。而SpringBoot应用本身就是一个ApplicationContext,因此除了正常使用Spring上下文进行测试,无须执行其它操作。

Maven依赖:

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

2、常用注解

从功能上讲,Spring Boot Test中的注解主要分如下几类:
类别示例说明配置类型@TestConfiguration等提供一些测试相关的配置入口mock类型@MockBean等提供mock支持启动测试类型@SpringBootTest等以Test结尾的注解,具有加载applicationContext的能力自动配置类型@AutoConfigureJdbc等以AutoConfigure开头的注解,具有加载测试支持功能的能力

2.1、配置类型的注解

注解作用实践中的使用**@TestComponent**该注解为另一种

@Component

,在语义上用来指定某个Bean是专门用于测试的该注解适用与测试代码和正式混合在一起时,不加载被该注解描述的Bean,使用不多**@TestConfiguration**该注解是另一种

@TestComponent

,它用于补充额外的Bean或覆盖已存在的Bean在不修改正式代码的前提下,使配置更加灵活**@TypeExcludeFilters**用来排除

@TestConfiguration

@TestComponent

适用于测试代码和正式代码混合的场景,使用不多**@OverrideAutoConfiguration**可用于覆盖

@EnableAutoCOnfiguration

,与

ImportAutoConfiguration

结合使用,以限制所加载的自动配置类在不修改正式代码的前提下,提供了修改配置自动配置类的能力**@PropertyMapping**定义

@AutoConfigure

注解中用到的变量名称,例如在

@AutoConfigureMockMvc

中定义名为

spring.test.mockmvc.webclient.enabled

的变量一般不使用

使用@SpringBootApplication启动测试或者生产代码,被@TestComponent描述的Bean会自动被排除掉。如果不是则需要向@SpringBootApplication添加TypeExcludeFilter。

2.2、Mock类型的注解

注解作用MockBean用于Mock指定的class或被注解的属性MockBeans使@MockBean支持在同一类型或属性上多次出现**@SpyBean用于spy指定的class或被注解的属性@SpyBeans**使@SpyBeans支持在同一类型或属性上次多次出现

@MockBean和@SpyBean这两个注解,在mockito框架中本来已经存在,且功能基本相同。Spring Boot Test又定义一份重复的注解,目的在于使MockBean和SpyBean被ApplicationContext管理,从而方便使用。

MockBean和SpyBean功能非常相似,都能模拟方法的各种行为。不同之处在于MockBean是全新的对象,跟正式对象没有关系;而SpyBean与正式对象紧密联系,可以模拟正式对象的部分方法,没有被模拟的方法仍然可以运行正式代码。

2.3、自动配置类型的注解

注解作用**@AutoConfigureJdbc自动配置JDBC@AutoConfigureCache自动配置缓存@AutoConfigureDataLdap自动配置LDAP@AutoConfigureJson自动配置JSON@AutoConfigureJsonTesters自动配置JsonTester@AutoConfigureDataJpa自动配置JPA@AutoConfigureTestEntityManager自动配置TestEntityManager@AutoConfigureRestDocs自动配置Rest Docs@AutoConfigureMockRestServiceServer自动配置MockRestServiceServer@AutoConfigureWebClient自动配置WebClient@AutoConfigureWebFlux自动配置WebFlux@AutoConfigureWebTestClient自动配置WebTestClient@AutoConfigureMockMvc自动配置MockMvc@AutoConfigureWebMvc自动配置WebMvc@AutoConfigureDataNeo4j自动配置Neo4j@AutoConfigureDataRedis自动配置Redis@AutoConfigureJooq自动配置Jooq@AutoCOnfigureTestDatabase**自动Test Database,可以使用内存数据库

这些注解可以搭配@Test使用,用于开启在@Test中未自动配置的功能。例如@SpringBootTest和@AutoConfigureMockMvc组合后,就可以注入org.springframework.test.web.servlet.MockMvc。

自动配置类型有两种使用方式:

  1. 在功能测试(即使用@SpringBootTest)时显示添加。
  2. 一般在切片测试中被隐式使用,例如@WebMvcTest注解时,隐式添加了@AutoConfigureCache、@AutoConfigureWebMvc和@AutoConfigureMockMvc。

2.4、启动测试类型的注解

所有的

@*Test

注解都被

@BootstrapWith

注解,它们可以启动ApplicationContext,是测试的入口,所有的测试类必须声明一个

@*Test

注解。
注解作用**@SpringBootTest自动侦测并加载@SpringBootApplication或@SpringBootConfiguration中的配置,默认web环境为Mock,不见听任务端口@DataRedisTest测试对Redis操作,自动扫描被@RedisHash描述的类,并配置Spring Data Redis的库@DataJpaTest测试基于JPA的数据库操作,同时提供了TestEntityManager替代JPA的EntityManager@DataJdbcTest测试基于Spring Data JDBC的数据库操作@JsonTest测试JSON的序列化和反序列化@WebMvcTest测试Spring MVC中的Controllers@WebFluxTest测试Spring WebFlux中的Controllers@RestClientTest测试对REST客户端的操作@DataLdapTest测试对LDAP的操作@DataMongoTest测试对MongoDB的操作@DataNeo4jTest**测试对Neo4j的操作
除了@SpringBootTest之外的注解都是用来进行切面测试的,他们会默认导入一些自动配置,点击查看官方文档。

一般情况,推荐使用@SpringBootTest而非其它切片测试的注解,简单有效。若某次改动仅涉及特定切片,可以考虑使用切片测试。SpringBootTest是这些注解中最常用的一个,其中包含的配置项如下:

  • value:指定配置属性
  • properties:指定配置属性,和value意义相同
  • classes:指定配置类,等同于@ContextConfiguration中的class,若没有显示指定,将查找嵌套的@Configuration类,然后返回到SpringBootConfiguration搜索配置
  • webEnviroment:指定web环境,可选值如下: - MOCK:此值为默认值,该类型提供一个mock环境,此时内嵌的服务(servlet容器)并没有真正启动,也不会监听web端口- RANDOM_PORT:启动一个真实的web服务,监听一个随机端口- DEFINED_PORT:启动一个真实的web服务,监听一个定义好的端口(从配置中读取)- NONE:启动一个非web的ApplicationContext,既不提供mock环境,也不提供真实的web服务

2.5、相似注解的区别和联系

  • @TestComment和@Comment:@TestComment是另一种@Component,在语义上用来指定某个Bean是专门用于测试的。使用@SpringBootApplication服务时,@TestComponent会被自动排除
  • @TestConfiguration和@Configuration:@TestConfiguration是Spring Boot Boot Test提供的,@Configuration是Spring Framework提供的。@TestConfiguration实际上是也是一种@TestComponent,只是这个@TestComponent专门用来做配置用。@TestConfiguration和@Configuration不同,它不会阻止@SpringBootTest的查找机制,相当于是对既有配置的补充或覆盖。
  • **@SpringBootTest和@WebMvcTest(或@*Test)**:都可以启动Spring的ApplicationContext @SpringBootTest自动侦测并加载@SpringBootApplication或@SpringBootConfiguration中的配置,@WebMvcTest不侦测配置,只是默认加载一些自动配置。@SpringBootTest测试范围一般比@WebMvcTest大。
  • @MockBean和@SpyBean:都能模拟方法的各种行为。不同之处在于MockBean是全新的对象,跟正式对象没有关系;而SpyBean与正式对象紧密联系,可以模拟正式对象的部分方法,没有被模拟的方法仍然可以运行正式代码。

3、SpringBootTest和Junit的使用

整体上,Spring Boot Test支持的测试种类,大致可以分为如下三类:

  1. 单元测试:一般面向方法,编写一般业务代码时,测试成本较大。涉及到的注解有@Test。
  2. 切片测试:一般面向于测试的边界功能,介于单元测试和功能测试之间。涉及到的注解有@WebMvcTest等。主要就是对于Controller的测试,分离了Service层,这里就涉及到Mock控制层所依赖的组件了。
  3. 功能测试:一般面向某个完整的业务功能,同时也可以使用切面测试中mock能力,推荐使用。涉及到的注解有@SpringBootTest等。

3.1、单元测试

默认无参数的@SpringBootTest 注解会加载一个Web Application Context并提供Mock Web Environment,但是不会启动内置的server。这点从日志中没有打印Tomcat started on port(s)可以佐证。

@SpringBootTestpublicclassAppTest{@AutowiredUserMapper userMapper;@Testpublicvoidtest(){User user =newUser();
        user.setName("tom");
        user.setAge(18);
        user.setHeight(1.88);Assertions.assertThat(userMapper.add(user)).isEqualTo(1);}}

3.2、集成测试

//指定@SpringBootTest的Web Environment为RANDOM_PORT//此时,将会加载ApplicationContext,并启动Server,Server监听在随机端口上。//在测试类中通过@LocalServerPort获取该端口值@SpringBootTest(webEnvironment =SpringBootTest.WebEnvironment.RANDOM_PORT)publicclassDemoTest{@LocalServerPortprivateInteger port;@Test@DisplayName("should access application")publicvoidshouldAccessApplication(){Assertions.assertThat(port).isGreaterThan(1024);}}

也可以通过指定@SpringBootTest的Web Environment为DEFINED_PORT 来指定server侦听应用程序配置的端口,默认为8080。不过这种指定端口的方式很少使用,因为如果本地同时启动应用时,会导致端口冲突。

4、MockMvc

MockMvc可以做到不启动项目工程就可以对结构进行测试。MockMvc实现了对HTTP请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,同时提供了一套验证的工具,使得请求的验证同一而且方便。

4.1、简单示例

创建一个简单的TestController,提供一个方法,返回一个字符串:

@RestControllerpublicclassTestController{@RequestMapping("/mock")publicStringmock(String name){return"Hello "+ name +"!";}}

单元测试:

@SpringBootTest@AutoConfigureMockMvcclassTestControllerTest{@AutowiredprivateMockMvc mockMvc;@Testvoidmock()throwsException{//mockMvc.perform执行一个请求
        mockMvc.perform(MockMvcRequestBuilders//构造请求.get("/mock")//设置返回值类型.accept(MediaType.APPLICATION_JSON)//添加请求参数.param("name","tom"))//添加执行完成后的断言.andExpect(MockMvcResultMatchers.status().isOk()).andExpect(MockMvcResultMatchers.content().string("Hello tom!"))//添加一个结果处理器,此处打印整个响应结果信息.andDo(MockMvcResultHandlers.print());}}

运行测试输出:

MockHttpServletRequest:
      HTTP Method= GET
      Request URI =/mock
       Parameters={name=[tom]}Headers=[Accept:"application/json"]Body=nullSessionAttrs={}Handler:Type=pers.zhang.controller.TestControllerMethod=pers.zhang.controller.TestController#mock(String)Async:Async started =falseAsync result =nullResolvedException:Type=nullModelAndView:View name =nullView=nullModel=nullFlashMap:Attributes=nullMockHttpServletResponse:Status=200Error message =nullHeaders=[Content-Type:"application/json",Content-Length:"10"]Content type = application/json
             Body=Hello tom!Forwarded URL =nullRedirected URL =nullCookies=[]

@AutoConfigureMockMvc注解提供了自动配置MockMvc的功能。@Autowired注入MockMvc对象。

MockMvc对象可以通过接口M哦查看Mv吃Builder的实现类获得。该接口提供一个唯一的build方法来构造MockMvc。主要有两个实现类:

  • StandaloneMockMvcBuilder:独立安装
  • DefaultMockMvcBuilder:集成Web环境测试(并不会真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)

MockMvcBuilders提供了对应的standaloneSetup和webAppContextSetup两种创建方法,在使用时直接调用即可,默认使用DefaultMOckMvcBuilder。

整个单元测试包含一下步骤:

  1. 准备测试环境
  2. 执行MockMvc请求
  3. 添加验证断言
  4. 添加结果处理器
  5. 得到MvcResult进行自定义断言/进行下一步的异步请求
  6. 卸载测试环境

4.2、自动配置

@AutoConfigureMockMvc提供了自动配置MockMvc的功能,源码如下:

@Target({ElementType.TYPE,ElementType.METHOD })@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@ImportAutoConfiguration@PropertyMapping("spring.test.mockmvc")public@interfaceAutoConfigureMockMvc{//是否应向MockMvc注册来自应用程序上下文的filter,默认truebooleanaddFilters()defaulttrue;//每次MockMvc调用后应如何打印MvcResult信息@PropertyMapping(skip =SkipPropertyMapping.ON_DEFAULT_VALUE)MockMvcPrintprint()defaultMockMvcPrint.DEFAULT;//如果MvcResult仅在测试失败时才打印信息。默认true,则表示只在失败时打印booleanprintOnlyOnFailure()defaulttrue;//当HtmlUnit在类路径上时,是否应该自动配置WebClient。默认为true@PropertyMapping("webclient.enabled")booleanwebClientEnabled()defaulttrue;//当Selenium位于类路径上时,是否应自动配置WebDriver。默认为true@PropertyMapping("webdriver.enabled")booleanwebDriverEnabled()defaulttrue;}

在AutoConfigureMockMvc的源码中,我们重点看它组合的

@ImportAutoConfiguration

注解。该注解同样是SpringBoot自动配置项目提供的,其功能类似

@EnableAutoConfiguration

,但又略有区别。@ImportAutoConfiguration同样用于导入自动配置类,不仅可以像@EnableAutoConfiguration那样排除指定的自动配置配置类,还可以指定使用哪些自动配置类,这是它们之间的重要区别之一。

另外,@ImportAutoConfiguration使用的排序规则与@EnableAutoConfiguration的相同,通常情况下,建议优先使用@EnableAutoConfiguration注解进行自动配置。但在单元测试中,则可考虑优先使用@ImportAutoCOnfiguration。源码如下:

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@Import(ImportAutoConfigurationImportSelector.class)public@interfaceImportAutoConfiguration{//指定引入的自动配置类@AliasFor("classes")Class<?>[]value()default{};//指定引入的自动配置类。如果为空,则使用META-INF/spring.factories中注册的指定类//其中spring.factories中注册的key为被该注解的类的全限定名称@AliasFor("value")Class<?>[]classes()default{};//排除指定自动配置类Class<?>[]exclude()default{};}

通过value属性,提供了指定自动配置类的功能,可以通过细粒度控制,根据需要引入相应功能的自动配置。没有@EnableAutoConfiguration一次注入全局生效的特性,但是有了指定的灵活性。

更值得注意的是classes属性,它也是用来指定自动配置类的,但它的特殊之处在于,如果未进行指定,则会默认搜索项目META-INF/spring.factories文件中注册的类,但是它
搜索的注册类在spring.factories中的key是被@ImportAutoConfiguration注解的类的全限
定名称。显然,这里的key为org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc。以上功能也就解释了为什么在单元测试中更多的是使用@ImportAutoConfiguration注解来进行自动配置了。

在spring-boot-test-autoconfigure项目的spring.factories文件中的相关配置如下:

# AutoConfigureMockMvc auto-configuration imports
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc=\
org.springframework.boot.test.autoconfigure.web.servlet.MockMvcAutoConfiguration,\
org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebClientAutoConfiguration,\
org.springframework.boot.test.autoconfigure.web.servlet.MockMvcWebDriverAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.test.autoconfigure.web.servlet.MockMvcSecurityConfiguration

也就是说,当使用@ImportAutoConfiguration注解,并未指定classes属性值时,默认自动配置上述自动配置类。

使用@AutoConfigureMockMvc注解会导入MockMvcAutoConfiguration自动配置类,该类就是专门为MockMvc相关功能提供自动配置的。

@Configuration(proxyBeanMethods =false)@ConditionalOnWebApplication(type =Type.SERVLET)@AutoConfigureAfter(WebMvcAutoConfiguration.class)@EnableConfigurationProperties({ServerProperties.class,WebMvcProperties.class})publicclassMockMvcAutoConfiguration{privatefinalWebApplicationContext context;privatefinalWebMvcProperties webMvcProperties;MockMvcAutoConfiguration(WebApplicationContext context,WebMvcProperties webMvcProperties){this.context = context;this.webMvcProperties = webMvcProperties;}....}

注解部分说明,MockMvcAutoConfiguration需要在Web应用程序类型为Servlet,且在WebMvcAutoConfiguration自动配置之后进行自动配置。

另外,通过@EnableConfigurationProperties导入了ServerProperties和WebMvcProperties两个配置属性类,并通过构造方法设置为成员变量。

4.3、使用方式

1、测试逻辑
  1. MockMvcBuilder构造MockMvc的构造器
  2. mockMvc调用perform,执行一个RequestBuilder请求,调用Controller的业务处理逻辑
  3. perform返回ResultActions,返回操作结果,通过ResultActions,提供了统一的验证方式
  4. 使用StatusResultMatchers对请求结果进行验证
  5. 使用ContentResultMatchers对请求返回的内容进行验证
2、MockMvcBuilder
MockMvc

是spring测试下的一个非常好用的类,他们的初始化需要在setUp中进行。

MockMvcBuilder

是用来构造

MockMvc

的构造器,其主要有两个实现:

StandaloneMockMvcBuilder

DefaultMockMvcBuilder

,前者继承了后者。

  1. MockMvcBuilders.webAppContextSetup(WebApplicationContext context):指定WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的MockMvc
  2. MockMvcBuilders.standaloneSetup(Object... controllers):通过参数指定一组控制器,这样就不需要从上下文获取了,比如this.mockMvc = MockMvcBuilders.standaloneSetup(this.controller).build();这些Builder还提供了其他api,可以自行百度
3、MockMvcRequestBuilders

从名字可以看出,RequestBuilder用来构建请求的,其提供了一个方法

buildRequest(ServletContext servletContext)

用于构建MockHttpServletRequest;其主要有两个子类

MockHttpServletRequestBuilder

MockMultipartHttpServletRequestBuilder

(如文件上传使用),即用来Mock客户端请求需要的所有数据。

常用API:

  • MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables):根据uri模板和uri变量值得到一个GET请求方式的RequestBuilder,如果在controller的方法中method选择的是RequestMethod.GET,那在controllerTest中对应就要使用MockMvcRequestBuilders.get
  • post(String urlTemplate, Object... urlVariables):同get类似,但是是POST方法
  • put(String urlTemplate, Object... urlVariables):同get类似,但是是PUT方法
  • delete(String urlTemplate, Object... urlVariables) :同get类似,但是是DELETE方法
  • options(String urlTemplate, Object... urlVariables):同get类似,但是是OPTIONS方法
4、ResultActions

调用

MockMvc.perform(RequestBuilder requestBuilder)

后将得到

ResultActions

,对ResultActions有以下三种处理:

  • ResultActions.andExpect:添加执行完成后的断言。添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确
  • ResultActions.andDo:添加一个结果处理器,比如此处使用.andDo(MockMvcResultHandlers.print())输出整个响应结果信息,可以在调试的时候使用
  • ResultActions.andReturn:表示执行完成后返回相应的结果

ResultHandler用于对处理的结果进行相应处理的,比如输出整个请求/响应等信息方便调试,Spring mvc测试框架提供了

MockMvcResultHandlers

静态工厂方法,该工厂提供了

ResultHandler print()

返回一个输出MvcResult详细信息到控制台的ResultHandler实现。

使用Content-type来指定不同格式的请求信息:

ALL =newMediaType("*","*");
APPLICATION_ATOM_XML =newMediaType("application","atom+xml");
APPLICATION_CBOR =newMediaType("application","cbor");
APPLICATION_FORM_URLENCODED =newMediaType("application","x-www-form-urlencoded");
APPLICATION_JSON =newMediaType("application","json");
APPLICATION_JSON_UTF8 =newMediaType("application","json",StandardCharsets.UTF_8);
APPLICATION_NDJSON =newMediaType("application","x-ndjson");
APPLICATION_OCTET_STREAM =newMediaType("application","octet-stream");
APPLICATION_PDF =newMediaType("application","pdf");
APPLICATION_PROBLEM_JSON =newMediaType("application","problem+json");
APPLICATION_PROBLEM_JSON_UTF8 =newMediaType("application","problem+json",StandardCharsets.UTF_8);
APPLICATION_PROBLEM_XML =newMediaType("application","problem+xml");
APPLICATION_RSS_XML =newMediaType("application","rss+xml");
APPLICATION_STREAM_JSON =newMediaType("application","stream+json");
APPLICATION_XHTML_XML =newMediaType("application","xhtml+xml");
APPLICATION_XML =newMediaType("application","xml");
IMAGE_GIF =newMediaType("image","gif");
IMAGE_JPEG =newMediaType("image","jpeg");
IMAGE_PNG =newMediaType("image","png");
MULTIPART_FORM_DATA =newMediaType("multipart","form-data");
MULTIPART_MIXED =newMediaType("multipart","mixed");
MULTIPART_RELATED =newMediaType("multipart","related");
TEXT_EVENT_STREAM =newMediaType("text","event-stream");
TEXT_HTML =newMediaType("text","html");
TEXT_MARKDOWN =newMediaType("text","markdown");
TEXT_PLAIN =newMediaType("text","plain");
TEXT_XML =newMediaType("text","xml");
5、ResultMatchers
ResultMatcher

用来匹配执行完请求后的结果验证,其就一个

match(MvcResult result)

断言方法,如果匹配失败将抛出相应的异常,spring mvc测试框架提供了很多

***ResultMatchers

来满足测试需求。

MockMvcResultMatchers

类提供了许多静态方法,提供了多种匹配器:

  • **request()**:返回RequestResultMatchers,访问与请求相关的断言 - asyncStarted:断言异步处理开始- asyncNotStarted:断言异步不开始- asyncResult:断言使用给定匹配器进行异步处理的结果- attribute:用于断言请求属性值- sessionAttribute:用于断言Session会话属性值- sessionAttributeDoesNotExist:断言Session会话属性不存在
  • **handler()**:返回HandlerResultMatchers,对处理请求的处理程序的断言的访问 - handlerType:断言处理请求的处理程序的类型- methodCall:断言用于处理请求的控制器方法- methodName:断言用于处理请求的控制器方法的名称- method:断言用于处理请求的控制器方法
  • **model()**:ModelResultMatchers,访问与模型相关的断言 - attribute:断言一个模型属性值- attributeExists:断言一个模型属性存在- attributeDoesNotExist:断言一个模型属性不存在- attributeErrorCount:断言给定的模型属性有指定个数的错误- attributeHasErrors:断言给定的模型属性有错误- attributeHasNoErrors:断言给定的模型属性没有错误- attributeHasFieldErrors:断言给定的模型属性字段有错误- attributeHasFieldErrorCode:使用精确字符串匹配断言模型属性的字段错误代码- errorCount:断言模型中的错误总数- hasErrors:断言模型中有错误- hasNoErrors:断言模型中没有错误- size:断言模型属性的数量
  • **view()**:返回ViewResultMatchers,访问所选视图上的断言 - name:断言视图名
  • **flash()**:返回FlashAttributeResultMatchers,访问flash属性断言 - attribute:断言flash属性的值- attributeExists:断言给定的flash属性是否存在- attributeCount:断言flash属性的数量
  • **forwardedUrl(@Nullable String expectedUrl)**:断言请求被转发到给定的URL
  • **forwardedUrlTemplate(String urlTemplate, Object… uriVars)**:断言请求被转发到给定的URL模板
  • **forwardedUrlPattern(String urlPattern)**:断言请求被转发到给定的URL
  • **redirectedUrl(String expectedUrl)**:断言请求被重定向到给定的URL
  • **redirectedUrlTemplate(String urlTemplate, Object… uriVars)**:断言请求被重定向到给定的URL模板
  • **redirectedUrlPattern(String urlPattern)**:断言请求被重定向到给定的URL
  • **status()**:返回StatusResultMatchers,访问响应状态断言 - is:断言响应状态码- is1xxInformational:断言响应状态码在1xx范围内- is2xxSuccessful:断言响应状态码在2xx范围内- is3xxRedirection:断言响应状态码在3xx范围内- is4xxClientError:断言响应状态码在4xx范围内- is5xxServerError:断言响应状态码在5xx范围内- reason:断言Servlet响应错误消息- isContinue:响应状态码是100- isSwitchingProtocols:响应状态码是101- isProcessing:响应状态码是102- isCheckpoint:响应状态码是103- isOk:响应状态码是200- isCreated:响应状态码是201- isAccepted:响应状态码是202- isNonAuthoritativeInformation:响应状态码是203- isNoContent:响应状态码是204- isResetContent:响应状态码是205- isPartialContent:响应状态码是206- isMultiStatus:响应状态码是207- isAlreadyReported:响应状态码是208- isImUsed:响应状态码是226- isMultipleChoices:响应状态码是300- isMovedPermanently:响应状态码是301- isFound:响应状态码是302- isSeeOther:响应状态码是303- isNotModified:响应状态码是304- isUseProxy:响应状态码是305- isTemporaryRedirect:响应状态码是307- isPermanentRedirect:响应状态码是308- isBadRequest:响应状态码是400- isUnauthorized:响应状态码是401- isPaymentRequired:响应状态码是402- isForbidden:响应状态码是403- isNotFound:响应状态码是404- isMethodNotAllowed:响应状态码是405- isNotAcceptable:响应状态码是406- isProxyAuthenticationRequired:响应状态码是407- isRequestTimeout:响应状态码是408- isConflict:响应状态码是409- isGone:响应状态码是410- isLengthRequired:响应状态码是411- isPreconditionFailed:响应状态码是412- isPayloadTooLarge:响应状态码是413- isUriTooLong:响应状态码是414- isUnsupportedMediaType:响应状态码是415- isRequestedRangeNotSatisfiable:响应状态码是416- isExpectationFailed:响应状态码是417- isIAmATeapot:响应状态码是418- isInsufficientSpaceOnResource:响应状态码是419- isMethodFailure:响应状态码是420- isDestinationLocked:响应状态码是421- isUnprocessableEntity:响应状态码是422- isLocked:响应状态码是423- isFailedDependency:响应状态码是424- isTooEarly:响应状态码是425- isUpgradeRequired:响应状态码是426- isPreconditionRequired:响应状态码是428- isTooManyRequests:响应状态码是429- isRequestHeaderFieldsTooLarge:响应状态码是431- isUnavailableForLegalReasons:响应状态码是451- isInternalServerError:响应状态码是500- isNotImplemented:响应状态码是501- isBadGateway:响应状态码是502- isServiceUnavailable:响应状态码是503- isGatewayTimeout:响应状态码是504- isHttpVersionNotSupported:响应状态码是505- isVariantAlsoNegotiates:响应状态码是506- isInsufficientStorage:响应状态码是507- isLoopDetected:响应状态码是508- isBandwidthLimitExceeded:响应状态码是509- isNotExtended:响应状态码是510- isNetworkAuthenticationRequired:响应状态码是511
  • **header()**:返回HeaderResultMatchers,访问响应头断言 - string:断言响应头的主值- stringValues:断言响应头的值- exists:断言指定的响应头存在- doesNotExist:断言指定的响应头不存在- longValue:将指定响应头断言为long- dateValue:断言指定响应头解析为日期
  • **content()**:返回ContentResultMatchers,访问响应体断言 - contentType:断言Content-Type,给定的内容类型必须完全匹配,包括类型、子类型和参数- contentTypeCompatibleWith:断言Content-Type与指定的类型兼容- encoding:断言响应的字符编码- string:断言响应体内容(作为字符串)- bytes:断言响应体内容(作为字节数组)- xml:断言响应体内容(作为Xml)- source:断言响应体内容(作为Source)- json:断言响应体内容(作为json)
  • **jsonPath(String expression, Object… args)**:返回JsonPathResultMatchers,使用JsonPath表达式访问响应体断言 - prefix:断言JSON有效负载是否添加了给定的前缀- value:根据JsonPath断言结果值- exists:根据JsonPath断言在给定路径上存在非空值- doesNotExist:根据JsonPath断言在给定路径上不存在非空值- isEmpty:根据JsonPath断言给定路径中存在空值- isNotEmpty:根据JsonPath断言给定路径中不存在空值- hasJsonPath:根据JsonPath断言给定路径中存在一个值- doesNotHaveJsonPath:根据JsonPath断言给定路径中不存在一个值- isString:根据JsonPath断言结果是String- isBoolean:根据JsonPath断言结果是Boolean- isNumber:根据JsonPath断言结果是Number- isArray:根据JsonPath断言结果是Array- isMap:根据JsonPath断言结果是Map
  • **jsonPath(String expression, Matcher<? super T> matcher)**:根据响应体计算给定的JsonPath表达式,并使用给定的Hamcrest Matcher断言结果值
  • **jsonPath(String expression, Matcher<? super T> matcher, Class targetType)**:根据响应体计算给定的JsonPath表达式,并使用给定的Hamcrest Matcher断言结果值,在应用匹配器之前将结果值强制转换为给定的目标类型
  • **xpath(String expression, Object… args)**:返回XpathResultMatchers,使用XPath表达式访问响应体断言,以检查响应体的特定子集 - node:计算XPath并断言使用给定的Hamcrest Matcher找到的Node内容- nodeList:计算XPath并断言与给定的Hamcrest Matcher找到的NodeList内容- exists:计算XPath并断言内容存在- doesNotExist:计算XPath并断言内容不存在- nodeCount:计算XPath并断言使用给定的Hamcrest Matcher找到的节点数- string:应用XPath并断言用给定的Hamcrest Matcher找到的String值- number:计算XPath并断言用给定的Hamcrest Matcher找到的Double值- booleanValue:计算XPath并断言找到的Boolean
  • **xpath(String expression, Map<String, String> namespaces, Object… args)**:使用XPath表达式访问响应体断言,以检查响应体的特定子集
  • **cookie()**:返回CookieResultMatchers,访问响应cookie断言 - value:使用给定的Hamcrest Matcher断言一个cookie值- exists:断言cookie存在- doesNotExist:断言cookie不存在- maxAge:使用Hamcrest Matcher断言cookie的maxAge- path:用Hamcrest Matcher断言一个cookie的路径- domain:使用Hamcrest Matcher断言cookie的域- comment:用Hamcrest Matcher断言一个cookie的注释- version:用Hamcrest Matcher断言一个cookie的版本- secure:断言cookie是否必须通过安全协议发送- httpOnly:断言cookie是否只能是HTTP
6、MvcResult

即执行完控制器后得到的整个结果,并不仅仅是返回值,其包含了测试时需要的所有信息。

MvcResult有两个实现类:

  • DefaultMvcResult:一个简单的默认实现
  • PrintingMvcResult:待打印功能的实现

常用方法:

  • getRequest:返回执行的请求
  • getResponse:返回结果响应
  • getHandler:返回已执行的处理程序
  • getInterceptors:返回处理程序周围的拦截器
  • getModelAndView:返回处理程序准备的ModelAndView
  • getResolvedException:返回由处理程序引发并通过HandlerExceptionResolver成功解决的任何异常
  • getFlashMap:返回在请求处理期间保存的FlashMap
  • getAsyncResult:得到异步执行的结果

5、业务代码

实体类:

@Data@NoArgsConstructor@AllArgsConstructorpublicclassUser{privateLong id;privateString name;privateInteger age;privateDouble height;}

Dao层:

@MapperpublicinterfaceUserMapper{List<User>list();Integeradd(User user);Integerupdate(User user);IntegerdeleteById(Long id);UsergetById(Long id);}

UserMapper.xml:

<!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="pers.zhang.mapper.UserMapper"><insertid="add">
        INSERT INTO user (name, age, height)
        VALUES (#{name}, #{age}, #{height})
    </insert><updateid="update">
        UPDATE user SET name = #{name}, age = #{age}, height = #{height}
        WHERE id = #{id}
    </update><deleteid="deleteById">
        DELETE FROM user
        WHERE id = #{id}
    </delete><selectid="list"resultType="pers.zhang.entity.User">
        SELECT id, name, age, height
        FROM user;
    </select><selectid="getById"resultType="pers.zhang.entity.User">
        SELECT id, name, age, height
        FROM user
        WHERE id = #{id}
    </select></mapper>

Service层:

publicinterfaceUserService{List<User>list();Integeradd(User user);Integerupdate(User user);IntegerdeleteById(Long id);UsergetById(Long id);}
@ServicepublicclassUserServiceImplimplementsUserService{@AutowiredprivateUserMapper userMapper;@OverridepublicList<User>list(){System.out.println("Call userMapper.list...");return userMapper.list();}@OverridepublicIntegeradd(User user){System.out.println("Call userMapper.add...");return userMapper.add(user);}@OverridepublicIntegerupdate(User user){System.out.println("Call userMapper.update...");return userMapper.update(user);}@OverridepublicIntegerdeleteById(Long id){System.out.println("Call userMapper.deleteById...");return userMapper.deleteById(id);}@OverridepublicUsergetById(Long id){System.out.println("Call userMapper.getById...");return userMapper.getById(id);}}

Controller层:

@RestController@RequestMapping("/user")publicclassUserController{@AutowiredprivateUserService userService;@GetMapping("/list")publicList<User>list(){System.out.println("Call UserService.list");return userService.list();}@GetMapping("/info")publicUsergetUserById(Long id){System.out.println("Call UserService.getUserById");return userService.getById(id);}@PostMapping("/add")publicIntegeradd(@RequestBodyUser user){System.out.println("Call UserService.add");return userService.add(user);}@PostMapping("/update")publicIntegerupdate(@RequestBodyUser user){System.out.println("Call UserService.update");return userService.update(user);}@PostMapping("/delete")publicIntegerdelete(Long id){System.out.println("Call UserService.delete");return userService.deleteById(id);}}

6、分层测试

6.1、Dao层测试

在UserMapperTest测试类中可以直接使用@Autowired来装配UserMapper这个Bean。而且,@SpringBootTest注解会自动帮我们完成启动一个Spring容器ApplicationContext,然后连接数据库,执行一套完整的业务逻辑。

importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importpers.zhang.entity.User;importjava.util.List;importstaticorg.junit.jupiter.api.Assertions.*;importstaticorg.assertj.core.api.Assertions.*;@SpringBootTestclassUserMapperTest{/**
     * 数据库user表内容如下:
     *
     *  id  |  name  |  age  |  height  |
     *  1      tom      18       1.77
     *  2      jerry    22       1.83
     *  3      mike     24       1.79
     */@AutowiredprivateUserMapper userMapper;@Testvoidlist(){List<User> list = userMapper.list();assertThat(list.size()).isEqualTo(3);assertThat(list).extracting("id","name","age","height").contains(tuple(1L,"tom",18,1.77),tuple(2L,"jerry",22,1.83),tuple(3L,"mike",24,1.79));}@Testvoidadd(){User user =newUser();
        user.setName("zhangsan");
        user.setAge(30);
        user.setHeight(1.66);Integer effectRows = userMapper.add(user);assertThat(effectRows).isEqualTo(1);}@Testvoidupdate(){User user =newUser();
        user.setName("zhangsan");
        user.setAge(33);
        user.setHeight(1.88);
        user.setId(7L);Integer effectRows = userMapper.update(user);assertThat(effectRows).isEqualTo(1);}@TestvoiddeleteById(){Integer effectRows = userMapper.deleteById(7L);assertThat(effectRows).isEqualTo(1);}@TestvoidgetById(){User expect =newUser();
        expect.setId(1L);
        expect.setName("tom");
        expect.setAge(18);
        expect.setHeight(1.77);User user = userMapper.getById(1L);assertThat(user).isEqualTo(expect);}}

6.2、Service层测试

上面的测试代码是连接真实数据库来执行真实的Dao层数据库查询逻辑。而在实际开发中,有时候需要独立于数据库进行Service层逻辑的开发。这个时候就可以直接把数据库Dao层代码Mock掉。

importorg.junit.jupiter.api.BeforeEach;importorg.junit.jupiter.api.Test;importorg.mockito.*;importpers.zhang.entity.User;importpers.zhang.mapper.UserMapper;importjava.util.ArrayList;importjava.util.List;importstaticorg.assertj.core.api.Assertions.*;importstaticorg.mockito.Mockito.*;classUserServiceImplTest{//Mock掉Dao层@MockprivateUserMapper userMapper;//把Mock掉的Dao层注入Service@InjectMocksprivateUserServiceImpl userService;@BeforeEachvoidsetup(){//开启Mockito注解MockitoAnnotations.openMocks(this);}@Testvoidlist(){List<User> users =newArrayList<>();
        users.add(newUser(10L,"zhangsan",18,1.77));
        users.add(newUser(11L,"lisi",22,1.83));//打桩when(userMapper.list()).thenReturn(users);//调用List<User> list = userService.list();
        list.forEach(System.out::println);//验证verify(userMapper,times(1)).list();}@Testvoidadd(){User user =newUser(1L,"tom",21,1.80);//打桩when(userMapper.add(isA(User.class))).thenReturn(1);//调用Integer effectRows = userService.add(user);assertThat(effectRows).isEqualTo(1);//验证verify(userMapper,times(1)).add(user);}@Testvoidupdate(){User user =newUser(2L,"tom",21,1.80);//打桩when(userMapper.update(argThat(u ->{return u !=null&& u.getId()!=null;}))).thenReturn(1);//调用Integer effectRows = userService.update(user);assertThat(effectRows).isEqualTo(1);//验证verify(userMapper,times(1)).update(user);}@TestvoiddeleteById(){//打桩when(userMapper.deleteById(anyLong())).thenReturn(1);//调用Integer effectRows = userService.deleteById(999L);assertThat(effectRows).isEqualTo(1);//验证verify(userMapper,times(1)).deleteById(999L);}@TestvoidgetById(){User user =newUser(1L,"xxx",40,1.92);//打桩when(userMapper.getById(1L)).thenReturn(user);//调用User actual = userService.getById(1L);assertThat(actual).isInstanceOf(User.class);//验证verify(userMapper,times(1)).getById(1L);}}

输出:

Call userMapper.update...Call userMapper.getById...Call userMapper.add...Call userMapper.list...User(id=10, name=zhangsan, age=18, height=1.77)User(id=11, name=lisi, age=22, height=1.83)Call userMapper.deleteById...

6.3、Controller层测试

spring-boot-starter-test提供了MockMvc对Controller测试功能的强大支持。

importorg.junit.jupiter.api.BeforeEach;importorg.junit.jupiter.api.Test;importorg.mockito.InjectMocks;importorg.mockito.Mock;importorg.mockito.MockitoAnnotations;importorg.springframework.http.MediaType;importorg.springframework.test.web.servlet.MockMvc;importorg.springframework.test.web.servlet.MvcResult;importorg.springframework.test.web.servlet.request.MockMvcRequestBuilders;importorg.springframework.test.web.servlet.setup.MockMvcBuilders;importpers.zhang.entity.User;importpers.zhang.service.UserService;importjava.util.Arrays;importstaticorg.mockito.Mockito.*;importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.*;importstaticorg.springframework.test.web.servlet.result.MockMvcResultHandlers.*;classUserControllerTest{privateMockMvc mockMvc;@MockprivateUserService userService;@InjectMocksprivateUserController userController;@BeforeEachvoidsetup(){//开启Mockito注解MockitoAnnotations.openMocks(this);//初始化MockMvc,将UserController注入其中
        mockMvc =MockMvcBuilders.standaloneSetup(userController).build();}@Testvoidlist()throwsException{//打桩when(userService.list()).thenReturn(Arrays.asList(newUser(1L,"tom",18,1.77),newUser(2L,"jerry",22,1.88)));//调用MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/user/list").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()).andDo(print()).andReturn();System.out.println(mvcResult.getResponse().getContentAsString());//验证verify(userService,times(1)).list();}@TestvoidgetUserById()throwsException{//打桩User user =newUser(1L,"tom",18,1.77);when(userService.getById(anyLong())).thenReturn(user);//调用MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/user/info").accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON).param("id","1")).andExpect(status().isOk()).andDo(print()).andReturn();System.out.println(mvcResult.getResponse().getContentAsString());//验证verify(userService,times(1)).getById(1L);}@Testvoidadd()throwsException{User user =newUser();
        user.setName("jerry");
        user.setAge(22);
        user.setHeight(1.74);//打桩when(userService.add(isA(User.class))).thenReturn(1);//调用MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/user/add").accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON).content("{\"name\": \"jerry\", \"age\": 22, \"height\": 1.74}")).andExpect(status().isOk()).andDo(print()).andReturn();System.out.println(mvcResult.getResponse().getContentAsString());//验证verify(userService,times(1)).add(user);}@Testvoidupdate()throwsException{User user =newUser(1L,"tom",18,1.77);//打桩when(userService.update(isA(User.class))).thenReturn(1);//调用MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/user/update").accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON).content("{\"id\": 1, \"name\": \"tom\", \"age\": 18, \"height\": 1.77}")).andExpect(status().isOk()).andDo(print()).andReturn();System.out.println(mvcResult.getResponse().getContentAsString());//验证verify(userService,times(1)).update(user);}@Testvoiddelete()throwsException{//打桩when(userService.deleteById(anyLong())).thenReturn(1);//调用MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/user/delete").accept(MediaType.APPLICATION_JSON).param("id","1")).andExpect(status().isOk()).andDo(print()).andReturn();System.out.println(mvcResult.getResponse().getContentAsString());//验证verify(userService,times(1)).deleteById(1L);}}

7、JSON接口测试

使用JsonPath可以像JavaScript语法一样方便地进行JSON数据返回的访问操作。

importorg.hamcrest.Matchers;importorg.junit.jupiter.api.Test;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.http.MediaType;importorg.springframework.test.web.servlet.MockMvc;importorg.springframework.test.web.servlet.request.MockMvcRequestBuilders;importorg.springframework.test.web.servlet.result.MockMvcResultHandlers;importorg.springframework.test.web.servlet.result.MockMvcResultMatchers;@SpringBootTest@AutoConfigureMockMvcclassJsonControllerTest{@AutowiredprivateMockMvc mockMvc;/**
     * 数据库user表内容如下:
     *
     *  id  |  name  |  age  |  height  |
     *  1      tom      18       1.77
     *  2      jerry    22       1.83
     *  3      mike     24       1.79
     */@Testvoidlist()throwsException{
        mockMvc.perform(MockMvcRequestBuilders.get("/user/list").accept(MediaType.APPLICATION_JSON))//响应码200.andExpect(MockMvcResultMatchers.status().isOk())//json数组长度为3.andExpect(MockMvcResultMatchers.jsonPath("$.length()",Matchers.equalTo(3)))//name包含指定值.andExpect(MockMvcResultMatchers.jsonPath("$..name",Matchers.contains("tom","jerry","mike"))).andDo(MockMvcResultHandlers.print());}@TestvoidgetUserById()throwsException{
        mockMvc.perform(MockMvcRequestBuilders.get("/user/info").accept(MediaType.APPLICATION_JSON).param("id","1")).andExpect(MockMvcResultMatchers.status().isOk()).andExpect(MockMvcResultMatchers.jsonPath("$.name",Matchers.equalTo("tom"))).andExpect(MockMvcResultMatchers.jsonPath("$.age",Matchers.equalTo(18))).andExpect(MockMvcResultMatchers.jsonPath("$.height",Matchers.equalTo(1.77))).andDo(MockMvcResultHandlers.print());}@Testvoidadd()throwsException{
        mockMvc.perform(MockMvcRequestBuilders.post("/user/add").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).content("{\"name\": \"zhangsan\", \"age\":  40, \"height\": 1.76}")).andExpect(MockMvcResultMatchers.status().isOk()).andExpect(MockMvcResultMatchers.jsonPath("$",Matchers.equalTo(1))).andDo(MockMvcResultHandlers.print());}@Testvoidupdate()throwsException{
        mockMvc.perform(MockMvcRequestBuilders.post("/user/update").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).content("{\"id\": 9, \"name\": \"lisi\", \"age\":  44, \"height\": 1.76}")).andExpect(MockMvcResultMatchers.status().isOk()).andExpect(MockMvcResultMatchers.jsonPath("$",Matchers.equalTo(1))).andDo(MockMvcResultHandlers.print());}@Testvoiddelete()throwsException{
        mockMvc.perform(MockMvcRequestBuilders.post("/user/delete").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).param("id","9")).andExpect(MockMvcResultMatchers.status().isOk()).andExpect(MockMvcResultMatchers.jsonPath("$",Matchers.equalTo(1))).andDo(MockMvcResultHandlers.print());}}
标签: spring boot test

本文转载自: https://blog.csdn.net/cold___play/article/details/135732392
版权归原作者 吴声子夜歌 所有, 如有侵权,请联系我们删除。

“SpringBoot Test详解”的评论:

还没有评论