4招了解前端单元测试

随着每个工程的复杂化、代码的高复用性要求和前端代码模块之间的高内聚低耦合的需求,前端工程中的单元测试流程就显得很有其必要。

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 环境运行 也可以在浏览器环境运行。

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

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

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

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

断言库

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

看一段代码示例:

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

如果对软件测试、接口测试、自动化测试、面试经验交流。感兴趣可以加软件测试交流:1085991341,还会有同行一起技术交流。

配置

这里的配置主要关注的是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 的官网

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

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

查看测试状态是否通过测试,如果未通过及时排查问题反复修改;如果通过可以在项目文档中添加一个测试通过的标识。


以上内容就是本篇的全部内容以上内容希望对你有帮助,有被帮助到的朋友欢迎点赞,评论。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355