文章目录
- vue项目单元测试编写-chai
- 开始
- 为什么使用断言库
- 使用
- BDD风格API
- 自然语句
- a(String type[, String errMsg])
- .not
- equal(Any val[, String errMsg])
- eql(Any val[, String errMsg])
- above&least&below&most
- within(Number start, Number finish[, String errMsg])
- instanceof(Constructor consturctor[, String errMsg])
- property(String name[, Any val, String errMsg])
- ownPropertyDescriptor(String name[, Object descriptor, String errMsg])
- lengthOf(Number len[, String errMsg])
- match(RegExp reg[, String errMsg])
- string(String str[, String, errMsg])
- include
- keys(key1, key2[, ...])
- throw()
- respondTo(String method[, String errMsg])
- itself
- satisfy(Function matcher[, String msg])
- .closeTo(Number expe, Number delta[, String errMsg])
- members(Array set[, String errMsg])
- oneOf(Array list[, String errMsg])
- change,increase,decrease
- by(Number delta[, String errMsg])
- finite
- fail(actual, expected, message, operator)
- extensible&sealed&frozen
- deep
- nested
- own
- ordered
- any和all
- .ok&.true&.false&.null&.undefined&.NaN
- exist
- empty
- 备注:API风格的错误信息
- 注意
- 参考
vue项目单元测试编写-chai
我们在测试时需要针对数据以及程序做一个预期,当程序的结果不是我们所预期的那样时,他会帮我们中断程序,并抛出一个错误,以便我们更好的对程序进行调试。
使用版本
4.x
开始
为什么使用断言库
因为mocha没有自己的断言库,所以需要第三方的断言库。而断言在我们单元测试中是必不可少的,他可以帮我们在某一个地方对程序的值,类型,结果等做出一系列的假设,当程序不符合预期时,他可以抛出一个错误来中断我们的测试,这样我们可以及时定位问题所在。比如:
// sum.js
export const sum = (x, y) => {
return x + y;
}
// example.spec.js
import { sum } from 'sum.js';
describe('myTest', function() {
it('测试一', function () {
// 对sum函数的调用结果进行断言
if (sum(1, 2) !== 3) {
// 只要程序抛出一个错误,mocha就会认为测试不通过
throw new Error('sum函数执行不符合预期');
}
});
});
上面的例子中,我们对一个加法函数sum进行测试,并且在测试用例中,对他的调用结果进行了断言,预期sum(1, 2)
的结果为3
,如果结果是正确的,那么测试会通过,如果其函数调用结果不符合我们的预期结果,那么就会抛出一个错误,这样,我们的这个测试用例就不会通过。
使用
基本使用
我们上面的例子中也使用了断言对函数结果进行了预期,但是这么写第一点就是比较麻烦,因为你在编写测试时,务必需要进行大量类似的判断,来测试程序中可能会出错的地方,那么每个地方都这样写,那就很冗余了。第二点就是这么写可能一样看上去代码比较混乱,并且代码所表达的意图也没有那么的“一目了然”。
这时候,断言库就派上用场了。他的简单使用如下:
// sum.js
export const sum = (x, y) => {
return x + y;
}
// example.spec.js
// 引入chai中的expect函数,其作用就是用于断言的函数
import { expect } from 'chai';
import { sum } from 'sum.js';
// 在describe中编写
describe('myTest', function() {
it('测试二', function () {
// 不符合预期的断言
expect(sum(1, 2)).equal(4);
});
})
结果:
1 failing
1) myTest
测试二:
AssertionError: expected 3 to equal 4
+ expected - actual
-3
+4
at Context. (dist\webpack:\tests\unit\example.spec.js:24:1)
我们故意对sum函数预期了一个错误的值,所以我们的测试结果并没有通过,我们可以看到,chai中的expect断言函数他帮我们封装了错误异常处理,在我们给出的预期值和程序的实际值不一致时,他自动帮我们抛出异常让测试单元不通过,并且我们从结果来看,他对错误进行了更加详细的描述,并给出了我们的预期值和实际值,让我们更好的定位问题。而且他还有一个好处,那就是chai支持三种断言风格以便我们更好的进行选择。
断言风格
chai支持三种断言风格:
其中包含TDD风格断言assert以及BDD风格两种断言:should和expect,他的简单使用如下:
assert
// 判断是否相等
const foo = 'bar';
assert.equal(foo, 'bar', 'foo equal `bar`');
should
const foo = 'bar';
foo.should.equal('bar');
expect
const foo = 'bar';
expect(foo).to.equal('bar');
assert风格的这里暂且不说,这里主要使用expect为主,BDD风格中的expect和should风格的断言,他们都可以使用一种类似于自然语言的方式对断言进行描述,他们都支持一种链式语法构造你的断言,他们区别主要在于语法上,而should在某些情况下使用会有一些问题,我们来看为什么,如下例子:
// 引入expect和should函数
const { expect, should } from 'chai';
const foo = 'bar';
// should风格
foo.should.be.a('string');
foo.should.equal('bar');
// expect风格
expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
使用should风格时只需要引入should函数,使用expect风格时只需要引入expect函数即可。上面的例子中,expect和should在写法上有所不同,should类似于:foo应该是一个字符串,foo应该是等于bra
,而expect风格则类似于:我预期foo变量是一个字符串,我预期foo的值等于bar
。大概就是这么个区别,他们都可以使用类似的自然语法构造你的断言,比如be,to
这些,后续会进行介绍。而在使用should时,各位应该注意:
should是通过实例化对象原型进行断言,所以,他在一些情况可能不会工作(比如值时undefined的时候就不能用它,你总不能:undefined.should...吧),而且据说他在ie下会有些问题,所以,使用expect会更好一些。
BDD风格API
expect和should他们都有类似的语法风格,只不过他们在构造断言的方式上有所不同,所以,api都是类似的。以下会列举一些常用的api,不是所有的。具体api请参考官网:https://www.chaijs.com/api/bdd/
下文中的用例代码:指代下文使用的sum函数
// sum.js
export const sum = (x, y) => {
return x + y;
}
自然语句
这里的自然语句是指为了提高你的断言的可读性所增加的链式结构语句,仅仅为了提高断言的可读性,没有特殊的作用。
- to
- be
- been
- is
- that
- which
- and
- has
- have
- with
- at
- of
- same
- but
- does
- still
英语好点的,应该都大致明白他们是干啥的,这里就不多说了,反正我英语比较烂。
a(String type[, String errMsg])
断言目标的类型是type
,注意,type
的开头是小写的
expect('foo').to.be.a('string');
expect({a: 1}).to.be.an('object');
expect(null).to.be.a('null');
expect(undefined).to.be.an('undefined');
expect(new Error).to.be.an('error');
expect(Promise.resolve()).to.be.a('promise');
expect(new Float32Array).to.be.a('float32array');
expect(Symbol()).to.be.a('symbol');
.not
否定一条链接之后所有的断言
expect(sum(1, 2)).not.equal(1).with.a('string');
上述例子的含义为:sum(1, 2)之和不等于1且不是一个字符串
,并且它是可以通过测试的。但是:
我们虽然可以使用not来对程序进行反预期,即预期他不是什么,但是最好不要这样去做,在断言时,你应该是对程序的结果进行一个期待的断言,而不是对他进行无数次非期待的断言。你应该预期他是什么,而不是预期他不是什么。
这也是我们不使用断言库时,所编写的断言时有些麻烦的地方,因为我们不使用断言库编写预期代码时,通常为了能抛出错误,一般需要对我们的预期的表达式进行取反,比如我们期望sum(1, 1)等于2,但是为了让测试能够正常工作,我们需要编写他不等于2时,就抛出异常,这多多少少和我们的编写思路有所冲突。
我们最好按照下面的方式进行断言编写:
expect(sum(1, 1)).to.equal(2); // 推荐
expect(sum(1, 1)).not.equal(1); // 不推荐
equal(Any val[, String errMsg])
断言目标是否全等于所提供的val值,如果增加.deep那么会将全等替换为深度相等。
expect(1).to.equal(1);
expect({ a: 1 }).to.deep.equal({ a: 1 });
eql(Any val[, String errMsg])
断言目标深度相等于提供的val。其实就是deep+equal的组合
关于深度相等,参考:https://github.com/chaijs/deep-eql
above&least&below&most
- @param { Number } n
- @param { String } errMsg
这四个都差不多,从名字来看,他们分别断言数字目标大于/大于等于/小于/小于等于所提供的数字n。并且,他们可以和.lengthOf
连用。
expect(1).to.be.below(2);
// 使用lengthOf获取长度,然后再使用他们进行比较
expect('foo').to.have.lengthOf.most(4);
ps:注意,最好的实践是预期目标数组等于某个值,而不是使用这些进行大小比较。
他们的别名
-
above:.gt&.greaterThan
-
least: .gte
-
below: .lt&.lessThan
-
most: .lte
within(Number start, Number finish[, String errMsg])
断言数字目标大于等于start且小于等于finish,即在他们的区间之内。当然也可以和lengthOf连用,并且。最好的实践也是直接预期等于某个值,而不是预期在某个区间之内(除非有必要)
instanceof(Constructor consturctor[, String errMsg])
断言目标是consturctor的实例
property(String name[, Any val, String errMsg])
校验断言目标是否含有一个名为name的属性,如果提供了第二个参数val,那么也会校验提供其属性值也是否相等,默认使用全等,你可以使用.deep
来修改为深度相等
expect({ a: 1 }).to.have.property('a');
// 除了key,还会比较值
expect({ a: 1 }).to.have.property('a', 1);
// 如果不加.deep会导致断言失败
expect({ a: { b: 2 } }).to.have.deep.property('a', { b: 2 });
默认他也会去查找原型链中的属性,如果你不想他去搜索原型链,则可以在其之前使用
.own
来限定他的作用范围只限于其对象本身。
ownPropertyDescriptor(String name[, Object descriptor, String errMsg])
断言目标中的name属性是一个目标本身的自定义描述符属性,可枚举和不可枚举的都包含在搜索范围。
内容较多,如需深入了解请自行查阅官方文档:https://www.chaijs.com/api/bdd/#method_ownpropertydescriptor
lengthOf(Number len[, String errMsg])
断言目标的length或size与给定的值len是否相同,第二个可选参数是当断言失败时,控制台输出的错误信息。
// 第二个参数可选
expect([1, 2, 3]).to.have.lengthOf(4, '数组长度与预期值不符');
// 错误信息也可以给expect的第二个参数,上面的代码等同于
expect([1, 2, 3], '数组长度与预期值不符').to.have.lengthOf(4);
结果
1) 测试A
测试一:
数组长度与预期值不符
+ expected - actual
-3
+4
ps:注意和.not使用时的编写准则。
由于兼容性问题,lengthOf的别名length不能直接链接到未经声明的方法,反正推荐使用lengthOf来代替length方法。
match(RegExp reg[, String errMsg])
断言目标是否与所给出的正则reg相匹配。
string(String str[, String, errMsg])
断言目标字符串是否包含所给出的字符串str。
expect('abcdef').to.have.string('cd');
include
太多了,略
keys(key1, key2[, …])
@parma {String|Array|Object} keys
断言目标(对象,数组,map或set)的键值是否全部包含所给的键keys,仅仅是在目标自身的属性上面进行检索。
当目标是一个对象或者数组时
keys键可以以一个或者多个字符串的形式出现,也可以是单个数组参数。
expect({a: 1, b: 2, c: 3}).to.have.keys('a', 'b', 'c');
expect({a: 1, b: 2, c: 3}).to.have.keys(['a', 'b', 'c']);
// 数组的键值指的就是下标
expect(['a', 'b', 'c']).to.have.keys(0, 1, 2);
// 这个键值的对比就不是全等了,比如下面这个也是可以通过测试的
expect(['a', 'b', 'c']).to.have.keys(0, 1, '2');
还可以是一个对象的对象参数,如果是这种情况,因为仅仅是对比其键,那么此对象的值会被忽略:
// 对象{a: 4, b: 5, c: 6}中属性的值会被忽略掉,你随意填什么都ok
// 断言是成功的
expect({a: 1, b: 2, c: 3}).to.have.keys({a: 4, b: 5, c: 6});
当目标对象是一个map或者set时
// 注意:set他的键名和键值可以说都是一个,和数组有区别的。
expect(new Set(['a', 'b'])).to.have.keys('a', 'b');
// 此时的mep类似于:{ 'a': 1, 'b': 2 }
expect(new Map([['a', 1], ['b', 2]])).to.have.keys('a', 'b');
这时候,参数就只能以参数分隔符的方式提供了,并且set和map的键的比较默认是===
全等的比较,你可以在前面增加.deep
来使用改为深度相等的方式进行比较:
// 断言失败
expect(new Set(['a', { b: 2 }])).to.have.keys('a', { b: 2 });
// 断言成功,注意,这里除了深度对比键之外,还会使用全等对比值是否相等
expect(new Set(['a', { b: 2 }])).to.have.deep.keys('a', { b: 2 });
// 下面因为b的值不全等,所以断言失败
expect(new Set(['a', { b: 2 }])).to.have.deep.keys('a', { b: '2' });
注意,默认情况下,你必须给出所有的键,这样断言才会通过,其等价于在断言链之前增加了一个.all
,你可以在链式之前使用.any
使目标仅包含给出的其中一个键,虽然两者都不存在时默认是使用all,但是推荐显示添加all来提高阅读性(上面给出的例子忽略掉了)
注意,当.include
和.any
同时存在时,仅.any
生效。
throw()
- @param { Error | ErrorConstructor } errorLike
- @param { String | RegExp } errMsgMatcher error message
- @param { String } msg optional
作用:他的预期一个函数的执行会抛出一个错误,比如:
var badFn = function () { throw new TypeError('Illegal salmon!'); };
// 断言badFn会抛出一个错误,注意,断言目标是一个函数,这个函数由throw执行
expect(badFn).to.throw();
他的参数挺多的,具体不同参数会发生什么,详情请跳转至文章末尾的官网参考。
这里要提一句的是,如果想要断言目标函数不会抛出异常,直接使用.not
链接不带参数的.throw
即可,而不是带有具体错误的.throw
。
关于throw执行目标函数时的this问题
因为目标函数由throw执行,所以,他无法知晓this的指向,比如方法的调用,解决方案:其一,你可以使用
bind
函数绑定this,第二你可以把需要调用的方法用一个函数进行包裹起来。
函数执行时的参数问题
如果此函数执行需要参数,那么你可以使用一个函数进行包裹来实现,其他有此需求的地方都可以使用这种方法。
respondTo(String method[, String errMsg])
检测目标对象是否拥有method方法。
当检测目标是一个非函数时
如果检测目标是一个非函数,那么他会断言该对象包含所提供的method方法,并且此方法可以是自身的也可以是继承的,可以是可枚举也可以是不可枚举的。
当检测目标是一个函数时
他会断言目标函数的prototype对象包含所提供的method方法,并且此方法可以是自身的也可以是继承的,可以是可枚举也可以是不可枚举的。
respondTo可以同.itself
进行链接,这时,不管断言目标是一个函数还是一个非函数的对象,检测的都是断言目标。
function Cat () {}
Cat.prototype.meow = function () {};
Cat.hiss = function () {};
expect(Cat).itself.to.respondTo('hiss').but.not.respondTo('meow');
// 即使是原型上的方法也可以通过
expect(new Cat).itself.to.respondTo('meow');
itself
强制链中的所有断言都针对的是给定的目标,即使目标是一个函数。比如.respondTo
的断言目标如果是一个函数,那么其实检测的是此函数的prototype
对象,而不是函数本身,而如果增加了.itself
链,则.respondTo
针对的就是此函数而不是ptototype
对象了。
satisfy(Function matcher[, String msg])
将断言目标作为参数传递给matcher
并执行,然后并断言返回结果是真值(不是true)
expect(1).to.satisfy((x) => {
return x > 0;
});
// 使用not
expect(1).to.not.satisfy((x) => {
return x < 0;
});
.closeTo(Number expe, Number delta[, String errMsg])
断言目标值是否在expe +/- delta之间,比如expect(2).to.closeTo(3, 1)
表示:断言2是否在3-1
和3+1
之间。并且是包含临界值的,即使大于等于和小于等于
members(Array set[, String errMsg])
断言目标数组,是否具有包含给定的数组成员set相同的成员
// 默认是不需要关心顺序
expect([1, 2, 3]).to.have.members([2, 1, 3]);
// 按照顺序来
expect([1, 2, 3]).to.have.ordered.members([1, 2, 3])
.but.not.ordered.members([2, 3, 1]);
// 默认是使用全等进行成员的比较,所以下面这一个断言失败
expect([1, 2, { a: 3 }]).to.have.members([2, 1, { a: 3 }]);
// 你可以使用deep来切换为深度比较,这样就可以通过断言
expect([1, 2, { a: 3 }]).deep.to.have.members([2, 1, { a: 3 }]);
默认,断言目标和所给定的数组都需要包含相同的成员,你可以使用.include
来指定仅需要包含断言的子集即可。
expect([1, 2, 3]).to.have.include.members([2]);
// 如果含有多个相同的断言目标数组中的成员,那么多余的会进行忽略,你别包含断言目标数组没有的成员就行了
expect([1, 2, 3]).to.have.include.members([2, 1, 1, 2]);
deep,ordered和include可以联合使用,注意,ordered和include连用时,记得保持顺序。
oneOf(Array list[, String errMsg])
断言目标值是否是数组list中的成员,默认使用===
全等,并且,最好的实践是断言目标是否等于某个值。
change,increase,decrease
不推荐直接使用change,最好是对明确的值进行比较
他们都是断言目标在给定函数参数执行后的值的变化。
let a = 1;
const add = () => { a += 1 };
const getA = () => { return a };
// 断言getA返回的值在add调用前后会发生变化
expect(add).to.change(getA);
// getA => 1 add调用前
// getA => 2 add调用后
increase则是断言调用后的值币调用前的值大,decrease则是断言调用后的值比调用前的值小。并且如果他们都提供了第二个参数props,那么第二个参数则表示比较的是返回值的props这个属性。
change的推荐代替写法
let a = 1;
const add = () => { a += 1 };
const getA = () => { return a };
expect(getA()).to.equal(1);
add();
expect(getA()).to.equal(2);
by(Number delta[, String errMsg])
当.by跟在.increase链式断言之后,.by断言.increase断言的增量为给定的差值(delta)。 当.by跟在.decrease链式断言之后,.by断言.decrease断言的减少量为给定的差值(delta)。
当.by跟在.change链式断言之后,.by断言.change断言的差量为给定的差值(delta)。但这样做会产生问题,因为这将会产生一个不确定的期待,通常最好的做法是确认一个精准的期待输出,而只用精确输出去断言。
var myObj = {val: 1}
, addTwo = function () { myObj.val += 2; }
, subtractTwo = function () { myObj.val -= 2; };
expect(addTwo).to.increase(myObj, 'val').by(2); // 推荐的做法
expect(addTwo).to.change(myObj, 'val').by(2); // 不推荐的做法
expect(subtractTwo).to.decrease(myObj, 'val').by(2); // 推荐的做法
expect(subtractTwo).to.change(myObj, 'val').by(2); // 不推荐的做法
finite
断言目标是一个数字,并且不是NaN
,正无穷和负无穷。
你可以在之前增加一个.not
,但最好不要这么做,因为断言的值可能不是一个数字,但可能是NaN,正无穷或者负无穷,所以,最好的做法是对其期待一个期望的类型,而不是断言他是许多个不符合断言的类型之一。
fail(actual, expected, message, operator)
抛出一个错误,可带参数,可不带参数。以下是错误打印时的参数配置:
actual:真实值, expected:预期值, message:错误信息, operator:选项
expect.fail();
expect.fail('err message');
expect.fail(1, 2)
expect.fail(1, 2, 'err message')
extensible&sealed&frozen
他们对应的就是es5对象的属性扩展,冻结等
-
extensible:断言目标对象是可扩展的
-
sealed:断言目标是冻结的,不可增加删除属性,但可以修改属性值
-
frozen:断言目标是冻结的,在sealed的基础上,既不可以增加删除属性,也不可以修改属性。
deep
将.equal, .include, .members, .keys 和 .property放在.deep链式之后将导致使用深度相等的方式来代替严格相等(===)
nested
在其后使用的.property 和 .include断言中可以使用.和括号表示法。这样可以断言深层次的属性。
expect({ a: { b: { c: { d: 1} } } }).to.have.nested.property('a.b.c.d', 1);
expect({ a: { b: { c: [ 1 ] } } }).to.have.nested.property('a.b.c[0]', 1);
own
使得其后的.property 和 .include断言中的继承属性被忽略。
Object.prototype.b = 2;
expect({a: 1}).to.have.own.property('a');
expect({a: 1}).to.have.property('b');
expect({a: 1}).to.not.have.own.property('b');
expect({a: 1}).to.own.include({a: 1});
expect({a: 1}).to.include({b: 2}).but.not.own.include({b: 2});
ordered
加在members断言之前会使其要求成员的顺序相同,members默认并没有要求顺序。
expect([1, 2, 3]).to.have.ordered.members([1, 2, 3])
.but.not.have.ordered.members([3, 2, 1]);
any和all
都是针对keys,如果在keys之前使用,any会使断言仅需要提供包含目标中的至少任一一个key即可,而all会要求提供包含目标的所有key。
expect({ a: 1, b: 2 }).to.have.all.keys('a', 'b');
expect({ a: 1, b: 2 }).to.have.any.keys('b');
.ok&.true&.false&.null&.undefined&.NaN
.ok是为了校验目标是一个真值,而.true是为了校验目标全等于===
true。.false就是校验目标全等于false,其他几个同理:
expect(true).to.be.true;
expect(undefined).to.be.undefined;
exist
断言目标不全等于null或undefined。但最好的实践还是让其全等于一个预期的值。
empty
他有如下情况:
-
当断言目标是一个字符串或者数组时,断言目标的
length
属性全等于0。 -
当断言目标是一个map或set时,断言目标的
size
全等于0当断言目标时一个非函数对象时,断言目标
自身
不含有可枚举的属性,符号属性会被排除在外,不进入计数。
备注:API风格的错误信息
当一些方法支持最后一个参数 errMsg 时,他们都是指当断言失败时的报告中打印的错误信息,当断言失败时,一般都会支持以下两种写法:
expect(target).somechain.somechain.(val, [msg]);
expect(target, [msg]).somechain.somechain.(val);
api中多次出现的errMsg均为可在此处传入错误信息的含义。如果该api参数中不包含[msg]参数,则说明其仅接受错误信息参数作为expect的第二个参数被给出。也就是仅支持第二种形式输出错误信息。
注意
- 如果需要对同一个函数或者程序内容进行比较复杂的断言,那么为了让断言易于阅读,并快速理解其表达的含义,可以考虑把比较复杂的断言拆分为多个断言。