什么是测试驱动开发(Test-Driven Development)?TDD不仅仅是一门技术,更是一种可持续变更产品的实践。在开始实现一个需求时,从整体设计,将编码部分切分好,先写测试用例,再把代码写完。在团队中,测试同学和开发同学共同协作创建测试,得益于测试文档的高可读性,团队中的所有人都可以以此了解讨论产品的细节、场景。
学习测试驱动开发有什么好处?
成熟的自动化测试系统能快速验证已有的功能,因此可以显著降低产品迭代的成本。同时测试驱动开发的思路会帮助我们写出高内聚、低耦合、模块化的代码,降低思维负担,让开发团队可持续高效的产出。
自动化测试和手动测试并不互斥,自动化测试系统能够帮助我们快速的验证代码是否能正常运行。手动测试是为了更好的观察产品。自动化测试系统让我们不用再担心之前的代码还能不能正常运行,从而专注于新功能,实现高效可持续迭代。
团队实践
在团队中,一般由测试同学编写测试代码。此时如果开发同学没有配合好测试同学的话,测试同学就只能为 API层 或 UI层 去编写测试。这两层很容易变动且很难评估覆盖到的代码。比较好的实践是参照测试金字塔的理念:底层的单元测试最多;中间的功能测试次之;顶层的UI测试最少。
要想快速编写模块化的单元测试,需要代码具备高内聚、低耦合的特征。所以开发同学也需要学习自动化测试技术来提升代码的可测试性、同时帮助测试同学集成自动化测试系统。
自动化测试系统的核心功能有什么
假设我们已经编写好了测试,由于测试代码分布在不同的项目中,为了方便管理,我们需要将测试集成到自动化测试系统中。一个自动化测试系统的核心功能有:
|- 自动化测试系统
|- 配置测试任务
|- 集成已编写的测试以供执行
|- 将已编写的测试自由组合为测试任务
|- 管理不同项目的测试任务
|- 评估测试覆盖率
|- 执行测试任务
|- 分布式执行测试任务
|- 中断执行、重新执行、自动执行、定时执行
|- 查看测试报告
|- 图形化展示测试结果
|- 详细的执行日志
如何处理历史项目
实际我们手头上的项目大多是没什么测试、或者没有完整的集成测试系统的。对于这样的项目我们应该怎样渐进的处理呢?
- 为新代码编写测试,这样负担比较小,容易开始实行测试驱动开发
- 为重要或者频繁变动的模块编写测试,能够保证系统稳定且提升迭代效率,收益很高
- 在改动某一模块时编写测试,此时对代码功能还比较熟悉,编写效率高。
在前端应用的实践
为前端应用编写的测试同时包含UI测试、功能测试、单元测试。我们以一个 TODO 应用为例,需要编写的测试用例有:
TODO 应用测试用例
|- UI测试 也可以叫 端到端(E2E)测试
|- TODO任务内容及数量 的DOM和内容能正确出现在视图
|- 点击新增任务按钮 DOM能正确改变
|- 点击删除任务按钮 DOM能正确改变
|- ...
|- 功能测试 也可以叫 API测试,在前端编写可以同时测试接口和数据渲染
|- 正确调用 request 方法
|- 正确调用 获取任务列表接口 ,返回正确的内容并渲染到视图
|- 正确调用 新增、更新、删除接口 返回正确的内容并渲染到视图
|- 接口返回错误时,能正确处理
|- ...
|- 单元测试
|- 为 公共请求方法 编写单元测试
|- 为 修改URL参数 工具函数编写单元测试
|- 为 URL解析 工具函数编写单元测试
|- ...
具体测试用例代码
- UI测试示例:点击新增任务按钮 DOM能正确改变
// 技术栈 protractor、chai
it('should successfully add a task', function() {
element(by.id('name')).sendKeys('Create Quality Code');
element(by.id('date')).sendKeys('12/15/2016');
element(by.id('submit')).click();
expect(element(by.id('message')).getText())
.to.eventually.contain('task added');
expect(element(by.id('tasks')).getText())
.to.eventually.contain('Create Quality Code');
});
- 功能测试示例:正确调用 删除任务接口
// 技术栈 karma、mocha、sinon、chai
it('deleteTask should call jCallService', function(done) {
sandbox.stub(window, 'jCallService', function(params) {
expect(params.method).to.be.eql('DELETE');
expect(params.url).to.be.eql('/tasks/123412341203');
done();
});
jDeleteTask('123412341203');
});
- 单元测试示例:能够正确往URL添加参数
// 技术栈 jest
it('should successfully add query params', () => {
expect(addUrlQuery('https://www.baidu.com/home', { args: 'xxx' })).toEqual(
'https://www.baidu.com/home?args=xxx'
);
});
相关文章
在后端应用的实践
后端应用的测试用例主要包含数据库测试、路由测试、服务测试、工具函数测试。同样以 TODO 应用为例:
- 数据库测试:能够正确建立数据库连接
// 技术栈 mocha、chai、mongodb
it('connect should set connection given valid database name', function(done) {
var callback = function(err) {
expect(err).to.be.null;
expect(db.get().databaseName).to.be.eql('todotest');
db.close();
done();
};
db.connect('mongodb://localhost/todotest', callback);
});
- 路由测试:调用新增接口能够正确返回
// 技术栈 mocha、chai
it('should register URI / for post', function() {
expect(router.post.calledWith('/', sandbox.match.any)).to.be.true;
});
it("post / handler should call model's add & return success message",
function(done) {
var sampleTask = {name: 't1', month: 12, day: 1, year: 2016};
sandbox.stub(task, 'add', function(newTask, callback) {
expect(newTask).to.be.eql(sampleTask);
callback(null);
});
var req = { body: sampleTask };
var res = stubResSend('task added', done);
var registeredCallback = router.post.firstCall.args[1];
registeredCallback(req, res);
});
- 服务测试:调用 task.add 服务能够正确执行并返回 null
// 技术栈 mocha、chai
it('add should return null for valid task', function(done) {
var callback = function(err) {
expect(err).to.be.null;
task.all(function(err, tasks) {
expect(tasks[3].name).to.be.eql('a new task');
done();
});
};
task.add(sampleTask, callback);
});
- 工具函数测试:校验传入的任务信息是否合法
// 技术栈 mocha、chai
it('should return false for null name', function() {
sampleTask.name = null;
expect(validateTask(sampleTask)).to.be.false;
});
养成良好的编码习惯
什么是良好的编码习惯?其实每个人的编码习惯都不尽相同,但只要能舒适的产出高质量代码就是适合自己的好习惯。我在这里聊聊自己的编码习惯供大家参考:
在开始开发前,先将需求切分成几件具体的事情,排好每件事的时间节点,根据紧急程度适当的多估一些时间。这样阶段性的完成编码任务,能很好的控制开发风险,也能在每个阶段有些成就感和休整。
不要着急敲代码,我曾经识一个编程大牛,排期三天的需求先思考设计两天半,最后半天编码实现,而且几乎没bug。我们没必要这么极端,但是在敲代码稍作思考是有益无害的,那么思考什么呢?主要分为系统设计和代码设计两块:
1. 系统设计
通过分析需求,我们需要先确定使用场景和目标用户,写出主要的模块和它们之间的关系,再单独详细设计核心模块。
2. 代码设计
说到代码设计,就要重提我们的主题:测试驱动开发。以这个思想出发,可以帮助我们设计出高内聚、低耦合、可持续迭代的代码。我们可以先为代码思考测试用例,在每个方法前写上TODO和功能注释。这样我们在编写的时候就能很快的进入思路。
举个例子🌰:编写一个轮播图组件
我们先进行系统设计,轮播图多用于显眼的位置,在满足轮播功能的同时我们希望它动画流畅自然,能够轮播相同大小、不同内容的DOM。能够支持自动轮播、手动切换的功能。为了实现这些功能,我们需要以下模块:1.展示单个轮播项;2.轮播到指定项的方法;3.触发轮播的事件
接下来对各个模块设计代码,1.展示单个轮播项,它需要将传入的DOM正确渲染,即输入的DOM等于渲染好的DOM;2.轮播到指定项,它需要正确计算轮播到指定项的偏移量,并且平滑的切换过去。即输入轮播项索引,能正确轮播到该索引;3.触发轮播的事件,它能够在正确的时机触发轮播事件,可以是自动定时、可以是点击箭头或索引小圆点等等。
当我们分好模块,想好传入的参数和正确的行为后,再进行编码,思路就清晰多啦。
总结思考如何提效
做完一个需求后,我们可以思考整个过程中哪些步骤可以提效。比如优化系统设计;提取可复用的代码片段;编写重复操作的脚本;自测更认真等等。
- 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。