*单元测试的必要性*
一般在我们的印象里,单元测试都是测试工程师的工作,前端负责代码就行了;百度搜索Vue单元测试,联想词出来的都是“单元测试有必要吗?” “单元测试是做什么的?”虽然我们平时项目中一般都会有测试工程师来对我们的页面进行测试“兜底”,但是根据我的观察,一般测试工程师并不会覆盖所有的业务逻辑,而且有一些深层次的代码逻辑测试工程师在不了解代码的情况下也根本无法进行触发。因此在这种情况下,我们并不能够完全的依赖测试工程师对我们项目测试,前端项目的单元测试就显得非常的有必要。
而且单元测试也能够帮助我们节省很大一部分自我测试的成本,假如我们有一个订单展示的组件,根据订单状态的不同以及其他的一些业务逻辑来进行对应文案的展示;我们想在页面上查看文案展示是否正确,这时就需要繁琐的填写下单信息后才能查看;如果第二天又又加入了一些新的逻辑判断(你前一天下的单早就过期啦),这时你有三个选择,第一种选择就是再次繁琐地填写订单并支付完(又给老板提供资金支持了),第二种选择就是死皮赖脸的求着后端同事给你更改订单状态(后端同事给你一个白眼自己体会),第三种选择就是代理接口或者使用mock数据(你需要编译整个项目运行进行测试)。
这时,单元测试就提供了第四种成本更低的测试方式,写一个测试用例,来对我们的组件进行测试,判断文案是否按照我们预想的方式进行展示;这种方式既不需要依赖后端的协助,也不需要对项目进行任何改动,可谓是省时又省力。
** 测试框架和断言库**
说到单元测试,我们首先来介绍一下流行的测试框架,主要是mocha和jest。先简单介绍下mocha,翻译成中文就是摩卡(人家是一种咖啡!不是抹茶啊),名字的由来估猜是因为开发人员喜欢喝摩卡咖啡,就像Java名字也是从咖啡由来一样,mocha的logo也是一杯摩卡咖啡:
和jest相比,两者主要的不同就是jest内置了集成度比较高的断言库expect.js,而mocha需要搭配额外的断言库,一般会选择比较流行的chai作为断言库,这里一直提到断言库,那么什么是断言库呢?我们首先来看下mocha是怎么来测试代码的,首先我们写了一个addNum函数,但是不确定是否返回我们想要的结果,因此需要对这个函数进行测试:
//src/index.js
function addNum(a, b) {
return a + b;
}
module.exports = addNum;
然后就可以写我们的测试文件了,所有的测试文件都放在tests目录下,一般会将测试文件和所要测试的源码文件同名,方便进行对应,运行mocha时会自动对tests目录下所有js文件进行测试:
//test/index.test.js
var addNum = require("../src/index");
describe("测试addNum函数", () => {
it("两数相加结果为两个数字的和", () => {
if (addNum(1, 2) !== 3) {
throw new Error("两数相加结果不为两个数字的和");
}
});
});
上面这段代码就是测试脚本的语法,一个测试脚本会包括一个或多个describe块,每个describe又包括一个或多个it块;这里describe称为测试套件(test suite),表示一组相关的测试,它包含了两个参数,第一个参数是这个测试套件的名称,第二个参数是实际执行的函数。
而it称为测试用例,表示一个单独的测试,是测试的最小单位,它也包含两个参数,第一个参数是测试用例的名称,第二个参数是实际执行的函数。
it块中就是我们需要测试的代码,如果运行结果不是我们所预期的就抛出异常;上面的测试用例写好后,我们就可以运行测试了。
测试addNum函数
√ 两数相加结果为两个数字的和
1 passing (5ms)
运行结果通过了,是我们想要的结果,说明我们的函数是正确的;但是每次都通过抛出异常来判断,多少有点繁琐了,断言库就出现了;断言的目的就是将测试代码运行后和我们的预期做比较,如果和预期一致,就表明代码没有问题;如果和预期不一致,就是代码有问题了;每一个测试用例最后都会有一个断言进行判断,如果没有断言,测试就没有意义了。
上面也说了mocha一般搭配chai断言库,而chai有好几种断言风格,比较常见的有should和expect两种风格,我们分别看下这两种断言:
var chai = require("chai"),
expect = chai.expect,
should = chai.should();
describe("测试addNum函数", () => {
it("1+2", () => {
addNum(1, 2).should.equal(3);
});
it("2+3", () => {
expect(addNum(2, 3)).to.be.equal(5);
});
});
这里should是后置的,在断言变量之后,而expect是前置的,作为断言的开始,两种风格纯粹看个人喜好;我们发现这里expect是从chai中获取的一个函数,而should则是直接调用,这是因为should实际上是给所有的对象都扩充了一个 getter 属性should,因此我们才能够在变量上使用.should方式来进行断言。
和chai的多种断言风格不同,jest内置了断言库expect,它的语法又有些不同:
describe("测试addNum函数", () => {
it("1+2", () => {
expect(addNum(1, 2)).toBe(3);
});
it("2+3", () => {
expect(addNum(2, 3)).toBe(5);
});
});
est中的expect直接通过toBe的语法,在形式上相较于mocha更为简洁;这两个框架在使用上极其相似,比如在异步代码上都支持done回调和async/await关键字,在断言语法和其他用法有些差别;两者也有相同的钩子机制,连名字都相同beforeEach和afterEach;在vue cli脚手架创建项目时,也可以在两个框架中进行选择其一,我们这里主要以jest进行测试。
** Jest**
Jest是Facebook出品的一个测试框架,相较于其他测试框架,最大的特点就是内置了常用的测试工具,比如自带断言、测试覆盖率工具,实现了开箱即用,这也和它官方的slogan相符。
Jest 是一个令人愉快的 JavaScript 测试框架,专注于简洁明快。
Jest几乎是零配置的,它会自动识别一些常用的测试文件,比如*.spec.js和 *.test.js后缀的测试脚本,所有的测试脚本都放在tests或__tests__目录下;我们可以在全局安装jest或者局部安装,然后在packages.json中指定测试脚本:
vue-cli4
"scripts": {
"test:unit": "vue-cli-service test:unit",
},
当我们运行npm run test:unit时会自动运行测试目录下所有测试文件,完成测试;我们在jest官网可能还会看到通过test函数写的测试用例:
test("1+2", () => {
expect(addNum(1, 2)).toBe(3);
});
和it函数相同,test函数也代表一个测试用例,mocha只支持it,而jest支持it和test,这里为了和jest官网保持统一,下面代码统一使用test函数。
** 匹配器**
我们经常需要对测试代码返回的值进行匹配测试,上面代码中的toBe是最简单的一个匹配器,用来测试两个数值是否相同。
test("test tobe", () => {
expect(2 + 2).toBe(4);
expect(true).toBe(true);
const val = "team";
expect(val).toBe("team");
expect(undefined).toBe(undefined);
expect(null).toBe(null);
});
toBe函数内部使用了Object.is来进行精确匹配,它的特性类似于===;对于普通类型的数值可以进行比较,但是对于对象数组等复杂类型,就需要用到toEqual来比较了:
test("expect a object", () => {
var obj = {
a: "1",
};
obj.b = "2";
expect(obj).toEqual({ a: "1", b: "2" });
});
test("expect array", () => {
var list = [];
list.push(1);
list.push(2);
expect(list).toEqual([1, 2]);
});
我们有时候还需要对undefined、null等类型或者对条件语句中的表达式的真假进行精确匹配,Jest也有五个函数帮助我们:
· toBeNull:只匹配null
· toBeUndefined:只匹配undefined
· toBeDefined:与toBeUndefined相反,等价于.not.toBeUndefined
· toBeTruthy:匹配任何 if 语句为真
· toBeFalsy:匹配任何 if 语句为假
test("null", () => {
const n = null;
expect(n).toBeNull();
expect(n).not.toBeUndefined();
expect(n).toBeDefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
test("0", () => {
const z = 0;
expect(z).not.toBeNull();
expect(z).not.toBeUndefined();
expect(z).toBeDefined();
expect(z).not.toBeTruthy();
expect(z).toBeFalsy();
});
test("undefined", () => {
const a = undefined;
expect(a).not.toBeNull();
expect(a).toBeUndefined();
expect(a).not.toBeDefined();
expect(a).not.toBeTruthy();
expect(a).toBeFalsy();
});
toBeTruthy和toBeFalsy用来判断在if语句中的表达式是否成立,等价于`if(n)和if(!n)``的判断。
对于数值类型的数据,我们有时候也可以通过大于或小于来进行判断:
test("number", () => {
const val = 2 + 2;
// 大于
expect(val).toBeGreaterThan(3);
// 大于等于
expect(val).toBeGreaterThanOrEqual(3.5);
// 小于
expect(val).toBeLessThan(5);
// 小于等于
expect(val).toBeLessThanOrEqual(4.5);
// 完全判断
expect(val).toBe(4);
expect(val).toEqual(4);
});
浮点类型的数据虽然我们也可以用toBe和toEqual来进行比较,但是如果遇到有些特殊的浮点数据计算,比如0.1+0.2就会出现问题,我们可以通过toBeCloseTo来判断:
test("float", () => {
// expect(0.1 + 0.2).toBe(0.3); 报错
expect(0.1 + 0.2).toBeCloseTo(0.3);
});
对于数组、set或者字符串等可迭代类型的数据,可以通过toContain来判断内部是否有某一项:shizhan
test("expect iterable", () => {
const shoppingList = [
"diapers",
"kleenex",
"trash bags",
"paper towels",
"milk",
];
expect(shoppingList).toContain("milk");
expect(new Set(shoppingList)).toContain("diapers");
expect("abcdef").toContain("cde");
});
实战案例
在components新建Counter.vue
<template>
<div>
<span>count: {{ count }}</span>
<button @click="handleClick">count++</button>
</div>
</template>
<script>
export default {
data () {
return {
count: 0
}
},
methods: {
handleClick () {
this.count++
this.$emit('change', this.count)
}
}
}
</script>
在tests/unit新建Counter.spec.js
import { mount } from '@vue/test-utils'
// @ts-ignore
import Counter from '@/components/Counter.vue'
import sinon from 'sinon'
// describe: 定义一个测试集
describe('Counter.vue', () => {
const change = sinon.spy()
// mount: 正常的渲染,子组件也会渲染
const wrapper = mount(Counter, {
listeners: {
change
}
})
// it: 每个 it 包含单元测试的最小集
// 计数器html
it('两数相加结果为两个数字的和', () => {
expect(wrapper.html()).toMatchSnapshot()
})
it('count++', () => {
const button = wrapper.find('button')
button.trigger('click')
// @ts-ignore
expect(wrapper.vm.count).toBe(1)
// 断言change方法被调用了
expect(change.called).toBe(true)
button.trigger('click')
expect(change.callCount).toBe(2)
})
})
版权归原作者 Jim-zf 所有, 如有侵权,请联系我们删除。