mocha
在第一小结中的测试中用到了 mocha 框架,这一节就说说 mocha 框架吧。下面整理的内容主要来源于官网,如需了解更多请移步mocha 官网。
mocha 是一个功能丰富的前端测试框架,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.
安装
安装有两种方式:1. 全局安装;2. 将 mocha 作为项目依赖模块安装。
npm install --global mocha
npm install --save-dev 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(add(1, 1)).to.be.equal(2);
所谓"断言",就是判断源码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误。上面这句断言的意思是,调用 add(1, 1),结果应该等于 2。所有的测试用例(it 块)都应该含有一句或多句的断言。它是编写测试用例的关键。断言功能由断言库来实现,Mocha 本身不带断言库,所以必须先引入断言库。
var expect = require('chai').expect;
断言库有很多种,Mocha 并不限制使用哪一种,它允许你使用你想要的任何断言库。上面代码引入的断言库是 chai,并且指定使用它的 expect 断言风格。下面这些常见的断言库:
- assert 这个是 Node.js 中的断言模块。
- should.js
- expect.js
- chaijs
- better-assert
- unexpected.js
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() {});
上面的这个测试用例,内部没有任何代码,由于没有抛出了错误,所以还是会通过。
在命令行中使用 mocha
在命令行使用 mocha 则需要在全局安装:npm install mocha -g
, 可以通过一些参数来测试指定的文件、指定展示结果的风格、导出测试报告等。可使用mocha --help
命令查看所有参数。
在项目中使用 mocha
初始化一个 node 项目
mkdir step1 && cd step1
npm init -y
npm install --save-dev mocha
创建一个文件夹 test, 并在里面创建一个 test.js ,写入下面的内容。
下面这段代码主要是简单测试了一下数组 [1, 2, 3]
的 indexOf()
方法。预期[1, 2, 3].indexOf(4)
返回-1。
var assert = require('assert');
describe('Array', function() {
describe('#indexOf()', function() {
it('should return -1 when the value is not present', function() {
assert.equal([1, 2, 3].indexOf(4), -1);
});
});
});
修改 package.json 文件。
{
"scripts": {
"test": "mocha"
}
}
然后打开终端,在命令行中运行npm run test
, 下面是运行结果。
mocha 基础用法
测试回调方法被多次调用
如果使用基于回调的异步测试,如果 done()被多次调用,则 Mocha 将抛出错误。这对于捕捉意外的双重回调很方便。
it('double done', function(done) {
// Calling `done()` twice is an error
setImmediate(done);
setImmediate(done);
});
回调多次意外运行错误结果
异步代码检查
使用 Mocha 测试异步代码非常简单!只需在测试完成时调用回调。通过向它添加一个回调函数(通常名为 done),Mocha 会知道它应该等待这个函数被调用来完成测试。该回调接受 Error 实例(或其子类)或伪造值;其他任何事情都会导致测试失败。
describe('User', function() {
describe('#save()', function() {
it('should save without error', function(done) {
var user = new User('Luna');
user.save(function(err) {
if (err) done(err);
else done();
});
});
});
});
为了使事情更简单,done()回调函数也接受一个 Error 实例(即新的 Error()),所以我们可以直接使用它:
describe('User', function() {
describe('#save()', function() {
it('should save without error', function(done) {
var user = new User('Luna');
user.save(done);
});
});
});
运行 Promise
或者,您可以不使用 done()回调,而是返回一个 Promise。如果您正在测试的 API 返回 Promise 而不是回调,这很有用:
beforeEach(function() {
return db.clear().then(function() {
return db.save([tobi, loki, jane]);
});
});
describe('#find()', function() {
it('respond with matching records', function() {
return db.find({ type: 'User' }).should.eventually.have.length(3);
});
});
使用 async/await
如果您的 JS 环境支持异步/等待,您也可以编写像这样的异步测试:
beforeEach(async function() {
await db.clear();
await db.save([tobi, loki, jane]);
});
describe('#find()', function() {
it('responds with matching records', async function() {
const users = await db.find({ type: 'User' });
users.should.have.length(3);
});
});
测试同步的代码
在测试同步代码时,省略回调,mocha 将自动继续下一次测试。
describe('Array', function() {
describe('#indexOf()', function() {
it('should return -1 when the value is not present', function() {
[1, 2, 3].indexOf(5).should.equal(-1);
[1, 2, 3].indexOf(0).should.equal(-1);
});
});
});
箭头函数
不鼓励在 mocha 中使用箭头函数。 表达式中绑定的 this,不能访问 mocha 上下文。例如,以下代码将失败:
describe('my suite', () => {
it('my test', () => {
// should set the timeout of this test to 1000 ms; instead will fail
this.timeout(1000);
assert.ok(true);
});
});
如果你不需要使用 mocha 的上下文,表达式是可以正常运行的。但是,如果最终需要重构,结果可能会与预期有所不同。
钩子
凭借其默认的“BDD”风格界面,Mocha 提供了 before(),after(),beforeEach()和 afterEach()之前的钩子。这些应该用于设置先决条件并在测试后进行清理。
describe('hooks', function() {
before(function() {
// runs before all tests in this block
});
after(function() {
// runs after all tests in this block
});
beforeEach(function() {
// runs before each test in this block
});
afterEach(function() {
// runs after each test in this block
});
// test cases
});
测试可以在你的钩子之前,之后或穿插出现。视情况而定,挂钩将按其定义的顺序运行;所有 before()
钩子运行(一次),然后任何 beforeEach()
钩子,测试,任何 afterEach()
钩子,以及最后的 after()
钩子(一次)。
钩子描述
任何钩子都可以通过可选的描述来调用,从而更容易查明测试中的错误。如果钩子被赋予了一个命名函数,那么如果没有提供描述,将使用该名称。
beforeEach(function() {
// beforeEach hook
});
beforeEach(function namedFun() {
// beforeEach:namedFun
});
beforeEach('some description', function() {
// beforeEach:some description
});
异步钩子
所有钩子(before(), after(), beforeEach(), afterEach())都可能是同步或异步的,其行为与常规测试案例非常相似。例如下面的代码,您可能希望在每次测试之前用虚拟内容填充数据库:
describe('Connection', function() {
var db = new Connection(),
tobi = new User('tobi'),
loki = new User('loki'),
jane = new User('jane');
// 每次测试前填充数据
beforeEach(function(done) {
db.clear(function(err) {
if (err) return done(err);
db.save([tobi, loki, jane], done);
});
});
describe('#find()', function() {
it('respond with matching records', function(done) {
db.find({ type: 'User' }, function(err, res) {
if (err) return done(err);
res.should.have.length(3);
done();
});
});
});
});
root 级别的钩子
你也可以选择任何文件并添加“root”级别的钩子。例如,在所有 describe()块之外添加 beforeEach()。这里定义的回调 beforeEach()在任何测试用例之前运行,而不管它存在于哪个文件中(这是因为 Mocha 有一个隐含的 describe()块,称为“root 套件”)。
beforeEach(function() {
console.log('before every test in every file');
});
延迟执行 root 套件
如果您需要在运行任何套件之前执行异步操作,则可能会延迟 root 套件。用--delay 标志运行 mocha。这将在全局上下文中附加一个特殊的回调函数 run():
setTimeout(function() {
// do some setup
describe('my suite', function() {
// ...
});
run();
}, 5000);
待定的测试用例
待定的测试用例就是指的那些最终需要完成而待完成测试的用例,这些实例只有描述而没有会回调。待测试将包含在测试结果中,并标记为待处理。未决测试不被视为失败测试。如下面这种:
describe('Array', function() {
describe('#indexOf()', function() {
// pending test below
it('should return -1 when the value is not present');
});
});
运行结果
测试用例管理
当有很多测试用例。有时,我们希望只运行其中的几个,这时可以用 only 方法。describe 块和 it 块都允许调用 only 方法,表示只运行某个测试套件或测试用例。
describe('Array', function() {
describe.only('#indexOf()', function() {
// ...
});
});
describe('Array', function() {
describe('#indexOf()', function() {
it.only('should return -1 unless present', function() {
// ...
});
it('should return the index when present', function() {
// ...
});
});
});
有时需要跳过一些测试用例可以使 skip 方法:
describe('Array', function() {
describe.skip('#indexOf()', function() {
// ...
});
});
describe('Array', function() {
describe('#indexOf()', function() {
it.skip('should return -1 unless present', function() {
// this test will not be run
});
it('should return the index when present', function() {
// this test will be run
});
});
});
有时需要根据环境来判断是否跳过或者指定运行一些实例,可以参考下面的代码
it('should only test in the correct environment', function() {
if (/* check test environment */) {
// make assertions
} else {
this.skip();
}
});
it('should only test in the correct environment', function() {
if (/* check test environment */) {
// make assertions
} else {
// do nothing
}
});
before(function() {
if (/* check test environment */) {
// setup code
} else {
this.skip();
}
});