浅谈前端单元测试

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

1.前端单元测试是什么

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

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

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

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

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

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

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

--- 修改代码的艺术面试

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

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个测试的构建失败,那么他们渐渐地适应二、三、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)的数据。其中的比较使用的是 == 而不是 ===。

it('assert.equal', () => {
  assert.equal(null, false, 'null compare with false');  // 报错
  assert.equal(null, true, 'null compare with true');  // 报错
  assert.equal(undefined, false, 'undefined compare with false'); // 报错
  assert.equal(undefined, true, 'undefined compare with true'); // 报错
  assert.equal('', false, '"" compare with false');  // 正常
})
复制代码

notEqual(actual, expected[, message])

用法同 assert.equal(actual, expect[, message]) 只是对预期结果取反(即不等于)。

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

用法同 assert.equal(actual, expect[, message]) 可是内部比较是使用的是 === 而不是 ==。

assert.notStrictEqual(actial, expected[, message])

用法同 assert.strictEqual(actual, expect[, message]) 只是对预期结果取反(即不严格等于)。

it('assert.strictEqual', () => {
  assert.strictEqual('', false); // 报错
})
复制代码

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

deepEqual 方法用于比较两个对象。比较的过程是比较两个对象的 key 和 value 值是否相同, 比较时用的是 == 而不是 ===。

it('assert.deepEqual', () => {
  const a = { v: 'value' };
  const b = { v: 'value' };
  assert.deepEqual(a, b);
})
复制代码

assert.notDeepEqual(actual, expected[, message])

用法同 assert.deepEqual(actual, expect[, message]) 只是对预期结果取反(即不严格深等于)。

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

用法同 assert.deepEqual(actual, expect[, message]) 可是内部比较是使用的是 === 而不是 ==。

assert.notDeepStrictEqual(actual, expected[, message])

用法同 assert.deepStrictEqual(actual, expect[, message]) 只是对结果取反(即不严格深等于)。

assert.throws(block[, error][, message])

错误断言与捕获, 断言指定代码块运行必定会报错或抛出错误。若代码运行未出现错误则会断言失败,断言异常。

it('throws', () => {
  var fun = function() {
    xxx
  };
  assert.throws(fun, 'fun error');
})
复制代码

assert.doesNotThrow(block[, error][, message])

错误断言与捕获, 用法同 throws 相似,只是和 throws 预期结果相反。断言指定代码块运行必定不会报错或抛出错误。若代码运行出现错误则会断言失败,断言异常。

it('throws', () => {
  var fun = function() {
    xxx
  };
  assert.doesNotThrow(fun, 'fun error');
})复制代码


相应的工具介绍以后,针对Mocha、Karma以及Travis.CI的用法谈点我的在操做时的实践经验。

Mocha

  • 安装mocha
npm install mocha -g
复制代码

固然也能够在不在全局安装,只安局部安装在项目中

npm install mocha --save
复制代码
  • 建立一个测试文件 test.js
var assert = require('assert')

describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return -1 when the value is not present', function() {
      assert.equal(-1, [1, 2, 3].indexOf(-1))
    })
  })
})
复制代码

这段文件和简单就是测试 Array 的一个 indexOf() 方法。这里我是用的断言库是 Node 所提供的 Assert 模块里的API。这里断言 -1 等于 数组 [1, 2, 3] 执行 indexOf(-1)后返回的值,若是测试经过则不会报错,若是有误就会报出错误。

下面咱们使用全局安装的 mocha 来运行一下这个文件 mocha test.js
下面是返回结果


基础测试用例实例

const assert = require('assert');

describe('测试套件描述', function() {
  it('测试用例描述: 1 + 2 = 3', function() {
    // 测试代码
    const result = 1 + 2;
    // 测试断言
    assert.equal(result, 3);
  });
});
复制代码

Mocha 测试用例主要包含下面几部分:

  1. describe 定义的测试套件(test suite)
  2. it 定义的测试用例(test case)
  3. 测试代码
  4. 断言部分

说明:每一个测试文件中能够有多个测试套件和测试用例。mocha不只能够在node环境运行, 也能够在浏览器环境运行;在node中运行也能够经过npm i mocha -g全局安装mocha而后以命令行的方式运行测试用例也是可行的。

这里略微详细介绍下测试脚本写法

Mocha 的做用是运行测试脚本,首先必须学会写测试脚本。所谓"测试脚本",就是用来测试源码的脚本。下面是一个加法模块 add.js 的代码。

// add.js
function add(x, y) {
  return x + y;
}

module.exports = add;
复制代码

要测试这个加法模块是否正确,就要写测试脚本。一般,测试脚本与所要测试的源码脚本同名,可是后缀名为.test.js(表示测试)或者.spec.js(表示规格)。好比,add.js 的测试脚本名字就是 add.test.js。

// add.test.js
var add = require('./add.js');
var expect = require('chai').expect;

describe('加法函数的测试', function() {
  it('1 加 1 应该等于 2', function() {
    expect(add(1, 1)).to.be.equal(2);
  });
});
复制代码

上面这段代码,就是测试脚本,它能够独立执行。测试脚本里面应该包括一个或多个 describe 块,每一个 describe 块应该包括一个或多个 it 块。

describe 块称为"测试套件"(test suite),表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称("加法函数的测试"),第二个参数是一个实际执行的函数。

it 块称为"测试用例"(test case),表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称("1 加 1 应该等于 2"),第二个参数是一个实际执行的函数。


expect 断言的优势是很接近天然语言,下面是一些例子。

// 相等或不相等
expect(4 + 5).to.be.equal(9);
expect(4 + 5).to.be.not.equal(10);
expect(foo).to.be.deep.equal({ bar: 'baz' });

// 布尔值为true
expect('everthing').to.be.ok;
expect(false).to.not.be.ok;

// typeof
expect('test').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');
expect(foo).to.be.an.instanceof(Foo);

// include
expect([1, 2, 3]).to.include(2);
expect('foobar').to.contain('foo');
expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo');

// empty
expect([]).to.be.empty;
expect('').to.be.empty;
expect({}).to.be.empty;

// match
expect('foobar').to.match(/^foo/);
复制代码

基本上,expect 断言的写法都是同样的。头部是 expect 方法,尾部是断言方法,好比 equal、a/an、ok、match 等。二者之间使用 to 或 to.be 链接。若是 expect 断言不成立,就会抛出一个错误。事实上,只要不抛出错误,测试用例就算经过。

it('1 加 1 应该等于 2', function() {});
复制代码

上面的这个测试用例,内部没有任何代码,因为没有抛出了错误,因此仍是会经过。


Karma

基于 karma 测试经常使用的一些模块

模块安装

# 基础测试库
npm install karma-cli -g
npm install karma mocha karma-mocha --save-dev

# 断言库
npm install should --save-dev
npm install karma-chai --save-dev

# 浏览器相关
npm install karma-firefox-launcher --save-dev
npm install karma-chrome-launcher --save-dev
复制代码

配置

这里的配置主要关注的是karma.conf.js的相关配置。若是要使用 karma 和 mocha 最好经过npm install karma-cli -g全局安装karma-cli具体配置配置说明

须要注意的两个字段:

  • singleRun: 若是值为 true, 则在浏览器运行完测试后会自动退出关闭浏览器窗口。singleRun的值咱们能够更具运行环境来动态赋值, 能够启动命令中添加NODE_ENV变量。
  • browsers: 浏览器配置(能够配置多个浏览器); 若是浏览器没法启动须要进行相关浏览器的配置。设置自启动浏览器时候若是浏览器启动失败可能须要设置为--no-sandbox模式。
{
  "browsers": ["Chrome", "ChromeHeadless", "ChromeHeadlessNoSandbox"],
  "customLaunchers": {
    "ChromeHeadlessNoSandbox": {
      "base": "ChromeHeadless",
      "flags": ["--no-sandbox"]
    }
  }
}
复制代码

或者

{
  "browsers": ["Chrome_travis_ci"],
  "customLaunchers": {
    "Chrome_travis_ci": {
      "base": "Chrome",
      "flags": ["--no-sandbox"]
    }
  }
}复制代码


Github项目接入Travis.CI进行集成自动化测试的步骤

  • 在github建立并完成一个能够待测试的项目。这里的完成是指须要完成基本的项目功能,和测试用例代码。
  • 配置travis-ci能识别读取的配置文件,这样travis-ci接入的时候才可以知道测试时的一些配置。
  • github 和 travis-ci 是个站点,换句话说就是两个东西若是能打通呢。须要用户登陆 travis-ci 并受权访问到你的 github 项目并进行相关的项目设置。
  • 接入完成后就能够根据本身的须要来运行写好的测试代码,也能够设置按期任务去跑测试。

  • 项目建立、完善项目功能和测试代码。

    • 项目需求: 实现一个求和方法
    • 测试: 经过 mocha 来测试完成的求和方法。

    下面是项目结构,项目建立完成后经过 npm i mocha -D 安装 mocha 模块。而后在本地运行 npm test 看是否可以测试经过。若是可以测试经过则说明咱们的能够继续下一步了。


    建立 travis-ci 测试配置文件

    建立 travis-ci 配置文件 .travis.yml, 文件内容。更多关于配置文件的说明在travis官网可查询

    language: node_js
    node_js:
      - "node"
      - "8.9.4"
    复制代码

    至此基本完成了项目开发和测试代码编写的过程,下一步就能够接入 travis-ci 测试了。

    接入 travis-ci

    经过GitHub登陆 travis-ci 的官网 www.travis-ci.org/


    找到GitHub上刚才建立的须要测试的项目,并开启测试


    查看测试过程,及时发现问题。


    查看测试状态是否经过测试,若是未经过及时排查问题反复修改;若是经过能够在项目文档中添加一个测试经过的标识。

    总结

    年初的时候我曾去面试一个公司,公司是一个创业公司,老板是前百度首席架构师林仕鼎先生,公司名字叫爱云校,主要业务是为基础教育提供数据服务,有本身的平台和产品。当天面试官问个人面试问题中最令我印象深入的就是:之前你作前端的时候的单元测试所用的框架和工做流是什么?当时说实话我很懵逼,懵逼到我甚至不知道在前端领域什么是单元测试?要测试什么?用的啥工具?一时语塞,我说之前没有用过,也没有作过前端测试。当时面试官老哥明显脸上有些失望,可是最终的结果是公司录用了我,我说出这个经历的缘由,其实想传达的意思是:如今的前端开发早已不是早年的切图、特效实现和视觉表现。如今的前端开发工程师更多的是你做为一个在整个项目产品开发团队中理论上(其实也是事实上)离用户最近的一个岗位,应不只仅限于实现纯传统前端的功能,更须要基于明白客户需求这个基础上统筹好前端和后端的良好耦合,以及前端功能的准确无误。根据本身的面试求职经历,今天在这里谈到的前端单元测试的内容,我的认为在某些公司项目中,可能并非必须的,但做为发展最快的IT领域之一,前端工程师掌握前端单元测试在将来时间只能是要求愈来愈硬性,而不是一直停留在无关紧要了解范畴,由于这跟前端开发的发展趋势息息相关。

    可是同时,在我我的观点范畴内,至少目前我仍是坚持开发为主测试为辅的流程,对于像TDD这种单元测试指导开发流程,目前并不推崇。我的认为,这是一个颇有创新性的方法论,也并非如今彻底不可行,我的认为只是可行的范畴还不够宽,可行的条件要求还很严苛。因此相对于TDD,测试主导开发,对于目前准备进阶的前端开发者,我的更建议,了解某种之后会使用的新趋势和技术是有必要的,但做为技术人应该在学习新的、前卫的技术的同时不可迷失自我一味追求新技术,更重要的是要磨练当下的主流技能。相比于将来的单元测试主导开发流程,倒不如在目前这个时间节点精进基础开发流程,好比让本身的JS代码更专一于模块化和功能化的实现,这样的同时也会让单元测试更有效率,真正发挥目前单元测试对前端工程化的做用。

    相关文章
    相关标签/搜索