本章节将梳理一下,我们日常开发中的单元测试场景。
目录(本文主要内容)
- 需求背景
- Service 层单元测试
- DAO 测试(数据库操作,以Mybatis 为例)
- 聚合服务的情况(比如,依赖某外部API)
- Controller 层单元测试
- Demo 下载:https://github.com/wangyushuai/springboot-quick-start
- 单元测试结果:
背景
单元测试还是很有必要的
试想,比如写一个一直不怎么变更的API ,那么可能Postman 跑一下,就完事儿了,但是这绝对是极少的情况 !!
工作几年的你我都知道, 出于产品需求的变更, 业务的迭代, 代码的质量的重构等等,这些原因常常需要我们改动代码, 特别是国内的互联网公司,业务迭代频繁,代码变动更加频繁, 如果单纯的依赖 “手点功能测试”, 那简直就是灾难。
如何解决以上的开发自测问题? 没错,就是单元测试 !
下面我们就介绍一下, 工作中常用的单元测试场景!
Service层单元测试
DAO层单元测试场景
由于代码中,已经有详细注释了,在此不具体展开,只说一下注意事项:
- @Transactional: 保证了单元测试结束后,会进行数据回滚,不会产生脏数据
- @SpringBootTest : 将装载SpringBoot, 保证我的Bean 及配置正常加载
package com.example.springboot.service.impl;
import com.example.springboot.domain.TestTable;
import com.example.springboot.service.TestTableService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import static org.junit.Assert.*;
/**
* 读写数据库单元测试场景
* 由于我们的TestTableService 直接调用了Mapper,故此处我们直接基于Service 进行单元测试
* 这个场景需要保证数据正常连接(否则数据库连接池无法完成初始化)
* @author yushuai_w@163.com
* @date 2019/8/5
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestTableServiceImplTest {
@Autowired
TestTableService testTableService;
/**
* 测试写入的参数
*/
private TestTable testTable;
/**
* 每个测试方法执行前,将执行此方法
*/
@Before
public void setUp() {
// 完成对测试参数的初始化
testTable = new TestTable();
testTable.setName("unit_test");
testTable.setAge(18);
testTable.setPhone("1234567890");
testTable.setCreateTime(new Date());
}
/**
* 对数据库的添加操作进行单元测试
* 测试逻辑如下:
* 调用Add方法,写入一条数据
* 调用查询方法,判断是否写入成功
* 注解@Transactional作用为: 单元测试方法体结束后,将进行数据回滚,不会产生脏数据(数据库写入单元测试的必备良选)
*/
@Transactional
@Test
public void add() {
// 写入,写入成功后,主键Id将对testTable 赋值
testTableService.add(testTable);
// 读取(此处应该基于selectOne正确情况下)
TestTable writeResult = testTableService.selectOne(testTable.getId());
Assert.assertNotNull("写入操作是否成功",writeResult);
Assert.assertEquals("写入操作是否成功",testTable.getName(),writeResult.getName());
}
}
存在外部API依赖单元测试场景
TestRequestApiService 类中写了 调用对外API 的逻辑,在聚合服务的类(Manager)中同时含有 DB操作和调用外部API的操作,如何单元测试?
这种场景,我们一般不关注 外部API 的正确性,我们一般只需要保证 外部API 正常,我们的逻辑正常就可以了。所以,一般采用Mock ,打桩的方法,对外部API 进行模拟,拿到正确的结果后,对我们自己写的逻辑,进行断言。
SpringBoot 单元测试依赖中 内置了 Mockito, 我们可以直接使用
划重点:
- @Mock, @InjectMock (组合使用, 无需启动整个项目,速度较快)
- @SpringBootTest, @MockBean (组合使用,由于启动了项目,加载了所有依赖,只会替换Mock的bean, 其他的bean 将是正常的bean)
- given (Mockito 的静态方法) 和 willReturn
注意事项:
- 由于我们这个类中,只有两个依赖,所以不需要加载整个SpringBoot项目,所以没有使用@SpringBootTest 注解
- 所以在Mock 的使用,我使用了@Mock(生成一个Mock对象) 和 @InjectMock(注入Mock对象), 这和在有@SpringBoot注解时,@MockBean 作用时一致的。
- 但是要注意,由于manager中,只有两个Dependency,两个我都有Mock, 如果有多个,需要多个都Mock ,不然初始化会失败。
package com.example.springboot.service.impl;
import com.example.springboot.domain.TestTable;
import com.example.springboot.service.TestRequestApiService;
import com.example.springboot.service.TestTableService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.test.context.junit4.SpringRunner;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.BDDMockito.given;
/**
* 测试聚合服务的场景
* @author yushuai_w@163.com
* @date 2019/8/5
*/
@RunWith(SpringRunner.class)
public class TestManagerServiceImplTest {
/**
* Mock TestTable Service 服务
*/
@Mock
TestTableService testTableService;
/**
* Mock TestRequestApiService
*/
@Mock
TestRequestApiService testRequestApiService;
/**
* 将Mock 注入我们要测试的服务
* 注意: @InjectMocks 要使用非抽象方法
*/
@InjectMocks
TestManagerServiceImpl testManagerService;
@Test
public void fun() {
TestTable obj = new TestTable();
obj.setName("unit_test");
given(testTableService.selectOne(anyLong())).willReturn(obj);
given(testRequestApiService.getHelloDetail(anyString())).willReturn("Welcome unit_test");
// 上面我们对Manager依赖的服务,进行了Mock, 在此种条件下,我们测试的方法应该返回 true
// 故我们断言如下
Assert.assertTrue("聚合服务fun方法正常情况",testManagerService.fun(1L));
//TODO: 其他错误或异常情况,读者可以自行扩展,在此不展开,欢迎大家提问交流
}
}
Controller 层单元测试
划重点:
- @AutoConfigureMockMvc
- MockMvc mockMvc (以及他的Perform,header,andExcept等语法)
package com.example.springboot.controller;
import com.example.springboot.domain.TestTable;
import com.example.springboot.service.TestTableService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.BDDMockito;
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.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultMatcher;
import javax.validation.constraints.NotNull;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* 控制器单元测试
* @author wangyushuai2@jd.com
* @date 2019/8/18
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class HelloControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
TestTableService testTableService;
@Before
public void setUp() throws Exception {
// mock 数据库查询
TestTable mockTestTable = new TestTable();
mockTestTable.setName("unitTest");
BDDMockito.given(testTableService.selectOne(anyLong())).willReturn(mockTestTable);
}
@Test
public void helloDetail() throws Exception {
String testRequestPath = "/api/v1/hello/1";
mockMvc.perform(get(testRequestPath)
.contentType(MediaType.APPLICATION_JSON)// 设置ContentType
//.header()
.content("{}") // 设置Body
.accept(MediaType.TEXT_PLAIN)
).andExpect(status().isOk()) // 断言请求状态
.andExpect( c -> assertNotNull(c.getResponse().toString())) // 自定义ResultMatcher语法
.andExpect(content().string("unitTest"))// 直接对结果进行比较
;
}
}
参考及推荐
本文直接介绍了单元测试场景,如何使用,下面的博客有介绍语法知识,比较全,推荐
https://blog.youkuaiyun.com/qq_35915384/article/details/80227297