共计 6060 个字符,预计需要花费 16 分钟才能阅读完成。
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.
*
* @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
支持以达到测试后回滚的目的。
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 标识)。如果跟预期相同只会出现下图 1。
5. 测试打桩
有个很常见的情形,在开发中有可能你调用的其他服务没有开发完,比如你有个短信发送接口还在办理短信接口手续,但是你还需要短信接口来进行测试。你可以通过 @MockBean
构建一个抽象接口的实现。拿上面的BookService
来说,假如其实现类逻辑还没有确定,我们可以通过规定其入参以及对应的返回值来模拟这个 bean 的逻辑,或者根据某个情形下进行某个路由操作的选择(如果入参是 A 则结果为 B,如果为 C 则 D)。这种模拟也被成为测试打桩。这里我们会用到Mockito
测试场景描述如下:
- 指定打桩对象的返回值
- 判断某个打桩对象的某个方法被调用及调用的次数
- 指定打桩对象抛出某个特定异常
一般有以下几种组合:
-
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 启用集成测试。对测试环境的搭建,测试代码的编写进行了实战操作,基本能满足日常开发测试需要,相信你能从本文学到不少东西。
相关的讲解代码可以从 gitee 获取。
也可通过我 个人博客 及时获取更多的干货分享。
关注公众号:Felordcn 获取更多资讯
个人博客:https://felord.cn