为什么要 Java 集成测试呢?
何为集成测试?
首先我们先了解什么是集成测试?
集成测试,也叫组装测试
或联合测试
。在单元测试
的基础上,将所有模块按照设计要求(如根据结构图)组装成为子系统或系统,进行集成测试。 ——百度百科
举个🌰,譬如下面这些测试属于 集成测试
:
- 跟
数据库
有交互 - 进行了
网络间通信
- 调用了
文件系统
- 需要你对环境作特定的准备(如编辑配置文件)才能运行的
简而言之,
强依赖外部环境
的测试,绝大部分是集成测试。
唠叨
记得 刚刚开始接触测试的时候,不以为然,觉得编写测试代码浪费时间,浪费精力。是一件吃力不讨好的事情。
但是只有自己实践起来,才知道测试的 "好"
好的测试,往往可以大大减少接口返工次数,bug 数量。
有了测试,我们可以放心的重构我们的代码,再也不用担心,改出问题了(引入新的 bug)
不信?
不如看我操作一波,感受一下 Java 集成测试的魅力
?
Login 例子
还是老样子,以 Login 接口为例。
Controller
@RestController
public class UserController {
@Autowired
private LoginService loginService;
@PostMapping("/login")
public BaseResponse<Boolean> login(@RequestBody LoginCommand command) {
return BaseResponse.OK(loginService.login(command));
}
}
Service
@Service
public class LoginService {
@Autowired
private UserRepository userRepository;
public boolean login(LoginCommand command) {
Optional<User> userOptional = userRepository.findByUsernameAndPassword(command.getUsername(), command.getPassword());
return userOptional.isPresent();
}
}
LoginRepository 使用的是 Jpa,这里就不多赘述了。
三部曲
我们在 test/java/xxx(包名)/ 下,新建一个 LoginITest 测试类,如下所示:
LoginItTest,其中 It 为 Integration Test 简写,集成测试
代码
public class LoginITest {
@Test
public void should_return_login_response_when_request_login_interface() {
// give
// when
// then
}
}
方法名,这里是参照 TDD命名方式,是用下划线命名。详细描述 该方法是测什么
这里使用 测试三部曲模板
- give:给定参数,准备条件
- when:当执行 xxxMethod 方法时
- then:期望的结果(断言)。
Give、When、Then 可以使用 Live Templates 快速生成。
然后基于 三部曲,编写测试代码:
测试登录成功
@SpringBootTest
@AutoConfigureMockMvc
public class LoginITest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper mapper;
@Test
public void should_return_login_success_when_request_login_interface() {
// give
// 数据库增加该记录
LoginCommand command = new LoginCommand();
command.setUsername("张三");
command.setPassword("123456");
// when
BaseResponse<Boolean> baseResponse = null;
try {
// Post 请求,并传入 请求 url
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/login")
.contentType(MediaType.APPLICATION_JSON) // 设置请求类型
.content(mapper.writeValueAsString(command)); // 设置入参,通过 mapper,把 入参 LoginCommand 转换为 String类型
String response = this.mockMvc.perform(builder)
.andExpect(MockMvcResultMatchers.status().isOk()) //设置期望返回状态码,isOk为 200
.andDo(MockMvcResultHandlers.print()) // 打印 请求日志
.andReturn()
.getResponse()
.getContentAsString();
// 把 String 结果,转化为具体 BaseResponse 对象。
baseResponse = this.mapper.readValue(response, new TypeReference<BaseResponse<Boolean>>() {
});
} catch (Exception e) {
e.printStackTrace();
}
// then
assertNotNull(baseResponse);
assertTrue(baseResponse.getData());
}
}
在测试类中,引入注解:
-
@SpringBootTest: 配置 SpringBoot 启动所需的上下文环境
-
@AutoConfigureMockMvc : 自动装配 MockMvc
以上是一个完整的测试请求流程,具体参数解释,如上所示
有了请求结果,我们就可以进行 断言,即 判断结果是否符合预期值
。
// then
assertNotNull(baseResponse); // 返回结果不为空
assertTrue(baseResponse.getData()); // 返回 Data 为 True,登录成功
最后运行测试,测试通过。
看到这里的朋友可能会吐槽到:就这???
这么麻烦,还需要写这么一大堆测试代码,值得吗?
至于测试代码
- 代码这方面,我们可以优化的嘛,如抽取一个通用的基类,就可减少请求流程代码了。亦或者抽取通用的 common-test 模式进行复用
至于值得吗? 我们对比以前两种做法:
-
比起我们写完接口,直接扔给前端,进行
联调测试
:- 这样依赖前端进行联调,还需要手动操作,点开页面进行接口测试
- 那如果接口前端没有对上咧?
-
亦或者 直接用
Postman/Swagger
,点点点测试-
通过 Postman 视乎可以达到测试接口效果,但是往往不是那么高效,入参不能很好模拟,需要手动输入。
-
入参代码可以复用,但是需要基于 Postman 进行编程,视乎没有 Java 集成测试那么贴切。
-
数据库无法隔离,会产生很多测试数据。
-
而编写集成测试:
- 使用 Java 语言,SpringBoot 自带 Test 测试依赖。
- 可以引入 H2 数据库,进行数据隔离,可重复运行。
- 可以进行代码复用,抽取属于自己的 common-base 模块
- 自己写的接口,自己测,不依赖别人!
这样看下来,你觉得值得吗?
重构
@SpringBootTest
@AutoConfigureMockMvc
public class BaseITest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper mapper;
public <T> BaseResponse<T> request(String url, Object object, TypeReference<BaseResponse<T>> typeReference) {
BaseResponse<T> baseResponse = null;
try {
MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post(url)
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(object));
String response = this.mockMvc.perform(builder)
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn()
.getResponse()
.getContentAsString();
baseResponse = this.mapper.readValue(response, typeReference);
} catch (Exception e) {
e.printStackTrace();
}
return baseResponse;
}
}
首先我们先抽取 通用基类,BaseITest
类,把 请求方法 移至 BaseITest 类中,如上所示
然后我们在 LoginITest 中调用变成:
@Test
public void should_return_login_response_when_request_login_interface() {
// give
LoginCommand command = new LoginCommand();
command.setUsername("张三");
command.setPassword("123456");
// when
BaseResponse<Boolean> baseResponse = request("/login", command, new TypeReference<BaseResponse<Boolean>>() {
});
// then
assertNotNull(baseResponse);
assertTrue(baseResponse.getData());
}
传入请求的 url
、参数
、返回结果类型
,即可获取 返回结果,BaseResposne
重构后,运行测试,测试正常通过。
然后我可以继续下一个测试用例, 测试登录失败的情况。
测试登录失败
@Test
public void should_return_login_fail_when_request_login_interface() {
// give
LoginCommand command = new LoginCommand();
command.setUsername("张三");
command.setPassword("123"); // 测试输入错误密码
// when
BaseResponse<Boolean> baseResponse = request("/login", command, new TypeReference<BaseResponse<Boolean>>() {
});
// then
assertNotNull(baseResponse);
assertFalse(baseResponse.getData()); // 返回 Data 为 False,登录失败
}
运行测试,测试通过,相信看到这里的朋友,应该都看得懂吧。
依赖
测试视乎进展的很顺利。
但是这里会隐藏了一个 严重的问题
。
如果我把数据库 张三这条数据 删了
,或者修改
这条数据的 账号或密码。
那岂不是 我们的测试 就 不通过了 ?
每次测试,都需要去数据库手动添加一条记录?
那我们测试 就变得很脆弱,强依赖数据库。
所以这里,我们还需要引入 一个 内存数据库 (H2)
。
H2
好处
隔离
我们真实的数据库:内存数据库,就是数据库文件存在于内存中,没有持久化
,当应用进程关闭时数据库与数据表会消失。所以我们可以在 H2 中,增删改查,都不会影响到 我们 真实的数据库,这样大大方便我们进行测试。- H2 配置上 MySQL 模式,几乎可以 “完美” 兼容MySQL 语法(但是会有一点点小区别)
引入依赖
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
并且在 /test/resources 包下 建立一个 测试环境的 yml ,可以在这里配置 h2 相关配置。
spring:
datasource:
name: database
platform: h2
driver-class-name: org.h2.Driver
# 配置 MODE=MySQL 模式
url: jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;
username: sa # 默认为 sa
password: # 默认为空
jpa:
show-sql: true
hibernate:
ddl-auto: update # Jpa默认为 update,没有表新建,有表更新操作。
Domain 配置上 @Entity,H2 数据库可以基于 Jpa 自动创建表
代码
接下来对测试代码,进行修改,由于是 内存数据库,所以每次启动都是 一个 新的数据库
,所以我们需要在测试中构造 一个 测试账号。
@Test
public void should_return_login_success_when_request_login_interface() {
// give
// 构建一个测试账号User
User user = new User();
user.setUsername("李四");
user.setPassword("1234");
userRepository.save(user);
LoginCommand command = new LoginCommand();
command.setUsername("李四");
command.setPassword("1234");
// when
BaseResponse<Boolean> baseResponse = request("/login", command, new TypeReference<BaseResponse<Boolean>>() {
});
// then
assertNotNull(baseResponse);
assertTrue(baseResponse.getData());
}
运行测试,测试通过。
扩展
后续可能会有 注册相关等测试需要使用到 User,所以可以把 创建 User 抽取一个 UserFixture ,代码如下所示
UserFixture,User 的
夹具
,参照 DDD 架构模式。
public class UserFixture {
public static User create() {
User user = new User();
user.setUsername("李四");
user.setPassword("1234");
return user;
}
}
// 使用
User user = UserFixture.create();
userRepository.save(user);
总结
在我们编写完接口后,我们不妨编写对应的接口测试,以此来保证接口质量。
在编写测试时,应该遵循 三部曲,划分好 入参
、测试方法
、断言
,对应的是 Give、When、Then
在编写集成测试时,应当采用 H2
数据库,避免测试数据污染 真实数据库。
在编写测试代码时,切勿忽视测试代码,测试代码也需要重构,优化,编写出优雅的测试代码,利于我们理解测试,不然都不知道再测啥 :)
最后
以上就是全部内容,希望能帮助到你~
如有不妥,欢迎指出,大家一起交流学习。
感谢阅读,下次再见。