万字前端效率大提速系列 🚀 :十二、测试驱动开发(TDD)和编码习惯漫谈

615 阅读8分钟

什么是测试驱动开发(Test-Driven Development)?TDD不仅仅是一门技术,更是一种可持续变更产品的实践。在开始实现一个需求时,从整体设计,将编码部分切分好,先写测试用例,再把代码写完。在团队中,测试同学和开发同学共同协作创建测试,得益于测试文档的高可读性,团队中的所有人都可以以此了解讨论产品的细节、场景。

学习测试驱动开发有什么好处?

成熟的自动化测试系统能快速验证已有的功能,因此可以显著降低产品迭代的成本。同时测试驱动开发的思路会帮助我们写出高内聚、低耦合、模块化的代码,降低思维负担,让开发团队可持续高效的产出。

自动化测试和手动测试并不互斥,自动化测试系统能够帮助我们快速的验证代码是否能正常运行。手动测试是为了更好的观察产品。自动化测试系统让我们不用再担心之前的代码还能不能正常运行,从而专注于新功能,实现高效可持续迭代。

团队实践

在团队中,一般由测试同学编写测试代码。此时如果开发同学没有配合好测试同学的话,测试同学就只能为 API层 或 UI层 去编写测试。这两层很容易变动且很难评估覆盖到的代码。比较好的实践是参照测试金字塔的理念:底层的单元测试最多;中间的功能测试次之;顶层的UI测试最少。

image.png

要想快速编写模块化的单元测试,需要代码具备高内聚、低耦合的特征。所以开发同学也需要学习自动化测试技术来提升代码的可测试性、同时帮助测试同学集成自动化测试系统。

自动化测试系统的核心功能有什么

假设我们已经编写好了测试,由于测试代码分布在不同的项目中,为了方便管理,我们需要将测试集成到自动化测试系统中。一个自动化测试系统的核心功能有:

|- 自动化测试系统
    |- 配置测试任务
        |- 集成已编写的测试以供执行
        |- 将已编写的测试自由组合为测试任务
        |- 管理不同项目的测试任务
    |- 评估测试覆盖率
    |- 执行测试任务
        |- 分布式执行测试任务
        |- 中断执行、重新执行、自动执行、定时执行
    |- 查看测试报告
        |- 图形化展示测试结果
        |- 详细的执行日志

如何处理历史项目

实际我们手头上的项目大多是没什么测试、或者没有完整的集成测试系统的。对于这样的项目我们应该怎样渐进的处理呢?

  • 为新代码编写测试,这样负担比较小,容易开始实行测试驱动开发
  • 为重要或者频繁变动的模块编写测试,能够保证系统稳定且提升迭代效率,收益很高
  • 在改动某一模块时编写测试,此时对代码功能还比较熟悉,编写效率高。

在前端应用的实践

为前端应用编写的测试同时包含UI测试、功能测试、单元测试。我们以一个 TODO 应用为例,需要编写的测试用例有:

TODO 应用测试用例

|- UI测试 也可以叫 端到端(E2E)测试
    |- TODO任务内容及数量 的DOM和内容能正确出现在视图
    |- 点击新增任务按钮 DOM能正确改变
    |- 点击删除任务按钮 DOM能正确改变
    |- ...
|- 功能测试 也可以叫 API测试,在前端编写可以同时测试接口和数据渲染
    |- 正确调用 request 方法
    |- 正确调用 获取任务列表接口 ,返回正确的内容并渲染到视图
    |- 正确调用 新增、更新、删除接口 返回正确的内容并渲染到视图
    |- 接口返回错误时,能正确处理
    |- ...
|- 单元测试
    |- 为 公共请求方法 编写单元测试
    |- 为 修改URL参数 工具函数编写单元测试
    |- 为 URL解析 工具函数编写单元测试
    |- ...

具体测试用例代码

  1. 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');
});
  1. 功能测试示例:正确调用 删除任务接口
// 技术栈 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');
 });
  1. 单元测试示例:能够正确往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 应用为例:

  1. 数据库测试:能够正确建立数据库连接
// 技术栈 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);
});
  1. 路由测试:调用新增接口能够正确返回
// 技术栈 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);
});
  1. 服务测试:调用 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);
});
  1. 工具函数测试:校验传入的任务信息是否合法
// 技术栈 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.触发轮播的事件,它能够在正确的时机触发轮播事件,可以是自动定时、可以是点击箭头或索引小圆点等等。

当我们分好模块,想好传入的参数和正确的行为后,再进行编码,思路就清晰多啦。

总结思考如何提效

做完一个需求后,我们可以思考整个过程中哪些步骤可以提效。比如优化系统设计;提取可复用的代码片段;编写重复操作的脚本;自测更认真等等。