TA的每日心情 | 无聊 12 小时前 |
---|
签到天数: 1050 天 连续签到: 1 天 [LV.10]测试总司令
|
1. 概要
软件测试是一个应用软件质量的保证。java开发者开发接口往往忽视接口单元测试。作为java开发如果会Mock单元测试,那么你的bug量将会大大降低。spring提供test测试模块,所以现在小胖哥带你来玩下springboot下的Mock单元测试,我们将对controller,service 的单元测试进行实战操作。
2. 依赖引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
按照上面引入依赖而且scope为 test 。该依赖提供了一下类库
·JUnit 4 : 目前最强大的java应用单元测试框架
· Spring Test & Spring Boot Test : Spring Boot 集成测试支持.
· AssertJ : 一个java断言库,提供测试断言支持.
· Hamcrest : 对象匹配断言和约束组件.
· Mockito : 知名 Java mock 模拟框架.
· JSONassert : JSON断言库.
· JsonPath : JSON XPath 操作类库.
以上都是在单元测试中经常接触的类库。有时间你最好研究一下。
3. 配置测试环境
一个Spring Boot 应用程序是一个 Spring ApplicationContext ,一般测试不会超出这个范围。
测试框架提供一个 @SpringBootTest 注解来提供SpringBoot单元测试环境支持。你使用的JUnit版本如果是 JUnit 4 不要忘记在测试类上添加 @RunWith(SpringRunner.class) , JUnit 5 就不需要了。默认情况下,@SpringBootTest不会启动服务器。您可以使用其 webEnvironment 属性进一步优化测试的运行方式, webEnvironment 相关讲解:
· 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环境时,是不会回滚的。这一点特别重要,请确保不会在生产发布测试中写入脏数据。
4. 编写测试类测试你的api
言归正传,首先我们编写了一个 BookService 作为Service 层。
- package cn.felord.mockspringboot.service;
- import cn.felord.mockspringboot.entity.Book;
- /**
- * The interface Book service.
- *
- * [url=home.php?mod=space&uid=267564]@Author[/url] Dax
- * [url=home.php?mod=space&uid=295386]@since[/url] 14 :54 2019-07-23
- */
- public interface BookService {
- /**
- * Query by title book.
- *
- * @param title the title
- * [url=home.php?mod=space&uid=26358]@return[/url] the book
- */
- Book queryByTitle(String title);
- }
复制代码 其实现类如下,为了简单明了没有测试持久层,如果持久层需要测试注意增删改需要Spring事务注解 @Transactional 支持以达到测试后回滚的目的。
- 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 下对应的类路径 编写自己的测试类:
- 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标识)。如果跟预期相同只会出现下图。
5. 测试打桩
有个很常见的情形,在开发中有可能你调用的其他服务没有开发完,比如你有个短信发送接口还在办理短信接口手续,但是你还需要短信接口来进行测试。你可以通过 @MockBean 构建一个抽象接口的实现。拿上面的 BookService 来说,假如其实现类逻辑还没有确定,我们可以通过规定其入参以及对应的返回值来模拟这个bean的逻辑,或者根据某个情形下进行某个路由操作的选择(如果入参是A则结果为B,如果为C则D)。这种模拟也被成为测试打桩。 这里我们会用到 Mockito
测试场景描述如下:
1)指定打桩对象的返回值
2)判断某个打桩对象的某个方法被调用及调用的次数
3)指定打桩对象抛出某个特定异常
一般有以下几种组合:
·do/when :包括 doThrow(…).when(…) / doReturn(…).when(…) / doAnswer(…).when(…)
· given/will :包括 given(…).willReturn(…) / given(…).willAnswer(…)
· when/then : 包括 when(…).thenReturn(…) / when(…).thenAnswer(…)
其他都好理解,着重介绍一下 Answer , Answer 正是为了解决如果入参是A则结果为B,如果为C则D这种路由操作的。接下来我们实操一下 ,跟最开始基本一样,只是更换成 @MockBean ;
然后利用 Mockito 编写打桩方法 void bookServiceMockBean(String title) ,模拟上面 BookServiceImpl 实现类。不过模拟的bean每次测试完都会自动重置。而且不能用于模拟在应用程序上下文刷新期间运行的bean的行为。
然后把这个方法注入controller 测试方法就可以测试了。
6. 其他
内置的 assertj 也是常用的断言,api非常友好,这里也通过 bookServiceTest() 简单演示了一下。
7. 总结
本文中实现了一些简单的Spring Boot启用集成测试。 对测试环境的搭建,测试代码的编写进行了实战操作,基本能满足日常开发测试需要,相信你能从本文学到不少东西。
|
|