SpringBoot对单元测试的支持在于提供了一系列注解和工具的集成,它们是通过两个项目提供的:
通常情况下,我们通过spring-boot-starter-test的Starter来引入SpringBoot的核心支持项目以及单元测试项目以及单元测试库。
spring-boot-starter-test包含的类库如下:
如果SpringBoot提供的基础类无法满足业务需求,我们也可以自行添加依赖。依赖注入的优点之一就是可以轻松使用单元测试。这种方式可以直接通过new来创建对象,而不需要涉及Spring。当然,也可以通过模拟对象来替换真实依赖。
如果需要集成测试,比如使用Spring的ApplicationContext,Spring同样能够提供无须部署应用程序或连接到其它基础环境的集成测试。而SpringBoot应用本身就是一个ApplicationContext,因此除了正常使用Spring上下文进行测试,无须执行其它操作。
Maven依赖:
org.springframework.boot spring-boot-starter-test test 从功能上讲,Spring Boot Test中的注解主要分如下几类:
| 类别 | 示例 | 说明 |
|---|---|---|
| 配置类型 | @TestConfiguration等 | 提供一些测试相关的配置入口 |
| mock类型 | @MockBean等 | 提供mock支持 |
| 启动测试类型 | @SpringBootTest等 | 以Test结尾的注解,具有加载applicationContext的能力 |
| 自动配置类型 | @AutoConfigureJdbc等 | 以AutoConfigure开头的注解,具有加载测试支持功能的能力 |
| 注解 | 作用 | 实践中的使用 |
|---|---|---|
| @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。
| 注解 | 作用 |
|---|---|
| MockBean | 用于Mock指定的class或被注解的属性 |
| MockBeans | 使@MockBean支持在同一类型或属性上多次出现 |
| @SpyBean | 用于spy指定的class或被注解的属性 |
| @SpyBeans | 使@SpyBeans支持在同一类型或属性上次多次出现 |
@MockBean和@SpyBean这两个注解,在mockito框架中本来已经存在,且功能基本相同。Spring Boot Test又定义一份重复的注解,目的在于使MockBean和SpyBean被ApplicationContext管理,从而方便使用。
MockBean和SpyBean功能非常相似,都能模拟方法的各种行为。不同之处在于MockBean是全新的对象,跟正式对象没有关系;而SpyBean与正式对象紧密联系,可以模拟正式对象的部分方法,没有被模拟的方法仍然可以运行正式代码。
| 注解 | 作用 |
|---|---|
| @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。
自动配置类型有两种使用方式:
所有的@*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是这些注解中最常用的一个,其中包含的配置项如下:
@ContextConfiguration中的class,若没有显示指定,将查找嵌套的@Configuration类,然后返回到SpringBootConfiguration搜索配置MOCK:此值为默认值,该类型提供一个mock环境,此时内嵌的服务(servlet容器)并没有真正启动,也不会监听web端口RANDOM_PORT:启动一个真实的web服务,监听一个随机端口DEFINED_PORT:启动一个真实的web服务,监听一个定义好的端口(从配置中读取)NONE:启动一个非web的ApplicationContext,既不提供mock环境,也不提供真实的web服务整体上,Spring Boot Test支持的测试种类,大致可以分为如下三类:
默认无参数的@SpringBootTest 注解会加载一个Web Application Context并提供Mock Web Environment,但是不会启动内置的server。这点从日志中没有打印Tomcat started on port(s)可以佐证。
@SpringBootTest public class AppTest { @Autowired UserMapper userMapper; @Test public void test() { User user = new User(); user.setName("tom"); user.setAge(18); user.setHeight(1.88); Assertions.assertThat(userMapper.add(user)).isEqualTo(1); } } //指定@SpringBootTest的Web Environment为RANDOM_PORT //此时,将会加载ApplicationContext,并启动Server,Server监听在随机端口上。 //在测试类中通过@LocalServerPort获取该端口值 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class DemoTest { @LocalServerPort private Integer port; @Test @DisplayName("should access application") public void shouldAccessApplication() { Assertions.assertThat(port).isGreaterThan(1024); } } 也可以通过指定@SpringBootTest的Web Environment为DEFINED_PORT 来指定server侦听应用程序配置的端口,默认为8080。不过这种指定端口的方式很少使用,因为如果本地同时启动应用时,会导致端口冲突。
MockMvc可以做到不启动项目工程就可以对结构进行测试。MockMvc实现了对HTTP请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,同时提供了一套验证的工具,使得请求的验证同一而且方便。
创建一个简单的TestController,提供一个方法,返回一个字符串:
@RestController public class TestController { @RequestMapping("/mock") public String mock(String name) { return "Hello " + name + "!"; } } 单元测试:
@SpringBootTest @AutoConfigureMockMvc class TestControllerTest { @Autowired private MockMvc mockMvc; @Test void mock() throws Exception { //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 = null Session Attrs = {} Handler: Type = pers.zhang.controller.TestController Method = pers.zhang.controller.TestController#mock(String) Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = [Content-Type:"application/json", Content-Length:"10"] Content type = application/json Body = Hello tom! Forwarded URL = null Redirected URL = null Cookies = [] @AutoConfigureMockMvc注解提供了自动配置MockMvc的功能。@Autowired注入MockMvc对象。
MockMvc对象可以通过接口M哦查看Mv吃Builder的实现类获得。该接口提供一个唯一的build方法来构造MockMvc。主要有两个实现类:
StandaloneMockMvcBuilder:独立安装DefaultMockMvcBuilder:集成Web环境测试(并不会真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)MockMvcBuilders提供了对应的standaloneSetup和webAppContextSetup两种创建方法,在使用时直接调用即可,默认使用DefaultMOckMvcBuilder。
整个单元测试包含一下步骤:
@AutoConfigureMockMvc提供了自动配置MockMvc的功能,源码如下:
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @ImportAutoConfiguration @PropertyMapping("spring.test.mockmvc") public @interface AutoConfigureMockMvc { //是否应向MockMvc注册来自应用程序上下文的filter,默认true boolean addFilters() default true; //每次MockMvc调用后应如何打印MvcResult信息 @PropertyMapping(skip = SkipPropertyMapping.ON_DEFAULT_VALUE) MockMvcPrint print() default MockMvcPrint.DEFAULT; //如果MvcResult仅在测试失败时才打印信息。默认true,则表示只在失败时打印 boolean printOnlyOnFailure() default true; //当HtmlUnit在类路径上时,是否应该自动配置WebClient。默认为true @PropertyMapping("webclient.enabled") boolean webClientEnabled() default true; //当Selenium位于类路径上时,是否应自动配置WebDriver。默认为true @PropertyMapping("webdriver.enabled") boolean webDriverEnabled() default true; } 在AutoConfigureMockMvc的源码中,我们重点看它组合的@ImportAutoConfiguration注解。该注解同样是SpringBoot自动配置项目提供的,其功能类似@EnableAutoConfiguration,但又略有区别。@ImportAutoConfiguration同样用于导入自动配置类,不仅可以像@EnableAutoConfiguration那样排除指定的自动配置配置类,还可以指定使用哪些自动配置类,这是它们之间的重要区别之一。
另外,@ImportAutoConfiguration使用的排序规则与@EnableAutoConfiguration的相同,通常情况下,建议优先使用@EnableAutoConfiguration注解进行自动配置。但在单元测试中,则可考虑优先使用@ImportAutoCOnfiguration。源码如下:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import(ImportAutoConfigurationImportSelector.class) public @interface ImportAutoConfiguration { //指定引入的自动配置类 @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 }) public class MockMvcAutoConfiguration { private final WebApplicationContext context; private final WebMvcProperties webMvcProperties; MockMvcAutoConfiguration(WebApplicationContext context, WebMvcProperties webMvcProperties) { this.context = context; this.webMvcProperties = webMvcProperties; } .... } 注解部分说明,MockMvcAutoConfiguration需要在Web应用程序类型为Servlet,且在WebMvcAutoConfiguration自动配置之后进行自动配置。
另外,通过@EnableConfigurationProperties导入了ServerProperties和WebMvcProperties两个配置属性类,并通过构造方法设置为成员变量。
MockMvcBuilder构造MockMvc的构造器mockMvc调用perform,执行一个RequestBuilder请求,调用Controller的业务处理逻辑perform返回ResultActions,返回操作结果,通过ResultActions,提供了统一的验证方式StatusResultMatchers对请求结果进行验证ContentResultMatchers对请求返回的内容进行验证MockMvc是spring测试下的一个非常好用的类,他们的初始化需要在setUp中进行。MockMvcBuilder是用来构造MockMvc的构造器,其主要有两个实现:StandaloneMockMvcBuilder和DefaultMockMvcBuilder,前者继承了后者。
MockMvcBuilders.webAppContextSetup(WebApplicationContext context):指定WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的MockMvcMockMvcBuilders.standaloneSetup(Object... controllers):通过参数指定一组控制器,这样就不需要从上下文获取了,比如this.mockMvc = MockMvcBuilders.standaloneSetup(this.controller).build();这些Builder还提供了其他api,可以自行百度从名字可以看出,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.getpost(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方法调用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 = new MediaType("*", "*"); APPLICATION_ATOM_XML = new MediaType("application", "atom+xml"); APPLICATION_CBOR = new MediaType("application", "cbor"); APPLICATION_FORM_URLENCODED = new MediaType("application", "x-www-form-urlencoded"); APPLICATION_JSON = new MediaType("application", "json"); APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8); APPLICATION_NDJSON = new MediaType("application", "x-ndjson"); APPLICATION_OCTET_STREAM = new MediaType("application", "octet-stream"); APPLICATION_PDF = new MediaType("application", "pdf"); APPLICATION_PROBLEM_JSON = new MediaType("application", "problem+json"); APPLICATION_PROBLEM_JSON_UTF8 = new MediaType("application", "problem+json", StandardCharsets.UTF_8); APPLICATION_PROBLEM_XML = new MediaType("application", "problem+xml"); APPLICATION_RSS_XML = new MediaType("application", "rss+xml"); APPLICATION_STREAM_JSON = new MediaType("application", "stream+json"); APPLICATION_XHTML_XML = new MediaType("application", "xhtml+xml"); APPLICATION_XML = new MediaType("application", "xml"); IMAGE_GIF = new MediaType("image", "gif"); IMAGE_JPEG = new MediaType("image", "jpeg"); IMAGE_PNG = new MediaType("image", "png"); MULTIPART_FORM_DATA = new MediaType("multipart", "form-data"); MULTIPART_MIXED = new MediaType("multipart", "mixed"); MULTIPART_RELATED = new MediaType("multipart", "related"); TEXT_EVENT_STREAM = new MediaType("text", "event-stream"); TEXT_HTML = new MediaType("text", "html"); TEXT_MARKDOWN = new MediaType("text", "markdown"); TEXT_PLAIN = new MediaType("text", "plain"); TEXT_XML = new MediaType("text", "xml"); ResultMatcher用来匹配执行完请求后的结果验证,其就一个match(MvcResult result)断言方法,如果匹配失败将抛出相应的异常,spring mvc测试框架提供了很多***ResultMatchers来满足测试需求。
MockMvcResultMatchers类提供了许多静态方法,提供了多种匹配器:
RequestResultMatchers,访问与请求相关的断言 asyncStarted:断言异步处理开始asyncNotStarted:断言异步不开始asyncResult:断言使用给定匹配器进行异步处理的结果attribute:用于断言请求属性值sessionAttribute:用于断言Session会话属性值sessionAttributeDoesNotExist:断言Session会话属性不存在HandlerResultMatchers,对处理请求的处理程序的断言的访问 handlerType:断言处理请求的处理程序的类型methodCall:断言用于处理请求的控制器方法methodName:断言用于处理请求的控制器方法的名称method:断言用于处理请求的控制器方法ModelResultMatchers,访问与模型相关的断言 attribute:断言一个模型属性值attributeExists:断言一个模型属性存在attributeDoesNotExist:断言一个模型属性不存在attributeErrorCount:断言给定的模型属性有指定个数的错误attributeHasErrors:断言给定的模型属性有错误attributeHasNoErrors:断言给定的模型属性没有错误attributeHasFieldErrors:断言给定的模型属性字段有错误attributeHasFieldErrorCode:使用精确字符串匹配断言模型属性的字段错误代码errorCount:断言模型中的错误总数hasErrors:断言模型中有错误hasNoErrors:断言模型中没有错误size:断言模型属性的数量ViewResultMatchers,访问所选视图上的断言 name:断言视图名FlashAttributeResultMatchers,访问flash属性断言 attribute:断言flash属性的值attributeExists:断言给定的flash属性是否存在attributeCount:断言flash属性的数量StatusResultMatchers,访问响应状态断言 is:断言响应状态码is1xxInformational:断言响应状态码在1xx范围内is2xxSuccessful:断言响应状态码在2xx范围内is3xxRedirection:断言响应状态码在3xx范围内is4xxClientError:断言响应状态码在4xx范围内is5xxServerError:断言响应状态码在5xx范围内reason:断言Servlet响应错误消息isContinue:响应状态码是100isSwitchingProtocols:响应状态码是101isProcessing:响应状态码是102isCheckpoint:响应状态码是103isOk:响应状态码是200isCreated:响应状态码是201isAccepted:响应状态码是202isNonAuthoritativeInformation:响应状态码是203isNoContent:响应状态码是204isResetContent:响应状态码是205isPartialContent:响应状态码是206isMultiStatus:响应状态码是207isAlreadyReported:响应状态码是208isImUsed:响应状态码是226isMultipleChoices:响应状态码是300isMovedPermanently:响应状态码是301isFound:响应状态码是302isSeeOther:响应状态码是303isNotModified:响应状态码是304isUseProxy:响应状态码是305isTemporaryRedirect:响应状态码是307isPermanentRedirect:响应状态码是308isBadRequest:响应状态码是400isUnauthorized:响应状态码是401isPaymentRequired:响应状态码是402isForbidden:响应状态码是403isNotFound:响应状态码是404isMethodNotAllowed:响应状态码是405isNotAcceptable:响应状态码是406isProxyAuthenticationRequired:响应状态码是407isRequestTimeout:响应状态码是408isConflict:响应状态码是409isGone:响应状态码是410isLengthRequired:响应状态码是411isPreconditionFailed:响应状态码是412isPayloadTooLarge:响应状态码是413isUriTooLong:响应状态码是414isUnsupportedMediaType:响应状态码是415isRequestedRangeNotSatisfiable:响应状态码是416isExpectationFailed:响应状态码是417isIAmATeapot:响应状态码是418isInsufficientSpaceOnResource:响应状态码是419isMethodFailure:响应状态码是420isDestinationLocked:响应状态码是421isUnprocessableEntity:响应状态码是422isLocked:响应状态码是423isFailedDependency:响应状态码是424isTooEarly:响应状态码是425isUpgradeRequired:响应状态码是426isPreconditionRequired:响应状态码是428isTooManyRequests:响应状态码是429isRequestHeaderFieldsTooLarge:响应状态码是431isUnavailableForLegalReasons:响应状态码是451isInternalServerError:响应状态码是500isNotImplemented:响应状态码是501isBadGateway:响应状态码是502isServiceUnavailable:响应状态码是503isGatewayTimeout:响应状态码是504isHttpVersionNotSupported:响应状态码是505isVariantAlsoNegotiates:响应状态码是506isInsufficientStorage:响应状态码是507isLoopDetected:响应状态码是508isBandwidthLimitExceeded:响应状态码是509isNotExtended:响应状态码是510isNetworkAuthenticationRequired:响应状态码是511HeaderResultMatchers,访问响应头断言 string:断言响应头的主值stringValues:断言响应头的值exists:断言指定的响应头存在doesNotExist:断言指定的响应头不存在longValue:将指定响应头断言为longdateValue:断言指定响应头解析为日期ContentResultMatchers,访问响应体断言 contentType:断言Content-Type,给定的内容类型必须完全匹配,包括类型、子类型和参数contentTypeCompatibleWith:断言Content-Type与指定的类型兼容encoding:断言响应的字符编码string:断言响应体内容(作为字符串)bytes:断言响应体内容(作为字节数组)xml:断言响应体内容(作为Xml)source:断言响应体内容(作为Source)json:断言响应体内容(作为json)JsonPathResultMatchers,使用JsonPath表达式访问响应体断言 prefix:断言JSON有效负载是否添加了给定的前缀value:根据JsonPath断言结果值exists:根据JsonPath断言在给定路径上存在非空值doesNotExist:根据JsonPath断言在给定路径上不存在非空值isEmpty:根据JsonPath断言给定路径中存在空值isNotEmpty:根据JsonPath断言给定路径中不存在空值hasJsonPath:根据JsonPath断言给定路径中存在一个值doesNotHaveJsonPath:根据JsonPath断言给定路径中不存在一个值isString:根据JsonPath断言结果是StringisBoolean:根据JsonPath断言结果是BooleanisNumber:根据JsonPath断言结果是NumberisArray:根据JsonPath断言结果是ArrayisMap:根据JsonPath断言结果是MapXpathResultMatchers,使用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并断言找到的Booleanvalue:使用给定的Hamcrest Matcher断言一个cookie值exists:断言cookie存在doesNotExist:断言cookie不存在maxAge:使用Hamcrest Matcher断言cookie的maxAgepath:用Hamcrest Matcher断言一个cookie的路径domain:使用Hamcrest Matcher断言cookie的域comment:用Hamcrest Matcher断言一个cookie的注释version:用Hamcrest Matcher断言一个cookie的版本secure:断言cookie是否必须通过安全协议发送httpOnly:断言cookie是否只能是HTTP即执行完控制器后得到的整个结果,并不仅仅是返回值,其包含了测试时需要的所有信息。
MvcResult有两个实现类:
常用方法:
实体类:
@Data @NoArgsConstructor @AllArgsConstructor public class User { private Long id; private String name; private Integer age; private Double height; } Dao层:
@Mapper public interface UserMapper { List list(); Integer add(User user); Integer update(User user); Integer deleteById(Long id); User getById(Long id); } UserMapper.xml:
INSERT INTO user (name, age, height) VALUES (#{name}, #{age}, #{height}) UPDATE user SET name = #{name}, age = #{age}, height = #{height} WHERE id = #{id} DELETE FROM user WHERE id = #{id} Service层:
public interface UserService { List list(); Integer add(User user); Integer update(User user); Integer deleteById(Long id); User getById(Long id); } @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public List list() { System.out.println("Call userMapper.list..."); return userMapper.list(); } @Override public Integer add(User user) { System.out.println("Call userMapper.add..."); return userMapper.add(user); } @Override public Integer update(User user) { System.out.println("Call userMapper.update..."); return userMapper.update(user); } @Override public Integer deleteById(Long id) { System.out.println("Call userMapper.deleteById..."); return userMapper.deleteById(id); } @Override public User getById(Long id) { System.out.println("Call userMapper.getById..."); return userMapper.getById(id); } } Controller层:
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @GetMapping("/list") public List list() { System.out.println("Call UserService.list"); return userService.list(); } @GetMapping("/info") public User getUserById(Long id) { System.out.println("Call UserService.getUserById"); return userService.getById(id); } @PostMapping("/add") public Integer add(@RequestBody User user) { System.out.println("Call UserService.add"); return userService.add(user); } @PostMapping("/update") public Integer update(@RequestBody User user) { System.out.println("Call UserService.update"); return userService.update(user); } @PostMapping("/delete") public Integer delete(Long id) { System.out.println("Call UserService.delete"); return userService.deleteById(id); } } 在UserMapperTest测试类中可以直接使用@Autowired来装配UserMapper这个Bean。而且,@SpringBootTest注解会自动帮我们完成启动一个Spring容器ApplicationContext,然后连接数据库,执行一套完整的业务逻辑。
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import pers.zhang.entity.User; import java.util.List; import static org.junit.jupiter.api.Assertions.*; import static org.assertj.core.api.Assertions.*; @SpringBootTest class UserMapperTest { /** * 数据库user表内容如下: * * id | name | age | height | * 1 tom 18 1.77 * 2 jerry 22 1.83 * 3 mike 24 1.79 */ @Autowired private UserMapper userMapper; @Test void list() { List 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) ); } @Test void add() { User user = new User(); user.setName("zhangsan"); user.setAge(30); user.setHeight(1.66); Integer effectRows = userMapper.add(user); assertThat(effectRows).isEqualTo(1); } @Test void update() { User user = new User(); user.setName("zhangsan"); user.setAge(33); user.setHeight(1.88); user.setId(7L); Integer effectRows = userMapper.update(user); assertThat(effectRows).isEqualTo(1); } @Test void deleteById() { Integer effectRows = userMapper.deleteById(7L); assertThat(effectRows).isEqualTo(1); } @Test void getById() { User expect = new User(); expect.setId(1L); expect.setName("tom"); expect.setAge(18); expect.setHeight(1.77); User user = userMapper.getById(1L); assertThat(user).isEqualTo(expect); } } 上面的测试代码是连接真实数据库来执行真实的Dao层数据库查询逻辑。而在实际开发中,有时候需要独立于数据库进行Service层逻辑的开发。这个时候就可以直接把数据库Dao层代码Mock掉。
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.*; import pers.zhang.entity.User; import pers.zhang.mapper.UserMapper; import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; class UserServiceImplTest { //Mock掉Dao层 @Mock private UserMapper userMapper; //把Mock掉的Dao层注入Service @InjectMocks private UserServiceImpl userService; @BeforeEach void setup() { //开启Mockito注解 MockitoAnnotations.openMocks(this); } @Test void list() { List users = new ArrayList<>(); users.add(new User(10L, "zhangsan", 18, 1.77)); users.add(new User(11L, "lisi", 22, 1.83)); //打桩 when(userMapper.list()).thenReturn(users); //调用 List list = userService.list(); list.forEach(System.out::println); //验证 verify(userMapper, times(1)).list(); } @Test void add() { User user = new User(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); } @Test void update() { User user = new User(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); } @Test void deleteById() { //打桩 when(userMapper.deleteById(anyLong())).thenReturn(1); //调用 Integer effectRows = userService.deleteById(999L); assertThat(effectRows).isEqualTo(1); //验证 verify(userMapper, times(1)).deleteById(999L); } @Test void getById() { User user = new User(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... spring-boot-starter-test提供了MockMvc对Controller测试功能的强大支持。
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import pers.zhang.entity.User; import pers.zhang.service.UserService; import java.util.Arrays; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; class UserControllerTest { private MockMvc mockMvc; @Mock private UserService userService; @InjectMocks private UserController userController; @BeforeEach void setup() { //开启Mockito注解 MockitoAnnotations.openMocks(this); //初始化MockMvc,将UserController注入其中 mockMvc = MockMvcBuilders.standaloneSetup(userController).build(); } @Test void list() throws Exception { //打桩 when(userService.list()).thenReturn( Arrays.asList( new User(1L, "tom", 18, 1.77), new User(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(); } @Test void getUserById() throws Exception { //打桩 User user = new User(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); } @Test void add() throws Exception { User user = new User(); 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); } @Test void update() throws Exception { User user = new User(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); } @Test void delete() throws Exception { //打桩 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); } } 使用JsonPath可以像JavaScript语法一样方便地进行JSON数据返回的访问操作。
import org.hamcrest.Matchers; 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.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; @SpringBootTest @AutoConfigureMockMvc class JsonControllerTest { @Autowired private MockMvc mockMvc; /** * 数据库user表内容如下: * * id | name | age | height | * 1 tom 18 1.77 * 2 jerry 22 1.83 * 3 mike 24 1.79 */ @Test void list() throws Exception { 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()); } @Test void getUserById() throws Exception { 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()); } @Test void add() throws Exception { 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()); } @Test void update() throws Exception { 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()); } @Test void delete() throws Exception { 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()); } }