什么是单元测试?
一个单元测试是一段代码(方法),调用另一段代码(方法),然后校验(断言)某些假设的正确性,单元测试主要是验证 SUT ( system under test )本身的实现逻辑的正确性
一个优秀的单元测试应该具备哪些条件?
- 快速
- 容易编写、易读
- 可重复执行的
- 覆盖被测方法的所有分支
- 结果是稳定的
- 无论时间间隔多久得到的结果是相同的
单元测试有哪些好处?
单元测试可以说是项目稳定运行的保障,一套成熟的单元测试系统不仅可以增加项目的稳定性、健壮性,同时也能增加你对自己代码的信心,方便你的后期维护、重构等工作。
或者当你维护千级别甚至万级别代码,时不时的需要修bug,更严重的是该程序还是跑在商业服务器上,出问题往往意味着真金白银的时候,你改动一点点可能就要考虑半天,这时候你可能会切身体会到单元测试的好处了
不使用单元测试行不行?
一部分人应该经历过维护老项目,经常出现改了A,B出问题了,就像下图中的小熊。 为什么会这样呢?我想这其中一很大部分是你并不清楚上一个人这块为什么这么写,这样写解决什么问题,这里不用我说想必大家也知道如果这里有单元测试,那么改之前和改之后有没有影响其他地方应该一目了然了吧。
测试,不仅仅关于未知
说起测试,往往与未知相关联。我们通过试验、调试、检测来获取获取反馈,不断调整。
误区
很多人没搞明白单元测试和集成测试的区别, postman测试的接口一般就是集成测试。
- 单元测试:测试的是程序员写的代码逻辑,函数拆分、架构是否合理等,代码对于程序员来说是透明的,可以做代码逻辑的覆盖性测试,做逻辑覆盖,使用 mock 就可以了,不需要连接数据库
- 集成测试:一般是集成数据库、MQ等,端到端(End-to-End,这里简单理解为:从需求发起,到需求满足的全程)的测试:属于黑盒、灰盒测试的范畴,不属于单元测试了,
在公司中如何应用单元测试?
- 代码覆盖率
- 代码审查
代码覆盖率
项目中代码覆盖率至少达到80%以上,如果严格些100%也是可以,但是一般情况下单元测试并不能很好的测试全部方法,例如一些支付成功回调接口。
代码审查
单纯使用代码覆盖率并不能保证你的项目真正应用了单元测试,如果没有一个好的制度管理约束那么只会使项目更乱,没有代码审查你可能都发现不了竟然有人写的单元测试连断言都没有。
我认为一个稳定项目的背后必然少不了代码审查这个流程,没有这项流程你可能会错过学习和提高生产力中很大很有趣的一部分,代码审查可以帮助你创造出可读,高质量、能持续使用多年的代码,并让你充满自信。
世间万物讲究个循环,正向和恶向,正确的方式、引导会使你创作的孩子越来越好正向循环,反之亦然。
如果你的公司没有单元测试,那么你可以工作部门变革的倡导者去尝试提出建议和优化,这种变革其实更多的是同事们心理上的改变,而非技术上的,人都是不喜欢改变的,因为那通常伴随着大量的害怕、不确定和怀疑。
当领导接受了你的建议,那么你必然成为这个领取的领导者,你会发现你将进入正向循环中,在此领域你会发挥出你的力量,让自己深耕这片领域。
请记住:机会都是争取来的。
单元测试
单元测试是从测试最小单元为维度展开,也就是只测一个方法,只关注代码逻辑,其他的都不关心,这里我使用mockito+junit为案例演示 参考 Github
配置Mockito和MockMvc
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
public class UserControllerUnitTest {
private MockMvc mockMvc;
/**
* 被测类中用的类使用@Mock模拟
*/
private UserService userService;
/**
* 模拟被测类
*/
private UserController userController;
public void init(){
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders
.standaloneSetup(userController)
.addFilters(new CORSFilter())
.build();
}
public void test_page() throws Exception {
/*MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/welcomeController").param("pn", "5"))
.andReturn();
//请求成功以后,请求域中会有pageInfo;我们可以取出pageInfo进行验证
MockHttpServletRequest request = result.getRequest();
PageInfo pi = (PageInfo) request.getAttribute("pageInfo");
assertEquals(1,pi.getPageNum());*/
}
}
查询所有数据
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
@Test
public void test_get_all_success() throws Exception {
List<User> users = Arrays.asList(
new User(1, "Foo"),
new User(2, "Bar"));
// 执行userService.getAll()时,返回users,这里是为了模拟/user接口的userService调用getAll返回的数据
when(userService.getAll()).thenReturn(users);
mockMvc.perform(get("/user"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].id", is(1)))
.andExpect(jsonPath("$[0].username", is("Foo")))
.andExpect(jsonPath("$[1].id", is(2)))
.andExpect(jsonPath("$[1].username", is("Bar")));
// 验证调用次数
verify(userService, times(1)).getAll();
verifyNoMoreInteractions(userService);
}
集成测试
集成测试每次测试都需要启动一次容器
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
SpringRunner.class)
public class ResourceServiceTest {
private ResourceService resourceService;
public void findResourceByCondition() {
QueryVO queryVO = new QueryVO();
List<Resource> dictByCondition = resourceService.findResourceByCondition(new Resource(), queryVO);
Assert.assertTrue(dictByCondition.size() > 0);
}
}
(实际开发中业务并非如此简单,可能涉及三方、MQ、等中间件服务,其实这也是上文所说为什么测试覆盖率80%也可以的原因。具体测试案例需要根据业务需求来定。
如何进行TDD
说到单元测试就离不开TDD,TDD:Test Driven Development(测试驱动开发),强调测试优先开发:红-绿-黑(重构)循环,具体参考下图
TDD更大意义在于驱动接口,而不仅仅是驱动代码细节
TDD要求根据需求先写粗粒度的功能测试,这些测试能驱动出程序的接口, 然后写细粒度的单元测试,驱动出细节代码
流程
- 写一个失败的测试用例,这一步通常测试接口而非实现,重点在于被测单元需要完成什么功能
- 写一点代码实现,让这个测试通过
- 重构代码(如果需要的话),转到第一步
自动化构建、部署、发布
最后分享一下我的博客后端 仓库 中包含一条线流程图,此发布线相对简单但也基本流程都包括了
如果你在阅读过程中有自己的见解或观点,烦请不吝指教,谢谢。