文章目录
vue项目单元测试编写-mocha
此文章涉及使用mocha进行vue组件的单元测试编写,chai和sinon单元测试辅助工具的使用。如果需要了解vue-cli3.0项目测试环境的安装,请移步到我上一篇文章中。本篇默认安装了上述测试库和测试环境。
前端vue单元测试环境搭建
mocha的使用
mocha是一个编写js单元测试的框架,这里我们使用他作为模块以及vue组件的单元测试框架。
语法&API
mocha是一个灵活,简洁的测试框架,所以他只提供了基础的测试语法和极少的API,他们都是函数,可以直接使用,无须引入之类的:
-
describe(name, fn) 定义一组测试,即测试套件
-
it(name, fn) 定义一项测试,是测试的最小单元
// moduleName.spec.js
// 定义一组测试
describe('describeName', () => {
// 定义一项测试
it('testName', () => {
// 测试内容
})
})
在上面代码中,一个测试文件包含多个describe块,describe你可以理解为一个测试套件,表示定义一组测试,而it则是最小的测试单元,定义一个测试用例。describe可以嵌套describe,一个describe可以包含多个it。你可以根据describe来划分业务模块和功能模块的测试,然后里面包含一组组相关的测试用例。以此来整理你的测试用例。
挂起测试
当你使用it时,只传递第一个参数,即单元测试的名称时,而没有传递第二个函数参数时,那么这个测试会被视为挂起状态pending
,也会体现在测试报告中。
关于测试的一些小约定
-
测试文件一般放在根目录下的tests文件夹中(一般约定是这个(vue-cli项目默认就是这个),好像也有用test文件夹的),然后编写测试的js文件一般以:
.test.js
或者.spec.js
结尾,这也是一个约定吧。当然了,这也只是大多数人的一个约定。 -
测试文件的文件名 = 被测试模块名 +
.(test|spec).js
,最好按模块或者组件或者业务逻辑分类进行测试模块的文件夹分类。以此来管理你的测试文件。
测试用例的编写
你可以使用describe来定义一个测试套件,即可以看做测试块,然后你也可以在describe中嵌套多个it,it指一个个的测试用例,你还可以在describe嵌套describe,比如:
// example.spec.js
describe('测试模块一', () => {
it('测试用例一', function () {
// 测试代码
});
it('测试用例二', function () {
// 测试代码
});
describe('测试子模块', () => {
it('测试用例三', function() {
// 测试代码
})
})
})
ps:上面代码有一点问题,在describe函数中,第二个参数的函数最好是不要使用箭头函数,因为如果使用箭头函数,就无法使用describe中的一些方法,比如
this.timeout
等。it中也最好不要用箭头函数,或者在整个使用mocha编写测试时最好都不要用箭头函数。如果你不需要使用上下文那用箭头函数也无所谓,不过,与其在编写时打擦边球,不如一开始就杜绝不推荐的用法。
测试结果
测试模块一
√ 测试用例一
√ 测试用例二
测试子模块
√ 测试用例三
4 passing (40ms)
测试通过和失败
在一个单元测试中,即一个it块中,如果其第二个参数的函数在执行时抛出了异常,那么这个测试就会被判定为不通过,换句话说:只要测试不抛出错误,测试就算是通过。
describe('测试模块一', () => {
it('测试用例一', function () {
// 空的测试用例,什么也没做
});
it('测试用例二', function () {
// 抛出错误
throw new Error('测试用例二测试出错');
});
})
输出结果(测试不通过):
测试模块一
√ 测试用例一
1) 测试用例二 // 红色的
1 passing (227ms)
1 failing
1) 测试模块一
测试用例二:
Error: 测试用例二测试出错
at Context.<anonymous> (dist\webpack:\tests\unit\example.spec.js:27:1)
异步测试用例
done函数
mocha默认每个测试用例最多执行2000毫秒,如果超时还没有得到结果,则会报错,而对于异步测试来说,这个时间往往是不够的,你可以在运行测试时指定-t或-timeout
参数来指定超时时间。(这里参数设置就不在这里演示了,在我的环境搭建那一篇文章中有说明如何进行参数设置)
例子
describe('异步测试一', function () {
it('测试一', function (done) {
setTimeout(() => {
// 记得调用,如果不调用,这个it测试就会超时,并报错
done();
}, 1000);
});
})
结果
异步测试一
√ 测试一 (1001ms) // 他会显示出这个异步测试的时间
1 passing (1s)
mocha默认会将测试所用的时间大于75ms的测试用例的具体时间高亮输出在测试的后面,你可以通过-s或-slow
来指定高亮显示超过指定时间的测试用例。
注意
你要是传递了在it中指定了done函数,那么就必须要记得调用它,不然,这个测试会一直在等待你调用来结束测试,直到执行超时。如果你不传递该参数,那么你就无须管。it的第二个函数中done参数就好像一个开关一般,你如果传递了,那么mocha就知道你是一个异步测试,需要等待你手动调用这个done才会结束这个it测试,如果没有传递done参数,那么当it的第二个参数函数执行完之后mocha就会直接结束掉这个it测试。
Promise支持
如果一个it测试用例直接返回一个Promise,那么,mocha会等待这个Promsie进行决议,并会根据他的决议的状态来觉得测试用例是否通过,例如:
describe('异步测试一', function () {
it('Promise测试', function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
})
});
})
结果
异步测试一
√ Promise测试 (1001ms)
1 passing (1s)
以上的Promise是1秒过后进入resolve状态,而如果是进入reject状态的话,那么测试就会不通过:
异步测试一
1) Promise测试
0 passing (1s)
1 failing
1) 异步测试一
Promise测试:
Error: the string "我是错误信息" was thrown, throw an Error :)
上面的结果中,我在reject时传递了一个字符串作为reject的错误信息,他也能将其打印出来。
如果是直接返回一个Promise时,那么done参数就没有必要了。
describe的钩子函数
在测试块describe中,存在这四个钩子函数,他会在describe执行的不同时期调用:
-
before():在该区块的所有测试用例之前执行
-
after():在该区块的所有测试用例之后执行
-
beforeEach():在每个单元测试(即it)执行前执行
-
afterEach():在每个单元测试(即it)执行后执行
使用
describe('钩子函数测试', function () {
let isRun = false;
beforeEach(function () {
isRun = true;
});
it('测试一', function () {
if (!isRun) {
throw new Error('钩子函数未执行');
}
});
})
结果
钩子函数测试
√ 测试一
1 passing (5ms)
测试通过,说明beforeEach钩子函数执行了,他们的执行顺序其实并不复杂,那么其他的钩子函数就不再演示了。
ps:如果在describe中使用了一个beforEach钩子,如果这个describe嵌套了describe,那么嵌套中的it测试在每次执行前也会执行这个beforeEach钩子,正好就是说明了:在
该区块
的所有测试用例
之前执行。
钩子函数的异步处理
钩子函数也可以进行异步处理,他的回调函数也接收一个done的参数,在mocha中和it是否是异步的判断条件一致,你传递了done就要调用done来结束。如:
describe('钩子函数测试', function () {
let isRun = false;
before(function (done) {
setTimeout(() => {
isRun = true;
done();
}, 50);
});
it('测试一', function () {
if (!isRun) {
throw new Error('钩子函数未执行');
}
});
})
结果还是能够通过测试,但是,他会不会如同it一样,可以返回一个Promise呢?
describe('钩子函数测试', function () {
let isRun = false;
before(function () {
return new Promise((resolve, reject) => {
setTimeout(() => {
isRun = true;
resolve();
}, 1000);
})
});
it('测试一', function () {
if (!isRun) {
throw new Error('钩子函数未执行');
}
});
})
结果
钩子函数测试
√ 测试一
1 passing (1s)
我们看到,也是可以通过的,那么如果这个返回的Promise的状态最后是reject,那么会发生什么呢?我们直接来看结果吧:
钩子函数测试
1) "before all" hook
0 passing (1s)
1 failing
1) 钩子函数测试
"before all" hook:
Error: Promise rejected with no or falsy reason
我们看到,他给出了清晰的提升,表示是before这个钩子函数出现了异常,并且测试没有通过,每个钩子给出的异常提示有所不同,各位可以试一试。
异步测试的补充
done方法
异步测试中的done方法,不管是钩子函数中的done方法还是it单元测试中的done方法,他们都接收一个Error对象(指js中的Error对象),当然你传字符串也行,注意,一旦你向done传递了参数,不管是字符串还是Error对象,mocha都会视为测试不通过,切记。
Promise和done
在3.0以及更新的版本中,如果同时返回了Promise并且调用了done方法,会触发一个异常,具体详见mocha的中文翻译中的介绍:https://www.jianshu.com/p/9c78548caffa
你别同时用就是了,也没必要。
async/await
钩子函数和it单元测试的函数参数也接受一个async/await函数。这样,他们的执行也是异步的。
延迟的根套件测试
如果你想在一个测试文件中的所有测试套件describe开始之前执行一些异步操作,那么你可以使用命令行中的--delay
来运行mocha,这样他会在测试文件的全局中注入一个run方法,只有你在运行run方法后,才开始此文件的测试。
// example.spec.js
let isRun = false;
setTimeout(() => {
isRun = true;
run();
}, 5000);
describe('钩子函数测试', function () {
it('测试一', function () {
if (!isRun) {
throw new Error('错误');
}
});
})
mocha在运行时,会等待5秒中才开始运行这个文件的测试用例,如果设置了--delay
参数,那么多个测试文件中,每个测试文件里面都需要显示执行run方法了。
ps:注意,你的run方法一定要在异步的行为中运行,不然,此文件的所有describe都无法执行。你即便使用
setTimeout(() => {}, 0);
或者立即决议的Promise中的then都行。即将其放在微任务或宏任务中。
管理测试用例
与其说是管理,不如说他在describe和it这两个函数上提供了一些方法以及mocha本身提供的特性来控制我们的测试套件和测试用例的执行。
only
describe和it方法都允许调用only方法,他表示仅仅运行某个测试套件或者测试用例。
describe('测试套件一', function () {
it('测试一', function () {
});
it.only('测试一', function () {
});
})
describe.only('测试套件二', function () {
it('测试一', function () {
});
it.only('测试一', function () {
});
})
结果:
测试套件一
√ 测试一
测试套件二
√ 测试一
2 passing (8ms)
在mocha的v3.0后的版本,他的only可以多次使用来定义运行测试用例的子集。即你想要那些进行测试,就在哪些测试套件或者测试用例上加only。
注意
-
他的only是针对所有测试文件,即一个文件中存在only,另一个不存在only,那么另外的那个文件的测试用例根本不会执行。
-
如果提供了钩子函数,那么钩子函数仍然会执行。
skip
他和only相反,是跳过哪些测试用例或者测试套件。如果同时存在only和skip,那么毫无疑问以only为主。所有被跳过的测试用例都会在报告中被标记为pending
。
最佳实践:使用skip跳过测试用例,而不是将其注释掉。
在it中你可以使用this.skip()来跳过这个测试用例,或者你想要跳过整个测试套件,你可以在before钩子函数中使用this.skip()进行跳过,这是为了方便你动态觉得要不要跳过某个测试:
describe('测试套件一', function () {
before(function () {
if (// 判断) {
// 某些情况跳过此测试套件
this.skip();
}
})
it('测试一', function () {
if(// 判断) {
// 某些情况跳过此测试
this.skip();
}
// 测试代码
});
it('测试二', function () {
// 测试代码
});
})
上诉代码中,如果before中执行了skip,那么这个测试套件的所有it单元测试都会变为pending状态,而before中没有执行skip,当测试一种执行了skip时,那么测试一会变为pending状态。测试二正常执行。
注意
-
被跳过的测试用例会被标记为pending状态,即使是在it中或者before钩子函数中使用this.skip()跳过测试套件或测试用例,也是pending状态。
-
不要什么也不做,一个测试应该编写断言或者使用this.skip。
-
mocha 3.0之前钩子函数和异步测试中不支持this.skip方法
关于skip和only跳过时的代码执行
当你把describe跳过了,那么并不意味着整个的describe跳过,那个describe绑定的函数还是会执行的,只不过其中里面的it的代码不会执行罢了,而it外部的代码还是会执行,要切记。
动态生成测试
你可以动态生成describe测试套件和it单元测试,以实现动态测试:
const descrName = ['测试套件一', '测试套件二'];
const itName = ['测试一', '测试二'];
descrName.forEach(function (dName) {
describe(dName, function () {
itName.forEach(function (name) {
it(name, function () {
// 测试代码
});
})
})
});
结果
测试套件一
√ 测试一
√ 测试二
测试套件二
√ 测试一
√ 测试二
4 passing (9ms)
如上,他产生了两个测试套件共四个测试用例。
测试耗时
当你在使用mocha命令行,并提供一个-s或者--slow
参数来指定当测试用例超过多少毫秒时则在报告中高亮显示测试用时。默认是75毫秒。你还可以在测试套件中指定这个值:
describe('测试', function() {
this.slow(1000);
it('测试用例一', function() {
// 测试代码
})
})
ps:至于这个超时是针对这个测试套件还是值的是整个测试呢,这里就不再啰嗦了,有兴趣的各位可以去尝试一下。而且如果和命令行中同时指定那谁的优先级高这个问题也略过了。
测试执行超时
即上下文的this.timeout(1000)
这种,它分为三种:
-
套件describe级别的超时
-
用例it级别的超时
-
钩子函数级别的超时
他们都可以使用this.timeout(0)
来取消超时限制。
参考
https://www.cnblogs.com/Leo_wl/p/5734889.html