软件测试是一个应用软件质量的保证。java开发者开发接口每每忽视接口单元测试。做为java开发若是会Mock单元测试,那么你的bug量将会大大下降。spring提供test测试模块,因此如今小胖哥带你来玩下springboot下的Mock单元测试,咱们将对controller,service 的单元测试进行实战操做。java
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
按照上面引入依赖并且scope为test。该依赖提供了一下类库git
以上都是在单元测试中常常接触的类库。有时间你最好研究一下。web
一个Spring Boot 应用程序是一个Spring ApplicationContext
,通常测试不会超出这个范围。
测试框架提供一个@SpringBootTest
注解来提供SpringBoot单元测试环境支持。你使用的JUnit版本若是是JUnit 4
不要忘记在测试类上添加@RunWith(SpringRunner.class)
,JUnit 5
就不须要了。默认状况下,@SpringBootTest不会启动服务器。您可使用其 webEnvironment
属性进一步优化测试的运行方式,webEnvironment
相关讲解:spring
MOCK
(默认):加载Web ApplicationContext并提供模拟Web环境。该选择下不会启动嵌入式服务器。若是类路径上没有Web环境,将建立常规非Web的 ApplicationContext
。你能够配合@AutoConfigureMockMvc
或@AutoConfigureWebTestClient
模拟的Web应用程序。RANDOM_PORT
:加载 WebServerApplicationContext
并提供真实的Web环境,启用的是随机web容器端口。DEFINED_PORT
:加载 WebServerApplicationContext
并提供真实的Web环境 和 RANDOM_PORT
不一样的是启用你激活的SpringBoot应用端口,一般都声明在application.yml
配置文件中。NONE
:经过SpringApplication
加载一个ApplicationContext
。但不提供 任何 Web环境(不管是Mock或其余)。注意事项:若是你的测试带有@Transactional
注解时,默认状况下每一个测试方法执行完就会回滚事务。可是当你的 webEnvironment
设置为RANDOM_PORT
或者 DEFINED_PORT
,也就是隐式地提供了一个真实的servlet web环境时,是不会回滚的。这一点特别重要,请确保不会在生产发布测试中写入脏数据。json
言归正传,首先咱们编写了一个 BookService
做为Service 层
api
package cn.felord.mockspringboot.service; import cn.felord.mockspringboot.entity.Book; /** * The interface Book service. * * @author Dax * @since 14 :54 2019-07-23 */ public interface BookService { /** * Query by title book. * * @param title the title * @return the book */ Book queryByTitle(String title); }
其实现类以下,为了简单明了没有测试持久层,若是持久层须要测试注意增删改须要Spring事务注解@Transactional
支持以达到测试后回滚的目的。springboot
package cn.felord.mockspringboot.service.impl; import cn.felord.mockspringboot.entity.Book; import cn.felord.mockspringboot.service.BookService; import org.springframework.stereotype.Service; import java.time.LocalDate; /** * @author Dax * @since 14:55 2019-07-23 */ @Service public class BookServiceImpl implements BookService { @Override public Book queryByTitle(String title) { Book book = new Book(); book.setAuthor("dax"); book.setPrice(78.56); book.setReleaseTime(LocalDate.of(2018, 3, 22)); book.setTitle(title); return book; } }
controller层以下:服务器
package cn.felord.mockspringboot.api; import cn.felord.mockspringboot.entity.Book; import cn.felord.mockspringboot.service.BookService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * @author Dax * @since 10:24 2019-07-23 */ @RestController @RequestMapping("/book") public class BookApi { @Resource private BookService bookService; @GetMapping("/get") public Book getBook(String title) { return bookService.queryByTitle(title); } }
咱们在Spring Boot maven项目的单元测试包 test
下对应的类路径 编写本身的测试类app
package cn.felord.mockspringboot; import cn.felord.mockspringboot.entity.Book; import cn.felord.mockspringboot.service.BookService; import org.assertj.core.api.Assertions; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.BDDMockito; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit4.SpringRunner; 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; import javax.annotation.Resource; import java.time.LocalDate; /** * The type Mock springboot application tests. */ @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class MockSpringbootApplicationTests { @Resource private MockMvc mockMvc; @MockBean private BookService bookService; @Test public void bookApiTest() throws Exception { String title = "java learning"; // mockbean 开始模拟 bookServiceMockBean(title); // mockbean 模拟完成 String expect = "{\"title\":\"java learning\",\"author\":\"dax\",\"price\":78.56,\"releaseTime\":\"2018-03-22\"}"; mockMvc.perform(MockMvcRequestBuilders.get("/book/get") .param("title", title)) .andExpect(MockMvcResultMatchers.content() .json(expect)) .andDo(MockMvcResultHandlers.print()); // mockbean 重置 } @Test public void bookServiceTest() { String title = "java learning"; bookServiceMockBean(title); Assertions.assertThat(bookService.queryByTitle("ss").getTitle()).isEqualTo(title); } /** * Mock打桩 * @param title the title */ private void bookServiceMockBean(String title) { Book book = new Book(); book.setAuthor("dax"); book.setPrice(78.56); book.setReleaseTime(LocalDate.of(2018, 3, 22)); book.setTitle(title); BDDMockito.given(bookService.queryByTitle(title)).willReturn(book); } }
测试类前两个注解不用说,第三个注解@AutoConfigureMockMvc
可能大家很陌生。这个是用来开启Mock Mvc测试的自动化配置的。框架
而后咱们编写一个测试方法bookApiTest()
来测试BookApi#getBook(String title)
接口。
逻辑是 MockMvc
执行一个模拟的get请求而后指望结果是expect
Json字符串而且将相应对象打印了出来(下图1标识)。一旦请求不经过将抛出java.lang.AssertionError
错误, 会把指望值(Expected
)跟实际值打印出来(下图2标识)。若是跟预期相同只会出现下图1。
有个很常见的情形,在开发中有可能你调用的其余服务没有开发完,好比你有个短信发送接口还在办理短信接口手续,可是你还须要短信接口来进行测试。你能够经过@MockBean
构建一个抽象接口的实现。拿上面的BookService
来讲,假如其实现类逻辑尚未肯定,咱们能够经过规定其入参以及对应的返回值来模拟这个bean的逻辑,或者根据某个情形下进行某个路由操做的选择(若是入参是A则结果为B,若是为C则D)。这种模拟也被成为测试打桩。 这里咱们会用到Mockito
测试场景描述以下:
通常有如下几种组合:
doThrow(…).when(…)
/ doReturn(…).when(…)
/ doAnswer(…).when(…)
given(…).willReturn(…)
/ given(…).willAnswer(…)
when(…).thenReturn(…)
/ when(…).thenAnswer(…)
其余都好理解,着重介绍一下Answer
, Answer
正是为了解决若是入参是A则结果为B,若是为C则D这种路由操做的。接下来咱们实操一下 ,跟最开始基本同样,只是更换成@MockBean
而后利用Mockito
编写打桩方法void bookServiceMockBean(String title)
,模拟上面BookServiceImpl
实现类。不过模拟的bean每次测试完都会自动重置。并且不能用于模拟在应用程序上下文刷新期间运行的bean的行为。
而后把这个方法注入controller 测试方法就能够测试了。
内置的assertj
也是经常使用的断言,api很是友好,这里也经过bookServiceTest()
简单演示了一下
本文中实现了一些简单的Spring Boot启用集成测试。 对测试环境的搭建,测试代码的编写进行了实战操做,基本能知足平常开发测试须要,相信你能从本文学到很多东西。
相关的讲解代码能够从gitee获取。
也可经过我 我的博客 及时获取更多的干货分享。
关注公众号:Felordcn获取更多资讯