TDD测试驱动开发
作者:胡小根
邮箱:hxg@haomo-studio.com
1 What 什么是TDD
TDD,全称Test Driven Development。测试驱动开发是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。
测试驱动开发主要指 单元测试
2 Why 为什么要采用TDD
- 自动测试代码是一张安全网:让我们写的代码沙子不会散掉,对自己的代码更自信;
- 自动测试代码引导先写TODO(花轮廓),再写实现(实现细节)。确保没有理解好需求前,不要写代码;
写测试代码会拖慢开发速度吗?
不会
相反会极大加速3 Who 谁要做测试驱动开发
- 前端
- 前端复杂业务逻辑开发人员(简单的交互逻辑无需编写)
- 三方库开发人员
- 组件开发人员
- 后端
- 复杂业务逻辑开发人员
- 第三方接口对接开发人员
- 后端通用模块开发人员
- 数据库视图开发人员
谁不要做测试驱动开发:
- 初级工程师
- 前端:应该只安排做静态页面+简单交互逻辑。凡是有算法和复杂/全局业务逻辑;
- 后端:应该只安排做代码生成、简单CRUD接口;
4 When 什么时候要写和跑测试
4.1 什么时候写
4.1.1 以下情况应该写
- 前端
- 前端复杂业务逻辑开发
- 三方库开发(业内名词:学习性测试)
- 组件开发人员
- 后端
- 复杂业务逻辑开发
- 第三方接口对接开发
- 后端通用模块开发
- 数据库视图开发
自研产品,如设计云:
- 前端:
- 组件/页面开发
- 低代码编辑器开发
- 后端:
- 核心api开发
4.1.2 以下情况不建议写
- 前端
- 切图业务
- 简单的CRUD操作
- 后端
- CRUD接口
4.2 什么时候运行
- 通过CI,自动运行前端和后端代码每次git commit的测试;
- 前端由于通过CI自动部署,所以相当于每次部署前都跑了测试用例;
- 后端每次在手动部署前,需要跑一遍测试用例;
5 How 如何做测试驱动开发
遵循:红->绿->重构 循环;
三定律:
- 在编写不能通过的单元测试前,不可编写生产代码;
- 之编写刚好无法通过的单元测试,不能编译也算不通过;
- 只可编写刚好足以通过当前失败测试的生产代码;
问题:先测轮廓,还是先测细节?有轮廓没细节的话,如何让轮廓通过测试?
5.1 原则
- 测试优先:测试代码先于实现代码,不允许有没有测试代码的实现代码。
- 独立测试:不同代码的测试应该相互独立,一个类对应一个测试类(对于C代码或C++全局函数,则一个文件对应一个测试文件),一个函数对应一个测试函数。用例也应各自独立,每个用例不能使用其他用例的结果数据,结果也不能依赖于用例执行顺序。 一个角色:开发过程包含多种工作,如:编写测试代码、编写产品代码、代码重构等。做不同的工作时,应专注于当前的角色,不要过多考虑其他方面的细节。
- 测试列表:代码的功能点可能很多,并且需求可能是陆续出现的,任何阶段想添加功能时,应把相关功能点加到测试列表中,然后才能继续手头工作,避免疏漏。
- 测试驱动:即利用测试来驱动开发,是TDD的核心。要实现某个功能,要编写某个类或某个函数,应首先编写测试代码,明确这个类、这个函数如何使用,如何测试,然后在对其进行设计、编码。
- 先写断言:编写测试代码时,应该首先编写判断代码功能的断言语句,然后编写必要的辅助语句。
- 可测试性:产品代码设计、开发时的应尽可能提高可测试性。每个代码单元的功能应该比较单纯,“各家自扫门前雪”,每个类、每个函数应该只做它该做的事,不要弄成大杂烩。尤其是增加新功能时,不要为了图一时之便,随便在原有代码中添加功能,对于C++编程,应多考虑使用子类、继承、重载等OO方法。
- 及时重构:对结构不合理,重复等“味道”不好的代码,在测试通过后,应及时进行重构。
- 小步前进:软件开发是复杂性非常高的工作,小步前进是降低复杂性的好办法。
5.2 方法
F.I.R.S.T 规则:
- 快速(Fast):测试可以快速运行,这样才喜欢经常运行;
- 独立(Indenpendent):用例不互相依赖;
- 可重复(Repeatable):在任何环境中重复通过;
- 自动验证(Self-Validating):不能通过人去验证,例如去查看日志中是否生成了某条日志;
- 及时(Timely):测试先于生产代码;
5.3 工具
5.3.1 前端工具
- 单元测试:Jest
beforeEach(async () => {
page = new Page({
id: 'page1',
projectId: 'project1',
title: '测试Page',
name: 'TestPage',
type: 1
})
blockComponents = await createComponents()
})
describe('测试Page.js', () => {
it(`测试 getChildBlockComponent`, () => {
for(let i = 0; i < 10; i++) {
page.addComponent(null, blockComponents[i], page.key)
}
let componentKey = blockComponents[3].key
let component = page.getChildBlockComponent(page, componentKey)
expect(componentKey).toBe(component.key)
})
});
- 组件测试:Vue Testing Library (此框架还支持html原生、React、Angular等框架的组件测试)
test('测试', async () => {
const {container, baseElement, debug} = render(HmBgCard, {
props: {
width: '500',
height: '500'
},
slots: {
'default': 'hm bg card'
}
});
debug();
expect(container.getElementsByClassName('hm-bg-card').length).toBe(1);
const ele = container.getElementsByClassName('hm-bg-card')[0];
expect(ele.outerHTML).toBe('<div class="hm-bg-card">hm bg card</div>');
5.3.2 后端工具
public class WaterLogicVerificationTest {
/***
* 少量数据测试
*/
@Test
public void TestCalcWaterUsage(){
Double[] doubles = {868.4000000000001, 868.4000000000001, ...};
List<Double> doubleList = Arrays.asList(doubles);
VehicleAttendanceStatisticsTask vehicleAttendanceStatisticsTask = new VehicleAttendanceStatisticsTask();
double v = vehicleAttendanceStatisticsTask.calcGpsUsage(doubleList);
Assert.assertEquals(v > 1400, true);
Assert.assertEquals(v < 1700, true);
}
}
- Component测试:SpringBootTest
@RunWith(SpringRunner.class)
@SpringBootTest
public class SampleTest {
@Resource
private JeecgDemoMapper jeecgDemoMapper;
@Resource
private IJeecgDemoService jeecgDemoService;
@Resource
private ISysDataLogService sysDataLogService;
@Resource
private MockController mock;
@Test
public void testSelect() {
System.out.println(("----- selectAll method test ------"));
List<JeecgDemo> userList = jeecgDemoMapper.selectList(null);
Assert.assertEquals(5, userList.size());
userList.forEach(System.out::println);
}
}
- 数据库测试:MyTap(只支持mysql,但是postgresql有对应的库)
-- test view_user_stat
# SELECT tap.has_view(DATABASE(), 'view_user_stat', 'view_user_stat--视图存在');
# SELECT tap.columns_are(DATABASE(), 'view_user_stat', 'normal_male_num,normal_female_num,lock_male_num,lock_female_num', 'view_user_stat--字段正确');
-- view_user_stat第一条数据
set @vusNmn = (select normal_male_num from view_user_stat limit 1),
@vusNFn = (select normal_female_num from view_user_stat limit 1),
@vusLmn = (select lock_male_num from view_user_stat limit 1),
@vusLfn = (select lock_female_num from view_user_stat limit 1);
-- view_user_stat总数
set @vusCount = (select count(*) from view_user_stat);
-- test ok
SELECT tap.ok(@vusNmn = 2 and @vusNFn = 1 and @vusLmn = 0 and @vusLfn = 0 and @vusCount = 1, 'view_user_stat 数据校验');
5.4 技巧
- 数据准备
- 从接口里拿
- 打断点拷贝数据
- 边界/接口定义
- 学习性测试
- 断言
- 每个测试一个断言(或者尽量少断言);
参考
- 代码整洁之道 (公司百度云盘:01资料文档/00技术/03学习资料/07软件工程/代码整洁之道.pdf)
- SpringBoot Test及注解详解
- SpringBoot的单元测试
- Spring Boot Test
- Testing in Spring Boot 2