SpringBoot Test详解
spring-boot-test项目:包含核心功能spring-boot-test-autoconfigure项目:支持自动配置通常情况下,我们通过spring-boot-starter-test的Starter来引入SpringBoot的核心支持项目以及单元测试项目以及单元测试库。JUnit:一个Java语言的单元测试框架:为SpringBoot应用提供集成测试和工具支持:支持流式断言的Java
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。
自动配置类型有两种使用方式:
- 在功能测试(即使用@SpringBootTest)时显示添加。
- 一般在切片测试中被隐式使用,例如@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支持的测试种类,大致可以分为如下三类:
- 单元测试:一般面向方法,编写一般业务代码时,测试成本较大。涉及到的注解有@Test。
- 切片测试:一般面向于测试的边界功能,介于单元测试和功能测试之间。涉及到的注解有@WebMvcTest等。主要就是对于Controller的测试,分离了Service层,这里就涉及到Mock控制层所依赖的组件了。
- 功能测试:一般面向某个完整的业务功能,同时也可以使用切面测试中mock能力,推荐使用。涉及到的注解有@SpringBootTest等。
3.1、单元测试
默认无参数的@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);
}
}
3.2、集成测试
//指定@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。不过这种指定端口的方式很少使用,因为如果本地同时启动应用时,会导致端口冲突。
4、MockMvc
MockMvc可以做到不启动项目工程就可以对结构进行测试。MockMvc实现了对HTTP请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,同时提供了一套验证的工具,使得请求的验证同一而且方便。
4.1、简单示例
创建一个简单的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。
整个单元测试包含一下步骤:
- 准备测试环境
- 执行MockMvc请求
- 添加验证断言
- 添加结果处理器
- 得到MvcResult进行自定义断言/进行下一步的异步请求
- 卸载测试环境
4.2、自动配置
@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两个配置属性类,并通过构造方法设置为成员变量。
4.3、使用方式
1、测试逻辑
MockMvcBuilder
构造MockMvc的构造器mockMvc
调用perform
,执行一个RequestBuilder
请求,调用Controller
的业务处理逻辑perform
返回ResultActions
,返回操作结果,通过ResultActions
,提供了统一的验证方式- 使用
StatusResultMatchers
对请求结果进行验证 - 使用
ContentResultMatchers
对请求返回的内容进行验证
2、MockMvcBuilder
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,可以自行百度
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 = 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");
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
:响应状态码是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
:响应状态码是511
- header():返回
HeaderResultMatchers
,访问响应头断言string
:断言响应头的主值stringValues
:断言响应头的值exists
:断言指定的响应头存在doesNotExist
:断言指定的响应头不存在longValue
:将指定响应头断言为longdateValue
:断言指定响应头解析为日期
- 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断言结果是StringisBoolean
:根据JsonPath断言结果是BooleanisNumber
:根据JsonPath断言结果是NumberisArray
:根据JsonPath断言结果是ArrayisMap
:根据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的maxAgepath
:用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
@AllArgsConstructor
public class User {
private Long id;
private String name;
private Integer age;
private Double height;
}
Dao层:
@Mapper
public interface UserMapper {
List<User> list();
Integer add(User user);
Integer update(User user);
Integer deleteById(Long id);
User getById(Long id);
}
UserMapper.xml:
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace= "pers.zhang.mapper.UserMapper" >
<insert id="add">
INSERT INTO user (name, age, height)
VALUES (#{name}, #{age}, #{height})
</insert>
<update id="update">
UPDATE user SET name = #{name}, age = #{age}, height = #{height}
WHERE id = #{id}
</update>
<delete id="deleteById">
DELETE FROM user
WHERE id = #{id}
</delete>
<select id = "list" resultType="pers.zhang.entity.User">
SELECT id, name, age, height
FROM user;
</select>
<select id="getById" resultType="pers.zhang.entity.User">
SELECT id, name, age, height
FROM user
WHERE id = #{id}
</select>
</mapper>
Service层:
public interface UserService {
List<User> 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<User> 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<User> 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);
}
}
6、分层测试
6.1、Dao层测试
在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<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)
);
}
@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);
}
}
6.2、Service层测试
上面的测试代码是连接真实数据库来执行真实的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<User> 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<User> 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...
6.3、Controller层测试
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);
}
}
7、JSON接口测试
使用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());
}
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)