浅谈前端单元测试

193 阅读11分钟
原文链接: click.aliyun.com

    首先声明一点,长期以来,前端开发的单元测试并不是在前端的开发过程中所必须的,也不是每个前端开发工程师所注意和重视的,甚至扩大到软件开发过程中单元测试这一环也不是在章程上有书面规定所要求的。但是随着每个工程的复杂化、代码的高复用性要求和前端代码模块之间的高内聚低耦合的需求,前端工程中的单元测试流程就显得很有其必要。

1.前端单元测试是什么

      首先我们要明确测试是什么:

       为检测特定的目标是否符合标准而采用专用的工具或者方法进行验证,并最终得出特定的结果。

       对于前端开发过程来说,这里的特定目标就是指我们写的代码,而工具就是我们需要用到的测试框架(库)、测试用例等。检测处的结果就是展示测试是否通过或者给出测试报告,这样才能方便问题的排查和后期的修正。

       基于测试“是什么”的说法,为便于刚从事前端开发的同行的进阶理解,那我们就列出单元测试它“不是什么”:

需要访问数据库的测试不是单元测试

需要访问网络的测试不是单元测试

需要访问文件系统的测试不是单元测试

--- 修改代码的艺术

对于单元测试“不是什么”的引用解释,至此点到为止。鉴于篇幅限制,对于引用内容,我想前端开发的同行们看到后会初步有一个属于自己的理解。

2.单元测试的意义以及为什么需要单元测试

2.1   单元测试的意义

      对于现在的前端工程,一个标准完整的项目,测试是非常有必要的。很多时候我们只是完成了项目而忽略了项目测试的部分,测试的意义主要在于下面几点:

  1. TDD(测试驱动开发) 被证明是有效的软件编写原则,它能覆盖更多的功能接口。
  2. 快速反馈你的功能输出,验证你的想法。
  3. 保证代码重构的安全性,没有一成不变的代码,测试用例能给你多变的代码结构一个定心丸。
  4. 易于测试的代码,说明是一个好的设计。做单元测试之前,肯定要实例化一个东西,假如这个东西有很多依赖的话,这个测试构7. 造过程将会非常耗时,会影响你的测试效率,怎么办呢?要依赖分离,一个类尽量保证功能单一,比如视图与功能分离,这样的话,你的代码也便于维护和理解。

2.2   为什么需要单元测试

  1. 首先是一个前端单元测试的根本性原由:JavaScript 是动态语言,缺少类型检查,编译期间无法定位到错误; JavaScript 宿主的兼容性问题。比如 DOM 操作在不同浏览器上的表现。
  2. 正确性:测试可以验证代码的正确性,在上线前做到心里有底。
  3. 自动化:当然手工也可以测试,通过console可以打印出内部信息,但是这是一次性的事情,下次测试还需要从头来过,效率不能得到保证。通过编写测试用例,可以做到一次编写,多次运行。
  4. 解释性:测试用例用于测试接口、模块的重要性,那么在测试用例中就会涉及如何使用这些API。其他开发人员如果要使用这些API,那阅读测试用例是一种很好地途径,有时比文档说明更清晰。
  5. 驱动开发,指导设计:代码被测试的前提是代码本身的可测试性,那么要保证代码的可测试性,就需要在开发中注意API的设计,TDD将测试前移就是起到这么一个作用。
  6. 保证重构:互联网行业产品迭代速度很快,迭代后必然存在代码重构的过程,那怎么才能保证重构后代码的质量呢?有测试用例做后盾,就可以大胆的进行重构。

3.如何写单元测试用例

3.1 原则

  • 测试代码时,只考虑测试,不考虑内部实现
  • 数据尽量模拟现实,越靠近现实越好
  • 充分考虑数据的边界条件
  • 对重点、复杂、核心代码,重点测试
  • 利用AOP(beforeEach、afterEach),减少测试代码数量,避免无用功能
  • 测试、功能开发相结合,有利于设计和代码重构

3.2 两个常用的单元测试方法论

   在单元测试中,常用的方法论有两个:TDD(测试驱动开发)&BDD(行为驱动开发)

   对于之前没听说过前端测试这两个模式的同行可以在此了解一下,篇幅限制此处不再敖述。

3.3 相信你看完之后也有一个自己对TDD和BDD的个人观点,在此我先谈谈我对TDD和BDD的 理解:

TDD(Test-driven development)

其基本思路是通过测试来推动整个开发的进行。

  • 单元测试的首要目的不是为了能够编写出大覆盖率的全部通过的测试代码,而是需要从使用者(调用者)的角度出发,尝试函数逻辑的各种可能性,进而辅助性增强代码质量

  • 测试是手段而不是目的。测试的主要目的不是证明代码正确,而是帮助发现错误,包括低级的错误

  • 测试要快。快速运行、快速编写

  • 测试代码保持简洁

  • 不会忽略失败的测试。一旦团队开始接受1个测试的构建失败,那么他们渐渐地适应2、3、4或者更多的失败。在这种情况下,测试集就不再起作用

需要注意的是

  • 一定不能误解了TDD的核心目的!

  • 测试不是为了覆盖率和正确率

  • 而是作为实例,告诉开发人员要编写什么代码

  • 红灯(代码还不完善,测试挂)-> 绿灯(编写代码,测试通过)-> 重构(优化代码并保证测试通过)

TDD的过程是

  1. 需求分析,思考实现。考虑如何“使用”产品代码,是一个实例方法还是一个类方法,是从构造函数传参还是从方法调用传参,方法的命名,返回值等。这时其实就是在做设计,而且设计以代码来体现。此时测试为红

  2. 实现代码让测试为”绿灯“

  3. 重构,然后重复测试

  4. 最终符合所有要求即:

    • 每个概念都被清晰的表达

    • 代码中无自我重复

    • 没有多余的东西

    • 通过测试

BDD(Behavior-driven development):

行为驱动开发(BDD),重点是通过与利益相关者(简单说就是客户)的讨论,取得对预期的软件行为的认识,其重点在于沟通

BDD过程是:

  1. 从业务的角度定义具体的,以及可衡量的目标

  2. 找到一种可以达到设定目标的、对业务最重要的那些功能的方法

  3. 然后像故事一样描述出一个个具体可执行的行为。其描述方法基于一些通用词汇,这些词汇具有准确无误的表达能力和一致的含义。例如,expect, should, assert

  4. 寻找合适语言及方法,对行为进行实现

  5. 测试人员检验产品运行结果是否符合预期行为。最大程度的交付出符合用户期望的产品,避免表达不一致带来的问题

4. Mocha/Karma+Travis.CI的前端测试工作流

      以上内容从什么是单元测试谈到单元测试的方法论。那么怎样用常用框架进行单元测试?单元测试的工具环境是什么?单元测试的实际示例是怎样的?

      首先应该简单介绍一下Mocha、Karma和Travis.CI

Mocha:mocha 是一个功能丰富的前端测试框架。所谓"测试框架",就是运行测试的工具。通过它,可以为JavaScript应用添加测试,从而保证代码的质量。mocha 既可以基于 Node.js 环境运行 也可以在浏览器环境运行。欲了解更多可去官方网站进行学习。其官方介绍为:

Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases. Hosted on GitHub.

Karma:一个基于Node.js的JavaScript测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流Web浏览器,也可集成到CI(Continuous integration)工具,也可和其他代码编辑器一起使用。这个测试工具的一个强大特性就是,它可以监控文件的变化,然后自行执行,通过console.log显示测试结果。Karma的一个强大特性就是,它可以监控一套文件的变换,并立即开始测试已保存的文件,用户无需离开文本编辑器。测试结果通常显示在命令行中,而非代码编辑器。这也就让 Karma 基本可以和任何 JS 编辑器一起使用。

Travis.CI: 提供的是持续集成服务(Continuous Integration,简称 CI)。它绑定 Github 上面的项目,只要有新的代码,就会自动抓取。然后,提供一个运行环境,执行测试,完成构建,还能部署到服务器。

持续集成指的是只要代码有变更,就自动运行构建和测试,反馈运行结果。确保符合预期以后,再将新代码"集成"到主干。

持续集成的好处在于,每次代码的小幅变更,就能看到运行结果,从而不断累积小的变更,而不是在开发周期结束时,一下子合并一大块代码。

对于Travis.CI,建议移步到阮大大廖大大的个人网站上学习,两位老师讲的要比我在这儿写的更清晰。

断言库

     基本工具框架介绍完毕后,相信稍微了解点测试的同行都知道,做单元测试是需要写测试脚本的,那么测试脚本就需要用到断言库。”断言“,个人理解即为”用彼代码断定测试此代码的正确性,检验并暴露此代码的错误。“那么对于前端单元测试来说,有以下常用断言库:

看一段代码示例:

expect(add(1, 1)).to.be.equal(2);

这是一句断言代码。

所谓"断言",就是判断源码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误。上面这句断言的意思是,调用 add(1, 1),结果应该等于 2。所有的测试用例(it 块)都应该含有一句或多句的断言。它是编写测试用例的关键。断言功能由断言库来实现,Mocha 本身不带断言库,所以必须先引入断言库。

引入断言库代码示例:

var expect = require('chai').expect;

断言库有很多种,Mocha 并不限制使用哪一种,它允许你使用你想要的任何断言库。上面代码引入的断言库是 chai,并且指定使用它的 expect 断言风格。下面这些常见的断言库:

此处主要介绍一下node assert中常用的API

  • assert(value[, message])
  • assert.ok(value[, message])
  • assert.equal(actual, expect[, message])
  • assert.notEqual(actual, expected[, message])
  • assert.strictEqual(actual, expect[, message])
  • assert.notStrictEqual(actial, expected[, message])
  • assert.deepEqual(actual, expect[, message])
  • assert.notDeepEqual(actual, expected[, message])
  • assert.deepStrictEqual(actual, expect[, message])
  • assert.notDeepStrictEqual(actual, expected[, message])
  • assert.throws(block[, error][, message])
  • assert.doesNotThrow(block[, error][, message])

assert(value[, message])

断言 value 的值是否为true,这里的等于判断使用的是 == 而不是 ===。message 是断言描述,为可选参数。

const assert = require('assert');
assert(true);
复制代码

assert.ok(value[, message])

使用方法同 assert(value[, message])

assert.equal(actual, expect[, message])

预期 actual 与 expect值相等。equal用于比较的 actual 和 expect 是基础类型(string, number, boolearn, null, undefined)的数据。其中的比较使用的是 == 而不是 ===。