聊一聊前端自动化测试

本文转载自 天猫前端博客,更多精彩文章请进入天猫前端博客检察

媒介

为什么要测试

之前不喜欢写测试,主假如以为编写和保护测试用例异常的浪费时刻。在真正写了一段时刻的基本组件和基本东西后,才发明自动化测试有许多长处。测试最重要的天然是提拔代码质量。代码有测试用例,虽不能说百分百无bug,但至少申明测试用例掩盖到的场景是没有题目的。有测试用例,宣布前跑一下,可以根绝种种疏忽而引发的功用bug。

自动化测试别的一个重要特征就是疾速反应,反应越敏捷意味着开辟效力越高。拿UI组件为例,开辟历程都是翻开浏览器革新页面点点点才一定UI组件事情状态是不是相符自身预期。接入自动化测试今后,经由过程剧本替代这些手动点击,接入代码watch后每次保留文件都能疾速得知自身的的修正是不是影响功用,节省了许多时刻,毕竟机械干事变比人老是要快很多。

有了自动化测试,开辟者会越发信托自身的代码。开辟者再也不会恐惧将代码交给他人保护,不必忧郁别的开辟者在代码里搞“损坏”。后人接办一段有测试用例的代码,修正起来也会越发自在。测试用例里异常清楚的阐释了开辟者和运用者关于这端代码的希冀和要求,也异常有利于代码的传承。

斟酌投入产出比来做测试

说了这么多测试的长处,并不代表一上来就要写出100%场景掩盖的测试用例。个人一向对峙一个看法:基于投入产出比来做测试。由于保护测试用例也是一大笔开支(毕竟没有若干测试会特地帮前端写营业测试用例,而前端运用的流程自动化东西更是没有测试介入了)。关于像基本组件、基本模子之类的不常变动且复用较多的部份,可以斟酌去写测试用例来保证质量。个人比较倾向于先写少许的测试用例掩盖到80%+的场景,保证掩盖重要运用流程。一些极度场景涌现的bug可以在迭代中构成测试用例沉淀,场景掩盖也将逐步趋近100%。但关于迭代较快的营业逻辑以及生计时刻不长的运动页面之类的就别花时刻写测试用例了,保护测试用例的时刻大了去了,本钱太高。

Node.js模块的测试

关于Node.js的模块,测试算是比较轻易的,毕竟源码和依靠都在当地,看得见摸得着。

测试东西

测试重要运用到的东西是测试框架、断言库以及代码掩盖率东西:

  1. 测试框架:MochaJasmine等等,测试重要供应了清楚简明的语法来形貌测试用例,以及对测试用例分组,测试框架会抓取到代码抛出的AssertionError,并增添一大堆附加信息,比方谁人用例挂了,为什么挂等等。测试框架平常供应TDD(测试驱动开辟)或BDD(行动驱动开辟)的测试语法来编写测试用例,关于TDD和BDD的对照可以看一篇比较着名的文章The Difference Between TDD and BDD。差别的测试框架支撑差别的测试语法,比方Mocha既支撑TDD也支撑BDD,而Jasmine只支撑BDD。这里后续以Mocha的BDD语法为例

  2. 断言库:Should.jschaiexpect.js等等,断言库供应了许多语义化的要领来对值做林林总总的推断。固然也可以不必断言库,Node.js中也可以直接运用原生assert库。这里后续以Should.js为例

  3. 代码掩盖率:istanbul等等为代码在语法级分支上办理,运转了办理后的代码,依据运转完毕后网络到的信息和办理时的信息来统计出当前测试用例的对源码的掩盖状态。

一个煎蛋的栗子

以以下的Node.js项目构造为例

.
├── LICENSE
├── README.md
├── index.js
├── node_modules
├── package.json
└── test
    └── test.js

起首天然是装置东西,这里先装测试框架和断言库:npm install --save-dev mocha should。装完后就可以最先测试之旅了。

比方当前有一段js代码,放在index.js

'use strict';
module.exports = () => 'Hello Tmall';

那末关于这么一个函数,起首须要定一个测试用例,这里很明显,运转函数,获得字符串Hello Tmall就算测试经由过程。那末就可以根据Mocha的写法来写一个测试用例,因而新建一个测试代码在test/index.js

'use strict';
require('should');
const mylib = require('../index');

describe('My First Test', () => {
  it('should get "Hello Tmall"', () => {
    mylib().should.be.eql('Hello Tmall');
  });
});

测试用例写完了,那末怎样晓得测试结果呢?

由于我们之前已装置了Mocha,可以在node_modules内里找到它,Mocha供应了敕令行东西_mocha,可以直接在./node_modules/.bin/_mocha找到它,运转它就可以实行测试了:

《聊一聊前端自动化测试》

如许就可以看到测试结果了。一样我们可以有意让测试不经由过程,修正test.js代码为:

'use strict';
require('should');
const mylib = require('../index');

describe('My First Test', () => {
  it('should get "Hello Taobao"', () => {
    mylib().should.be.eql('Hello Taobao');
  });
});

就可以看到下图了:

《聊一聊前端自动化测试》

Mocha实际上支撑许多参数来供应许多天真的掌握,比方运用./node_modules/.bin/_mocha --require should,Mocha在启动测试时就会自身去加载Should.js,如许test/test.js里就不须要手动require('should');了。更多参数设置可以查阅Mocha官方文档

那末这些测试代码离别是啥意思呢?

这里起首引入了断言库Should.js,然后引入了自身的代码,这里it()函数定义了一个测试用例,经由过程Should.js供应的api,可以异常语义化的形貌测试用例。那末describe又是干什么的呢?

describe干的事变就是给测试用例分组。为了尽量多的掩盖种种状态,测试用例每每会有许多。这时刻经由过程分组就可以比较轻易的治理(这里提一句,describe是可以嵌套的,也就是说外层分组了以后,内部还可以份子组)。别的另有一个异常重要的特征,就是每一个分组都可以举行预处置惩罚(beforebeforeEach)和后处置惩罚(after, afterEach)。

假如把index.js源码改成:

'use strict';
module.exports = bu => `Hello ${bu}`;

为了测试差别的bu,测试用例也对应的改成:

'use strict';
require('should');
const mylib = require('../index');
let bu = 'none';

describe('My First Test', () => {
  describe('Welcome to Tmall', () => {
    before(() => bu = 'Tmall');
    after(() => bu = 'none');
    it('should get "Hello Tmall"', () => {
      mylib(bu).should.be.eql('Hello Tmall');
    });
  });
  describe('Welcome to Taobao', () => {
    before(() => bu = 'Taobao');
    after(() => bu = 'none');
    it('should get "Hello Taobao"', () => {
      mylib(bu).should.be.eql('Hello Taobao');
    });
  });
});

一样运转一下./node_modules/.bin/_mocha就可以看到以下图:

《聊一聊前端自动化测试》

这里before会在每一个分组的一切测试用例运转前,相对的after则会在一切测试用例运转后实行,假如要以测试用例为粒度,可以运用beforeEachafterEach,这两个钩子则会离别在该分组每一个测试用例运转前和运转后实行。由于许多代码都须要模仿环境,可以再这些beforebeforeEach做这些准备事情,然后在afterafterEach里做回收操纵。

异步代码的测试

回调

这里很显然代码都是同步的,但许多状态下我们的代码都是异步实行的,那末异步的代码要怎样测试呢?

比方这里index.js的代码变成了一段异步代码:

'use strict';
module.exports = (bu, callback) => process.nextTick(() => callback(`Hello ${bu}`));

由于源代码变成异步,所以测试用例就得做革新:

'use strict';
require('should');
const mylib = require('../index');

describe('My First Test', () => {
  it('Welcome to Tmall', done => {
    mylib('Tmall', rst => {
      rst.should.be.eql('Hello Tmall');
      done();
    });
  });
});

这里传入it的第二个参数的函数新增了一个done参数,当有这个参数时,这个测试用例会被以为是异步测试,只需在done()实行时,才以为测试完毕。那假如done()一向没有实行呢?Mocha会触发自身的超时机制,凌驾一定时刻(默许是2s,时长可以经由过程--timeout参数设置)就会自动停止测试,并以测试失利处置惩罚。

固然,beforebeforeEachafterafterEach这些钩子,一样支撑异步,运用体式格局和it一样,在传入的函数第一个参数加上done,然后在实行完成后实行即可。

Promise

寻常我们直接写回调会觉得自身很low,也轻易涌现回调金字塔,我们可以运用Promise来做异步掌握,那末关于Promise掌握下的异步代码,我们要怎样测试呢?

起首把源码做点革新,返回一个Promise对象:

'use strict';
module.exports = bu => new Promise(resolve => resolve(`Hello ${bu}`));

固然,假如是co党也可以直接运用co包裹:

'use strict';
const co = require('co');
module.exports = co.wrap(function* (bu) {
  return `Hello ${bu}`;
});

对应的修正测试用例以下:

'use strict';
require('should');
const mylib = require('../index');

describe('My First Test', () => {
  it('Welcome to Tmall', () => {
    return mylib('Tmall').should.be.fulfilledWith('Hello Tmall');
  });
});

Should.js在8.x.x版本自带了Promise支撑,可以直接运用fullfilled()rejected()fullfilledWith()rejectedWith()等等一系列API测试Promise对象。

注重:运用should测试Promise对象时,请一定要return,一定要return,一定要return,不然断言将无效

异步运转测试

有时刻,我们可以并不只是某个测试用例须要异步,而是全部测试历程都须要异步实行。比方测试Gulp插件的一个计划就是,起首运转Gulp使命,完成后测试天生的文件是不是和预期的一致。那末怎样异步实行全部测试历程呢?

实在Mocha供应了异步启动测试,只须要在启动Mocha的敕令后加上--delay参数,Mocha就会以异步体式格局启动。这类状态下我们须要通知Mocha什么时刻最先跑测试用例,只须要实行run()要领即可。把适才的test/test.js修正成下面如许:

'use strict';
require('should');
const mylib = require('../index');

setTimeout(() => {
  describe('My First Test', () => {
    it('Welcome to Tmall', () => {
      return mylib('Tmall').should.be.fulfilledWith('Hello Tmall');
    });
  });
  run();
}, 1000);

直接实行./node_modules/.bin/_mocha就会发作下面如许的杯具:

《聊一聊前端自动化测试》

那末加上--delay尝尝:

《聊一聊前端自动化测试》

熟习的绿色又回来了!

代码掩盖率

单元测试玩得差不多了,可以最先尝尝代码掩盖率了。起首须要装置代码掩盖率东西istanbul:npm install --save-dev istanbul,istanbul一样有敕令行东西,在./node_modules/.bin/istanbul可以寻找到它的身影。Node.js端做代码掩盖率测试很简朴,只须要用istanbul启动Mocha即可,比方上面谁人测试用例,运转./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --delay,可以看到下图:

《聊一聊前端自动化测试》

这就是代码掩盖率结果了,由于index.js中的代码比较简朴,所以直接就100%了,那末修正一下源码,加个if吧:

'use strict';
module.exports = bu => new Promise(resolve => {
  if (bu === 'Tmall') return resolve(`Welcome to Tmall`);
  resolve(`Hello ${bu}`);
});

测试用例也随着变一下:

'use strict';
require('should');
const mylib = require('../index');

setTimeout(() => {
  describe('My First Test', () => {
    it('Welcome to Tmall', () => {
      return mylib('Tmall').should.be.fulfilledWith('Welcome to Tmall');
    });
  });
  run();
}, 1000);

换了姿态,我们再来一次./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --delay,可以获得下图:

《聊一聊前端自动化测试》

当运用istanbul运转Mocha时,istanbul敕令自身的参数放在--之前,须要传递给Mocha的参数放在--以后

如预期所想,掩盖率不再是100%了,这时刻我想看看哪些代码被运转了,哪些没有,怎样办呢?

运转完成后,项面前目今会多出一个coverage文件夹,这里就是放代码掩盖率结果的处所,它的构造大抵以下:

.
├── coverage.json
├── lcov-report
│   ├── base.css
│   ├── index.html
│   ├── prettify.css
│   ├── prettify.js
│   ├── sort-arrow-sprite.png
│   ├── sorter.js
│   └── test
│       ├── index.html
│       └── index.js.html
└── lcov.info
  • coverage.json和lcov.info:测试结果形貌的json文件,这个文件可以被一些东西读取,天生可视化的代码掩盖率结果,这个文件背面接入延续集成时还会提到。

  • lcov-report:经由过程上面两个文件由东西处置惩罚后天生的掩盖率结果页面,翻开可以异常直观的看到代码的掩盖率

这里open coverage/lcov-report/index.html可以看到文件目次,点击对应的文件进入到文件概略,可以看到index.js的掩盖率如图所示:

《聊一聊前端自动化测试》

这里有四个目的,经由过程这些目的,可以量化代码掩盖状态:

  • statements:可实行语句实行状态

  • branches:分支实行状态,比方if就会发生两个分支,我们只运转了个中的一个

  • Functions:函数实行状态

  • Lines:行实行状态

下面代码部份,没有被实行过得代码会被标红,这些标红的代码每每是bug滋长的泥土,我们要尽量消弭这些赤色。为此我们增加一个测试用例:

'use strict';
require('should');
const mylib = require('../index');

setTimeout(() => {
  describe('My First Test', () => {
    it('Welcome to Tmall', () => {
      return mylib('Tmall').should.be.fulfilledWith('Welcome to Tmall');
    });
    it('Hello Taobao', () => {
      return mylib('Taobao').should.be.fulfilledWith('Hello Taobao');
    });
  });
  run();
}, 1000);

再来一次./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --delay,从新翻开掩盖率页面,可以看到赤色已消逝了,掩盖率100%。目的完成,可以睡个牢固觉了

集成到package.json

好了,一个简朴的Node.js测试算是做完了,这些测试使命都可以集合写到package.jsonscripts字段中,比方:

{
  "scripts": {
    "test": "NODE_ENV=test ./node_modules/.bin/_mocha --require should",
    "cov": "NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --delay"
  },
}

如许直接运转npm run test就可以跑单元测试,运转npm run cov就可以跑代码掩盖率测试了,轻易快捷

对多个文件离别做测试

平常我们的项目都邑有许多文件,比较引荐的要领是对每一个文件零丁去做测试。比方代码在./lib/下,那末./lib/文件夹下的每一个文件都应该对应一个./test/文件夹下的文件名_spec.js的测试文件

为什么要如许呢?不能直接运转index.js进口文件做测试吗?

直接从进口文件来测实际上是黑盒测试,我们并不晓得代码内部运转状态,只是看某个特定的输入可否获得希冀的输出。这平常可以掩盖到一些重要场景,然则在代码内部的一些边沿场景,就很难直接经由过程从进口输入特定的数据来处理了。比方代码里须要发送一个要求,进口只是传入一个url,url自身准确与否只是一个方面,当时的网络状态和效劳器状态是没法预知的。传入雷同的url,可以由于效劳器挂了,也可以由于网络发抖,致使要求失利而抛出毛病,假如这个毛病没有获得处置惩罚,极可以致使毛病。因而我们须要把黑盒翻开,对个中的每一个小块做白盒测试。

固然,并非一切的模块测起来都这么轻松,前端用Node.js常干的事变就是写构建插件和自动化东西,典范的就是Gulp插件和敕令行东西,那末这俩种特定的场景要怎样测试呢?

Gulp插件的测试

如今前端构建运用最多的就是Gulp了,它简明的API、流式构建理念、以及在内存中操纵的机能,让它备受追捧。虽然如今有像webpack如许的后起之秀,但Gulp照旧凭借著其繁华的生态圈担当着前端构建的相对主力。现在天猫前端就是运用Gulp作为代码构建东西。

用了Gulp作为构建东西,也就免不了要开辟Gulp插件来满足营业定制化的构建需求,构建历程本质上实际上是对源代码举行修正,假如修正历程当中涌现bug极可以直接致使线上毛病。因而针对Gulp插件,尤其是会修正源代码的Gulp插件一定要做细致的测试来保证质量。

又一个煎蛋的栗子

比方这里有个煎蛋的Gulp插件,功用就是往一切js代码前加一句解释// 天猫前端招人,有意向的请发送简历至lingyucoder@gmail.com,Gulp插件的代码也许就是如许:

'use strict';

const _ = require('lodash');
const through = require('through2');
const PluginError = require('gulp-util').PluginError;
const DEFAULT_CONFIG = {};

module.exports = config => {
  config = _.defaults(config || {}, DEFAULT_CONFIG);
  return through.obj((file, encoding, callback) => {
    if (file.isStream()) return callback(new PluginError('gulp-welcome-to-tmall', `Stream is not supported`));
    file.contents = new Buffer(`// 天猫前端招人,有意向的请发送简历至lingyucoder@gmail.com\n${file.contents.toString()}`);
    callback(null, file);
  });
};

关于这么一段代码,怎样做测试呢?

一种体式格局就是直接捏造一个文件传入,Gulp内部实际上是经由过程vinyl-fs从操纵系统读取文件并做成假造文件对象,然后将这个假造文件对象交由through2制造的Transform来改写流中的内容,而外层使命之间经由过程orchestrator掌握,保证实行递次(假如不相识可以看看这篇翻译文章Gulp头脑——Gulp高等技能)。固然一个插件不须要体贴Gulp的使命治理机制,只须要体贴传入一个vinyl对象可否准确处置惩罚。因而只须要捏造一个假造文件对象传给我们的Gulp插件就可以了。

起首设想测试用例,斟酌两个重要场景:

  1. 假造文件对象是流花样的,应该抛出毛病

  2. 假造文件对象是Buffer花样的,可以平常对文件内容举行加工,加工完的文件加上// 天猫前端招人,有意向的请发送简历至lingyucoder@gmail.com的头

关于第一个测试用例,我们须要建立一个流花样的vinyl对象。而关于各第二个测试用例,我们须要建立一个Buffer花样的vinyl对象。

固然,起首我们须要一个被加工的源文件,放到test/src/testfile.js下吧:

'use strict';
console.log('hello world');

这个源文件异常简朴,接下来的使命就是把它离别封装成流花样的vinyl对象和Buffer花样的vinyl对象。

构建Buffer花样的假造文件对象

构建一个Buffer花样的假造文件对象可以用vinyl-fs读取操纵系统里的文件天生vinyl对象,Gulp内部也是运用它,默许运用Buffer:

'use strict';
require('should');
const path = require('path');
const vfs = require('vinyl-fs');
const welcome = require('../index');

describe('welcome to Tmall', function() {
  it('should work when buffer', done => {
    vfs.src(path.join(__dirname, 'src', 'testfile.js'))
      .pipe(welcome())
      .on('data', function(vf) {
        vf.contents.toString().should.be.eql(`// 天猫前端招人,有意向的请发送简历至lingyucoder@gmail.com\n'use strict';\nconsole.log('hello world');\n`);
        done();
      });
  });
});

如许测了Buffer花样后算是完成了重要功用的测试,那末要怎样测试流花样呢?

构建流花样的假造文件对象

计划一和上面一样直接运用vinyl-fs,增添一个参数buffer: false即可:

把代码修正成如许:

'use strict';
require('should');
const path = require('path');
const vfs = require('vinyl-fs');
const PluginError = require('gulp-util').PluginError;
const welcome = require('../index');

describe('welcome to Tmall', function() {
  it('should work when buffer', done => {
    // blabla
  });
  it('should throw PluginError when stream', done => {
    vfs.src(path.join(__dirname, 'src', 'testfile.js'), {
      buffer: false
    })
      .pipe(welcome())
      .on('error', e => {
        e.should.be.instanceOf(PluginError);
        done();
      });
  });
});

如许vinyl-fs直接从文件系统读取文件并天生流花样的vinyl对象。

假如内容并不来自于文件系统,而是泉源于一个已存在的可读流,要怎样把它封装成一个流花样的vinyl对象呢?

如许的需求可以借助vinyl-source-stream

'use strict';
require('should');
const fs = require('fs');
const path = require('path');
const source = require('vinyl-source-stream');
const vfs = require('vinyl-fs');
const PluginError = require('gulp-util').PluginError;
const welcome = require('../index');

describe('welcome to Tmall', function() {
  it('should work when buffer', done => {
    // blabla
  });
  it('should throw PluginError when stream', done => {
    fs.createReadStream(path.join(__dirname, 'src', 'testfile.js'))
      .pipe(source())
      .pipe(welcome())
      .on('error', e => {
        e.should.be.instanceOf(PluginError);
        done();
      });
  });
});

这里起首经由过程fs.createReadStream建立了一个可读流,然后经由过程vinyl-source-stream把这个可读流包装成流花样的vinyl对象,并交给我们的插件做处置惩罚

Gulp插件实行毛病时请抛出PluginError,如许可以让gulp-plumber如许的插件举行毛病治理,防备毛病停止构建历程,这在gulp watch时异常有效

模仿Gulp运转

我们捏造的对象已可以跑通功用测试了,然则这数据泉源终究是自身捏造的,并非用户一样平常的运用体式格局。假如采纳最接近用户运用的体式格局来做测试,测试结果才越发牢靠和实在。那末题目来了,怎样模仿实在的Gulp环境来做Gulp插件的测试呢?

起首模仿一下我们的项目构造:

test
├── build
│   └── testfile.js
├── gulpfile.js
└── src
    └── testfile.js

一个浅易的项目构造,源码放在src下,经由过程gulpfile来指定使命,构建结果放在build下。根据我们寻常运用体式格局在test目次下搭好架子,而且写好gulpfile.js:

'use strict';
const gulp = require('gulp');
const welcome = require('../index');
const del = require('del');

gulp.task('clean', cb => del('build', cb));

gulp.task('default', ['clean'], () => {
  return gulp.src('src/**/*')
    .pipe(welcome())
    .pipe(gulp.dest('build'));
});

接着在测试代码里来模仿Gulp运转了,这里有两种计划:

  1. 运用child_process库供应的spawnexec开子历程直接跑gulp敕令,然后测试build目次下是不是是想要的结果

  2. 直接在当前历程猎取gulpfile中的Gulp实例来运转Gulp使命,然后测试build目次下是不是是想要的结果

开子历程举行测试有一些坑,istanbul测试代码掩盖率常常没法跨历程的,因而开子历程测试,起首须要子历程实行敕令时加上istanbul,然后还须要手动去网络掩盖率数据,当开启多个子历程时还须要自身做掩盖率结果数据兼并,相称贫苦。

那末不开子历程怎样做呢?可以借助run-gulp-task这个东西来运转,其内部的机制就是起首猎取gulpfile文件内容,在文件尾部加上module.exports = gulp;后require gulpfile从而猎取Gulp实例,然后将Gulp实例递交给run-sequence挪用内部未开放的APIgulp.run来运转。

我们采纳不开子历程的体式格局,把运转Gulp的历程放在before钩子中,测试代码变成下面如许:

'use strict';
require('should');
const path = require('path');
const run = require('run-gulp-task');
const CWD = process.cwd();
const fs = require('fs');

describe('welcome to Tmall', () => {
  before(done => {
    process.chdir(__dirname);
    run('default', path.join(__dirname, 'gulpfile.js'))
      .catch(e => e)
      .then(e => {
        process.chdir(CWD);
        done(e);
      });
  });
  it('should work', function() {
    fs.readFileSync(path.join(__dirname, 'build', 'testfile.js')).toString().should.be.eql(`// 天猫前端招人,有意向的请发送简历至lingyucoder@gmail.com\n'use strict';\nconsole.log('hello world');\n`);
  });
});

如许由于不须要开子历程,代码掩盖率测试也可以和平常Node.js模块一样了

测试敕令行输出

双一个煎蛋的栗子

固然前端写东西并不只限于Gulp插件,偶然还会写一些辅佐敕令啥的,这些辅佐敕令直接在终端上运转,结果也会直接展现在终端上。比方一个简朴的运用commander完成的敕令行东西:

// in index.js
'use strict';
const program = require('commander');
const path = require('path');
const pkg = require(path.join(__dirname, 'package.json'));

program.version(pkg.version)
  .usage('[options] <file>')
  .option('-t, --test', 'Run test')
  .action((file, prog) => {
    if (prog.test) console.log('test');
  });

module.exports = program;

// in bin/cli
#!/usr/bin/env node
'use strict';
const program = require('../index.js');

program.parse(process.argv);

!program.args[0] && program.help();

// in package.json
{
  "bin": {
    "cli-test": "./bin/cli"
  }
}

阻拦输出

要测试敕令行东西,天然要模仿用户输入敕令,这一次照旧挑选不开子历程,直接用捏造一个process.argv交给program.parse即可。敕令输入了题目也来了,数据是直接console.log的,要怎样阻拦呢?

这可以借助sinon来阻拦console.log,而且sinon异常知心的供应了mocha-sinon轻易测试用,如许test.js大抵就是这个模样:

'use strict';
require('should');
require('mocha-sinon');
const program = require('../index');
const uncolor = require('uncolor');

describe('cli-test', () => {
  let rst;
  beforeEach(function() {
    this.sinon.stub(console, 'log', function() {
      rst = arguments[0];
    });
  });
  it('should print "test"', () => {
    program.parse([
      'node',
      './bin/cli',
      '-t',
      'file.js'
    ]);
    return uncolor(rst).trim().should.be.eql('test');
  });
});

PS:由于敕令行输出时经常会运用colors如许的库来增加色彩,因而在测试时记得用uncolor把这些色彩移除

小结

Node.js相干的单元测试就扯这么多了,另有许多场景像效劳器测试什么的就不扯了,由于我不会。固然前端最重要的事情照样写页面,接下来扯一扯怎样对页面上的组件做测试。

页面测试

关于浏览器里跑的前端代码,做测试要比Node.js模块要贫苦很多。Node.js模块纯js代码,运用V8运转在当地,测试用的林林总总的依靠和东西都能疾速的装置,而前端代码不单单议要测试js,CSS等等,更贫苦的事须要模仿林林总总的浏览器,比较罕见的前端代码测试计划有下面几种:

  1. 构建一个测试页面,人肉直接到假造机上开种种浏览器跑测试页面(比方公司的f2etest)。这个计划的瑕玷就是不好做代码掩盖率测试,也不好延续化集成,同时人肉事情较多

  2. 运用PhantomJS构建一个捏造的浏览器环境跑单元测试,长处是处理了代码掩盖率题目,也可以做延续集成。这个计划的瑕玷是PhantomJS毕竟是Qt的webkit,并非实在浏览器环境,PhantomJS也有林林总总兼容性坑

  3. 经由过程Karma挪用本机种种浏览器举行测试,长处是可以跨浏览器做测试,也可以测试掩盖率,但延续集成时须要注重只能开PhantomJS做测试,毕竟集成的Linux环境不可以有浏览器。这可以说是现在看到的最好的前端代码测试体式格局了

这里以gulp为构建东西做测试,背面在React组件测试部份再引见以webpack为构建东西做测试

叒一个煎蛋的栗子

前端代码照旧是js,一样可以用Mocha+Should.js来做单元测试。翻开node_modules下的Mocha和Should.js,你会发明这些优异的开源东西已异常知心的供应了可在浏览器中直接运转的版本:mocha/mocha.jsshould/should.min.js,只须要把他们经由过程script标签引入即可,别的Mocha还须要引入自身的款式mocha/mocha.css

起首看一下我们的前端项目构造:

.
├── gulpfile.js
├── package.json
├── src
│   └── index.js
└── test
    ├── test.html
    └── test.js

比方这里源码src/index.js就是定义一个全局函数:

window.render = function() {
  var ctn = document.createElement('div');
  ctn.setAttribute('id', 'tmall');
  ctn.appendChild(document.createTextNode('天猫前端招人,有意向的请发送简历至lingyucoder@gmail.com'));
  document.body.appendChild(ctn);
}

而测试页面test/test.html大抵上是这个模样:

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="../node_modules/mocha/mocha.css"/>
  <script src="../node_modules/mocha/mocha.js"></script>
  <script src="../node_modules/should/should.js"></script>
</head>

<body>
  <div id="mocha"></div>
  <script src="../src/index.js"></script>
  <script src="test.js"></script>
</body>

</html>

head里引入了测试框架Mocha和断言库Should.js,测试的结果会被显现在<div id="mocha"></div>这个容器里,而test/test.js里则是我们的测试的代码。

前端页面上测试和Node.js上测试没啥太大差别,只是须要指定Mocha运用的UI,并须要手动挪用mocha.run()

mocha.ui('bdd');
describe('Welcome to Tmall', function() {
  before(function() {
    window.render();
  });
  it('Hello', function() {
    document.getElementById('tmall').textContent.should.be.eql('天猫前端招人,有意向的请发送简历至lingyucoder@gmail.com');
  });
});
mocha.run();

在浏览器里翻开test/test.html页面,就可以看到结果了:

《聊一聊前端自动化测试》

在差别的浏览器里翻开这个页面,就可以看到当前浏览器的测试了。这类体式格局能兼容最多的浏览器,固然要跨机械之前记得把资本上传到一个测试机械都能接见到的处所,比方CDN。

测试页面有了,那末来尝尝接入PhantomJS吧

运用PhantomJS举行测试

PhantomJS是一个模仿的浏览器,它能实行js,以至另有webkit衬着引擎,只是没有浏览器的界面上衬着结果罢了。我们可以运用它做许多事变,比方对网页举行截图,写爬虫爬取异步衬着的页面,以及接下来要引见的——对页面做测试。

固然,这里我们不是直接运用PhantomJS,而是运用mocha-phantomjs来做测试。npm install --save-dev mocha-phantomjs装置完成后,就可以运转敕令./node_modules/.bin/mocha-phantomjs ./test/test.html来对上面谁人test/test.html的测试了:

《聊一聊前端自动化测试》

单元测试没题目了,接下来就是代码掩盖率测试

掩盖率办理

起首第一步,改写我们的gulpfile.js

'use strict';
const gulp = require('gulp');
const istanbul = require('gulp-istanbul');

gulp.task('test', function() {
  return gulp.src(['src/**/*.js'])
    .pipe(istanbul({
      coverageVariable: '__coverage__'
    }))
    .pipe(gulp.dest('build-test'));
});

这里把掩盖率结果保留到__coverage__内里,把打完点的代码放到build-test目次下,比方适才的src/index.js的代码,在运转gulp test后,会天生build-test/index.js,内容大抵是这个模样:

var __cov_WzFiasMcIh_mBvAjOuQiQg = (Function('return this'))();
if (!__cov_WzFiasMcIh_mBvAjOuQiQg.__coverage__) { __cov_WzFiasMcIh_mBvAjOuQiQg.__coverage__ = {}; }
__cov_WzFiasMcIh_mBvAjOuQiQg = __cov_WzFiasMcIh_mBvAjOuQiQg.__coverage__;
if (!(__cov_WzFiasMcIh_mBvAjOuQiQg['/Users/lingyu/gitlab/dev/mui/test-page/src/index.js'])) {
   __cov_WzFiasMcIh_mBvAjOuQiQg['/Users/lingyu/gitlab/dev/mui/test-page/src/index.js'] = {"path":"/Users/lingyu/gitlab/dev/mui/test-page/src/index.js","s":{"1":0,"2":0,"3":0,"4":0,"5":0},"b":{},"f":{"1":0},"fnMap":{"1":{"name":"(anonymous_1)","line":1,"loc":{"start":{"line":1,"column":16},"end":{"line":1,"column":27}}}},"statementMap":{"1":{"start":{"line":1,"column":0},"end":{"line":6,"column":1}},"2":{"start":{"line":2,"column":2},"end":{"line":2,"column":42}},"3":{"start":{"line":3,"column":2},"end":{"line":3,"column":34}},"4":{"start":{"line":4,"column":2},"end":{"line":4,"column":85}},"5":{"start":{"line":5,"column":2},"end":{"line":5,"column":33}}},"branchMap":{}};
}
__cov_WzFiasMcIh_mBvAjOuQiQg = __cov_WzFiasMcIh_mBvAjOuQiQg['/Users/lingyu/gitlab/dev/mui/test-page/src/index.js'];
__cov_WzFiasMcIh_mBvAjOuQiQg.s['1']++;window.render=function(){__cov_WzFiasMcIh_mBvAjOuQiQg.f['1']++;__cov_WzFiasMcIh_mBvAjOuQiQg.s['2']++;var ctn=document.createElement('div');__cov_WzFiasMcIh_mBvAjOuQiQg.s['3']++;ctn.setAttribute('id','tmall');__cov_WzFiasMcIh_mBvAjOuQiQg.s['4']++;ctn.appendChild(document.createTextNode('天猫前端招人\uFF0C有意向的请发送简历至lingyucoder@gmail.com'));__cov_WzFiasMcIh_mBvAjOuQiQg.s['5']++;document.body.appendChild(ctn);};

这都什么鬼!不管了,横竖运转它就好。把test/test.html内里引入的代码从src/index.js修正为build-test/index.js,保证页面运转时运用的是编译后的代码。

编写钩子

运转数据会存放到变量__coverage__里,然则我们还须要一段钩子代码在单元测试完毕后猎取这个变量里的内容。把钩子代码放在test/hook.js下,内里内容如许写:

'use strict';

var fs = require('fs');

module.exports = {
  afterEnd: function(runner) {
    var coverage = runner.page.evaluate(function() {
      return window.__coverage__;
    });
    if (coverage) {
      console.log('Writing coverage to coverage/coverage.json');
      fs.write('coverage/coverage.json', JSON.stringify(coverage), 'w');
    } else {
      console.log('No coverage data generated');
    }
  }
};

如许准备事情事情就功德圆满了,实行敕令./node_modules/.bin/mocha-phantomjs ./test/test.html --hooks ./test/hook.js,可以看到以下图结果,同时掩盖率结果被写入到coverage/coverage.json内里了。

《聊一聊前端自动化测试》

天生页面

有了结果掩盖率结果就可以天生掩盖率页面了,起首看看掩盖率概略吧。实行敕令./node_modules/.bin/istanbul report --root coverage text-summary,可以看到下图:

《聊一聊前端自动化测试》

照样本来的配方,照样想熟习的滋味。接下来运转./node_modules/.bin/istanbul report --root coverage lcov天生掩盖率页面,实行完后open coverage/lcov-report/index.html,点击进入到src/index.js

《聊一聊前端自动化测试》

一颗赛艇!如许我们对前端代码就能做掩盖率测试了

接入Karma

Karma是一个测试集成框架,可以轻易地以插件的情势集成测试框架、测试环境、掩盖率东西等等。Karma已有了一套相称圆满的插件系统,这里尝试在PhantomJS、Chrome、FireFox下做测试,起首须要运用npm装置一些依靠:

  1. karma:框架本体

  2. karma-mocha:Mocha测试框架

  3. karma-coverage:掩盖率测试

  4. karma-spec-reporter:测试结果输出

  5. karma-phantomjs-launcher:PhantomJS环境

  6. phantomjs-prebuilt: PhantomJS最新版本

  7. karma-chrome-launcher:Chrome环境

  8. karma-firefox-launcher:Firefox环境

装置完成后,就可以开启我们的Karma之旅了。照样之前的谁人项目,我们把该消灭的消灭,只留下源文件和而是文件,并增添一个karma.conf.js文件:

.
├── karma.conf.js
├── package.json
├── src
│   └── index.js
└── test
    └── test.js

karma.conf.js是Karma框架的设置文件,在这个例子里,它也许是这个模样:

'use strict';

module.exports = function(config) {
  config.set({
    frameworks: ['mocha'],
    files: [
      './node_modules/should/should.js',
      'src/**/*.js',
      'test/**/*.js'
    ],
    preprocessors: {
      'src/**/*.js': ['coverage']
    },
    plugins: ['karma-mocha', 'karma-phantomjs-launcher', 'karma-chrome-launcher', 'karma-firefox-launcher', 'karma-coverage', 'karma-spec-reporter'],
    browsers: ['PhantomJS', 'Firefox', 'Chrome'],
    reporters: ['spec', 'coverage'],
    coverageReporter: {
      dir: 'coverage',
      reporters: [{
        type: 'json',
        subdir: '.',
        file: 'coverage.json',
      }, {
        type: 'lcov',
        subdir: '.'
      }, {
        type: 'text-summary'
      }]
    }
  });
};

这些设置都是什么意思呢?这里挨个申明一下:

  • frameworks: 运用的测试框架,这里照旧是我们熟习又亲热的Mocha

  • files:测试页面须要加载的资本,上面的test目次下已没有test.html了,一切须要加载内容都在这里指定,假如是CDN上的资本,直接写URL也可以,不过发起尽量运用当地资本,如许测试更快而且纵然没网也可以测试。这个例子里,第一行载入的是断言库Should.js,第二行是src下的一切代码,第三行载入测试代码

  • preprocessors:设置预处置惩罚器,在上面files载入对应的文件前,假如在这里设置了预处置惩罚器,会先对文件做处置惩罚,然后载入处置惩罚结果。这个例子里,须要对src目次下的一切资本增加掩盖率办理(这一步之前是经由过程gulp-istanbul来做,如今karma-coverage框架可以很轻易的处置惩罚,也不须要钩子啥的了)。背面做React组件测试时也会在这里运用webpack

  • plugins:装置的插件列表

  • browsers:须要测试的浏览器,这里我们挑选了PhantomJS、FireFox、Chrome

  • reporters:须要天生哪些代码报告

  • coverageReporter:掩盖率报告要怎样天生,这里我们希冀天生和之前一样的报告,包括掩盖率页面、lcov.info、coverage.json、以及敕令行里的提醒

好了,设置完成,来尝尝吧,运转./node_modules/karma/bin/karma start --single-run,可以看到以下输出:

《聊一聊前端自动化测试》

可以看到,Karma起首会在9876端口开启一个当地效劳,然后离别启动PhantomJS、FireFox、Chrome去加载这个页面,网络到测试结果信息以后离别输出,如许跨浏览器测试就处理啦。假如要新增浏览器就装置对应的浏览器插件,然后在browsers里指定一下即可,异常天真轻易。

那假如我的mac电脑上没有IE,又想测IE,怎样办呢?可以直接运转./node_modules/karma/bin/karma start启动当地效劳器,然后运用其他机械开对应浏览器直接接见本机的9876端口(固然这个端口是可设置的)即可,一样挪动端的测试也可以采纳这个要领。这个计划统筹了前两个计划的长处,弥补了其不足,是现在看到最优异的前端代码测试计划了

React组件测试

客岁React旋风平常囊括环球,固然天猫也在手艺上紧跟时期脚步。天猫商家端营业已周全切入React,构成了React组件系统,险些一切新营业都采纳React开辟,而老营业也在不停向React迁徙。React大红大紫,这里零丁拉出来说一讲React+webpack的打包计划怎样举行测试

这里只聊React Web,不聊React Native

现实上天猫现在并未采纳webpack打包,而是Gulp+Babel编译React CommonJS代码成AMD模块运用,这是为了可以在新老营业运用上越发天真,固然也有部份营业采纳webpack打包并上线

叕一个煎蛋的栗子

这里建立一个React组件,目次构造大抵如许(这里略过CSS相干部份,只需跑通了,集成CSS像PostCSS、Less都没啥题目):

.
├── demo
├── karma.conf.js
├── package.json
├── src
│   └── index.jsx
├── test
│   └── index_spec.jsx
├── webpack.dev.js
└── webpack.pub.js

React组件源码src/index.jsx也许是这个模样:

import React from 'react';
class Welcome extends React.Component {
  constructor() {
    super();
  }
  render() {
    return <div>{this.props.content}</div>;
  }
}
Welcome.displayName = 'Welcome';
Welcome.propTypes = {
  /**
   * content of element
   */
  content: React.PropTypes.string
};
Welcome.defaultProps = {
  content: 'Hello Tmall'
};
module.exports = Welcome;

那末对应的test/index_spec.jsx则也许是这个模样:

import 'should';
import Welcome from '../src/index.jsx';
import ReactDOM from 'react-dom';
import React from 'react';
import TestUtils from 'react-addons-test-utils';
describe('test', function() {
  const container = document.createElement('div');
  document.body.appendChild(container);
  afterEach(() => {
    ReactDOM.unmountComponentAtNode(container);
  });
  it('Hello Tmall', function() {
    let cp = ReactDOM.render(<Welcome/>, container);
    let welcome = TestUtils.findRenderedComponentWithType(cp, Welcome);
    ReactDOM.findDOMnode(welcome).textContent.should.be.eql('Hello Tmall');
  });
});

由因而测试React,天然要运用React的TestUtils,这个东西库供应了不少轻易查找节点和组件的要领,最重要的是它供应了模仿事宜的API,这可以说是UI测试最重要的一个功用。更多关于TestUtils的运用请参考React官网,这里就不扯了…

代码有了,测试用例也有了,接下就差跑起来了。karma.conf.js一定就和上面不一样了,起首它要多一个插件karma-webpack,由于我们的React组件是须要webpack打包的,不打包的代码压根就没法运转。别的还须要注重代码掩盖率测试也涌现了变化。由于如今多了一层Babel编译,Babel编译ES6、ES7源码天生ES5代码后会发生许多polyfill代码,因而假如对build完成以后的代码做掩盖率测试会包括这些polyfill代码,如许测出来的掩盖率显然是不牢靠的,这个题目可以经由过程isparta-loader来处理。React组件的karma.conf.js也许是这个模样:

'use strict';
const path = require('path');

module.exports = function(config) {
  config.set({
    frameworks: ['mocha'],
    files: [
      './node_modules/phantomjs-polyfill/bind-polyfill.js',
      'test/**/*_spec.jsx'
    ],
    plugins: ['karma-webpack', 'karma-mocha',, 'karma-chrome-launcher', 'karma-firefox-launcher', 'karma-phantomjs-launcher', 'karma-coverage', 'karma-spec-reporter'],
    browsers: ['PhantomJS', 'Firefox', 'Chrome'],
    preprocessors: {
      'test/**/*_spec.jsx': ['webpack']
    },
    reporters: ['spec', 'coverage'],
    coverageReporter: {
      dir: 'coverage',
      reporters: [{
        type: 'json',
        subdir: '.',
        file: 'coverage.json',
      }, {
        type: 'lcov',
        subdir: '.'
      }, {
        type: 'text-summary'
      }]
    },
    webpack: {
      module: {
        loaders: [{
          test: /\.jsx?/,
          loaders: ['babel']
        }],
        preLoaders: [{
          test: /\.jsx?$/,
          include: [path.resolve('src/')],
          loader: 'isparta'
        }]
      }
    },
    webpackMiddleware: {
      noInfo: true
    }
  });
};

这里相干于之前的karma.conf.js,重要有以下几点区分:

  1. 由于webpack的打包功用,我们在测试代码里直接import组件代码,因而不再须要在files里手动引入组件代码

  2. 预处置惩罚内里须要对每一个测试文件都做webpack打包

  3. 增加webpack编译相干设置,在编译源码时,须要定义preLoaders,并运用isparta-loader做代码掩盖率办理

  4. 增加webpackMiddleware设置,这里noInfo作用是不须要输出webpack编译时那一大串信息

如许设置基本上就完成了,跑一把./node_modules/karma/bin/karma start --single-run

《聊一聊前端自动化测试》

很好,结果相符预期。open coverage/lcov-report/index.html翻开掩盖率页面:

《聊一聊前端自动化测试》

鹅妹辅音!!!直接对jsx代码做的掩盖率测试!如许React组件的测试大体上就落成了

小结

前端的代码测试重要难度是怎样模仿林林总总的浏览器环境,Karma给我们供应了很好地体式格局,关于当地有的浏览器能自动翻开并测试,当地没有的浏览器则供应直接接见的页面。前端尤其是挪动端浏览器品种繁多,很难做到圆满,但我们可以经由过程这类体式格局完成主流浏览器的掩盖,保证每次上线大多数用户没有题目。

延续集成

测试结果有了,接下来就是把这些测试结果接入到延续集成当中。延续集成是一种异常优异的多人开辟实践,经由过程代码push触发钩子,完成自动运转编译、测试等事情。接入延续集成后,我们的每一次push代码,每一个Merge Request都邑天生对应的测试结果,项目的其他成员可以很清楚地相识到新代码是不是影响了现有的功用,在接入自动告警后,可以在代码提交阶段就疾速发明毛病,提拔开辟迭代效力。

延续集成会在每次集成时供应一个险些空缺的假造机械,并拷贝用户提交的代码到机械当地,经由过程读取用户项面前目今的延续集成设置,自动化的装置环境和依靠,编译和测试完成后天生报告,在一段时刻以后开释假造机械资本。

开源的延续集成

开源比较着名的延续集成效劳当属Travis,而代码掩盖率则经由过程Coveralls,只需有GitHub账户,就可以很轻松的接入Travis和Coveralls,在网站上勾选了须要延续集成的项目今后,每次代码push就会触发自动化测试。这两个网站在跑完测试今后,会自动天生测试结果的小图片

《聊一聊前端自动化测试》

Travis会读取项面前目今的travis.yml文件,一个简朴的例子:

language: node_js
node_js:
  - "stable"
  - "4.0.0"
  - "5.0.0"
script: "npm run test"
after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls"

language定义了运转环境的言语,而对应的node_js可以定义须要在哪几个Node.js版本做测试,比方这里的定义,代表着会离别在最新稳定版、4.0.0、5.0.0版本的Node.js环境下做测试

而script则是测试应用的敕令,平常状态下,都应该把自身这个项目开辟所须要的敕令都写在package.json的scripts内里,比方我们的测试要领./node_modules/karma/bin/karma start --single-run就应该如许写到scripts里:

{
  "scripts": {
    "test": "./node_modules/karma/bin/karma start --single-run"
  }
}

而after_script则是在测试完成以后运转的敕令,这里须要上传掩盖率结果到coveralls,只须要装置coveralls库,然后猎取lcov.info上传给Coveralls即可

更多设置请参照Travis官网引见

如许设置后,每次push的结果都可以上Travis和Coveralls看构建和代码掩盖率结果了

《聊一聊前端自动化测试》

《聊一聊前端自动化测试》

小结

项目接入延续集成在多人开辟同一个堆栈时刻能起到很大的用处,每次push都能自动触发测试,测试没过会发作告警。假如需求采纳Issues+Merge Request来治理,每一个需求一个Issue+一个分支,开辟完成后提交Merge Request,由项目Owner担任兼并,项目质量将更有保证

总结

这里只是前端测试相干学问的一小部份,另有异常多的内容可以深切发掘,而测试也仅仅是前端流程自动化的一部份。在前端手艺疾速生长的本日,前端项目不再像昔时的刀耕火种平常,越来越多的软件工程履历被集成到前端项目中,前端项目正向工程化、流程化、自动化方向高速奔驰。另有更多优异的提拔开辟效力、保证开辟质量的自动化计划亟待我们发掘。

    原文作者:天镶
    原文地址: https://segmentfault.com/a/1190000004558796
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞