代码demo
1、什么是单元测试
单元测试允许你将独立单元的代码进行隔离测试,其目的是为开发者提供对代码的信心。通过编写细致且有意义的测试,你能够有信心在构建新特性或重构已有代码的同时,保持应用的功能和稳定。
2、Vue单元测试要会什么?
- Vue
- Jest
- vue的测试环境搭建,创建项目时已经安装unit Test
3、修改配置文件
3.1、支持测试test后缀的文件
因为Vue自带的配置只支持spec配置的文件,所以可以修改配置支持测试test后缀的文件
<br/>
修改jest.config.js文件,添加以下字段
testMatch: [
"**/**tests**/**/*.[tj]s?(x)",// *test*/目录的js,ts,jsx,tsx文件 官方推荐
"**/?(*.)+(spec|test).[tj]s?(x)"// 后缀为spec或者test的js,ts,jsx,tsx文件
]
让Eslint能够检测到test后缀文件,修改.eslintrc.js文件修改overrides下files字段
"**/tests/unit/**/*.spec.{j,t}s?(x)" // 修改前
"**/tests/unit/**/*.(spec|test).{j,t}s?(x)" // 修改后
3.2、支持控制台打印测试例子描述语
没描述
修改jest.config.js文件,添加以下字段
verbose: true // 支持控制台打印测试例子描述语
有描述
3.3、支持控制台打印测试报告和导出测试报告
没导出报告
修改package.json文件,script添加以下字段
"test:unit:coverage": "vue-cli-service test:unit --coverage",
可以使用下面的测试命令
yarn run test:unit:coverage // 命令作为测试需要报告的情况,
yarn run test // 命令作为常规测试
有导出报告
4、Jest的基本语法
4.1、test和expect
Jest拥有众多API,可以测试各种开发场景,其核心API是expect()和test(),每个测试用例都离不开这两个核心功能。test()函数主要用于描述一个测试用例,expect()函数是用于指示我们期望的结果。
test("1+1=2", () => {
expect(1 + 1).toBe(2);
});
还有其他的期望结果,比如期望结果为某种数据类型 expect.any()
// 期望结果是数组类型 toEqual true
test("结果是数组类型", () => {
expect([1, 2]).toEqual(expect.any(Array));
});
4.2、Matchers匹配器
匹配器(Matchers)是Jest中非常重要的一个概念,它可以提供很多种方式来让你去验证你所测试的返回值,匹配器可以通俗理解为相等操作。
4.2.1、相等匹配Matchers
toBe()
toBe()是测试expect的期望值是否全等于结果值。toBe()相当于js中的===
// toBe true
test("1+1=2", () => {
expect(1 + 1).toBe(2);
});
// toBe false
test("'1'+'1'=2", () => {
expect("1" + "1").toBe(2);
});
toEqual()
相对于toBe()的完全相等,toEqual()匹配的是内容是否相等,一般用于测试对象或数组的内容相等,不等于js中的==
// toEqual true
test("判断数组相同", () => {
const arr = [1, 2];
expect(arr).toEqual([1, 2]);
});
// toEqual false
test("'2'=2", () => {
expect('2').toBe(2);
});
4.2.2、相等匹配Matchers
toBeTruthy()
测试expect的期望值是否为true,在js的if语句中会返回true
// toBeNull true
test("判断null", () => {
expect(null).toBeNull();
});
toBeFalsy()
测试expect的期望值是否为false,在js的if语句中会返回false
// toBeUndefined true
test("判断undefined", () => {
expect(undefined).toBeUndefined();
});
toBeUndefined()
测试expect的期望值是否为undefined
// toBeTruthy true
test("在if中会返回true", () => {
expect(1).toBeTruthy();
expect([]).toBeTruthy();
expect({}).toBeTruthy();
});
toBeNull()
测试expect的期望值是否为null
// toBeFalsy true
test("在if中会返回false", () => {
expect(0).toBeFalsy();
expect("").toBeFalsy();
});
4.2.3、逻辑判断Matche
not
取非相当于js中的!
// not true
test("取非", () => {
expect(0).not.toBeTruthy();
});
toBeGreaterThan ()
大于相当于js中的>
// toBeGreaterThan true
test("大于", () => {
expect(10).toBeGreaterThan(9);
});
toBeGreaterThanOrEqual ()
大于等于相当于js中的>=
// toBeGreaterThanOrEqual true
test("大于等于", () => {
expect(10).toBeGreaterThanOrEqual(9);
expect(10).toBeGreaterThanOrEqual(10);
});
toBeLessThan ()
小于相当于js中的<
// toBeLessThan true
test("小于", () => {
expect(9).toBeLessThan(10);
});
toBeLessThanOrEqual ()
小于等于相当于js中的<=
// toBeLessThanOrEqual true
test("小于等于", () => {
expect(9).toBeLessThanOrEqual(10);
expect(10).toBeLessThanOrEqual(10);
});
4.2.4、浮点计算
toBeCloseTo()
toBeCloseTo()浮点计算时不能做到严格相等要使用toBeCloseTo,检测浮点数在某一精度是否相等
// toBe false
test("0.1 + 0.2 == 0.3", () => {
expect(0.1 + 0.2).toBe(0.3);
});
// toBeCloseTo true
test("0.1 + 0.2 == 0.3 5精度", () => {
expect(0.1 + 0.2).toBeCloseTo(0.3, 5);
});
4.2.5、其他常用的匹配器
toMatch()
字符串匹配器 和字符串的match相同
// toMatch true
test("'sa' 包含于 'sabj'", function () {
expect("sabj").toMatch("sa");
});
toHaveLength()
检测对象是否具有length属性或者其属性值是否具体为某个值
// toHaveLength false
test("Number类型没有length属性", () => {
expect(2).toHaveLength(1);
});
// toHaveLength true
test("[1,2,3] 的长度为3", () => {
expect([1, 2, 3]).toHaveLength(3);
});
toHaveProperty()
检测对象是否具有某个字段,或者对应的字段值是否正确
// toHaveProperty false
test("该对象存在字段name", () => {
const obj = {
a: 1,
};
expect(obj).toHaveProperty("name");
});
// toHaveProperty true
test("该对象存在字段name,值为test", () => {
const obj = {
name: "test",
};
expect(obj).toHaveProperty("name", "test");
});
toContain()
数组匹配器 用于判断数组中是否包含某些值,严格相等
// toContain false
test("['1',1]中存在1", () => {
expect(["1", 2]).toContain(1);
});
// toContain true
test("['a','b']中存在a", () => {
expect(["a", "b"]).toContain("a");
});
5、Jest测试异步代码
5.1、定时器
新建timer.js,并编写方法函数
export default (fn) => {
setTimeout(() => {
fn()
}, 2000)
}
新建timer.test.js编写测试用例
test('测试timer', () => {
timer(() => {
expect(1+1).toBe(3)
})
})
运行后我们发现测试用例居然通过了,在测试用例里写的是1+1=3这怎么可能会是对的呢?如果说jest没有抽风的话,那可能的原因应该就是我们写的断言没有被执行到了,在timer.js文件中进行调试看看
export default (fn) => {
setTimeout(() => {
console.log("timeout1");
fn();
console.log("timeout2");
}, 2000);
}
执行后发现,果然在控制台中并没有把timeout打印出来,通过这个小例子我们不难发现,jest执行test方法时,从函数内部第一行执行到最后一行,当执行逻辑走到代码块最后一行时,没有异常就会返回测试成功,这个过程中不会去等待异步代码的执行结果,所以我们这样的测试方法,不管setTimeout里怎么实现,回调函数里怎么实现,都不会执行回调函数内部的逻辑。如果我们需要测试代码在真正执行了定时器里的异步逻辑后,才返回测试结果,我们需要给test方法的回调函数传入一个done参数,并在test方法内异步执行的代码中调用这个done方法,这样,test方法会等到done所在的代码块内容执行完毕后才返回测试结果,所以需修改测试用例
// false
test("测试timer 加done", (done) => {
timer(() => {
expect(1 + 1).toBe(3);
done();
});
});
这次执行测试代码后会发现,测试代码终于是报错了。同时只打印出了timeout1,可见jest在异常后的代码都不会执行。但这里我们又遇到了一个问题,设置定时器后结果真的是在2s后才返回,现在是只有一个定时器,如果以后项目中的延时有多个呢,或者说某个定时器的延迟时间很长的话,我们不是要等很长时间才能拿到结果,这就非常影响效率了。jest也考虑到了这一点,让我们可以使用fakeTimers模拟真实的定时器。这个fakeTimers在遇到定时器时,允许我们立即跳过定时器等待时间,执行内部逻辑。
// true
test("测试useFakeTimers", () => {
jest.useFakeTimers();
const fn = jest.fn();
timer(fn);
// 时间快进2秒
jest.advanceTimersByTime(2000);
expect(fn).toHaveBeenCalledTimes(2);
});
- 首先,使用jest.fn()生成一个jest提供的用来测试的函数,这样我们之后回调函数不需要自己去写一个
- 其次,使用jest.useFakeTimers()方法启动fakeTimer
- 最后,可以通过jest.advanceTimersByTime()方法,参数传入毫秒时间,jest会立即跳过这个时间值,还可以通过toHaveBeenCalledTimes()这个mathcer来测试函数的调用次数。
5.2 异步请求
我们在开发过程中,难免会进行数据请求等异步操作,Jest也考虑到了这一点,现在我们以异步请求数据为例,来说明如何使用Jest进行异步代码测试。
<br/>
为了能够访问api还需要修改jest.config.js文件,添加以下字段
testEnvironmentOptions: {
url: "http://localhost:8080" // 自己的URL
}
首先新建request.js, request.test.js这两个文件,在request.js文件请求一个本地的json文件
import axios from "axios";
function requestCallback(fn) {
axios.get("http://localhost:8080/test.json").then((res) => {
fn(res);
}).catch(res=>{
fn(res);
})
}
我们在request.test.js中,为了保证异步代码执行完毕后结束测试,和之前介绍的一样,在test的回调函数中传入done参数,在回调函数里执行done(),代码如下
test("测试回调请求", (done) => {
request.requestCallback((res) => {
var obj = {
"a": 1,
"b": 2
};
expect(res.data[0]).toEqual(obj);
done();
});
});
5.3 promise请求
我们现在改造一下request.js的代码,让它返回一个promise:
function requestPromise() {
return axios.get('http://localhost:8080/test.json')
}
为了测试上述代码,我们request.test.js也要做一定的修改:
test('测试promise请求', () => {
return request.requestPromise().then((res) => {
var obj = {
"a": 1,
"b": 2
};
expect(res.data[0]).toEqual(obj);
})
})
注意,上面的写法不需要传入done参数了,但是,需要我们使用return返回,如果不写return,那jest执行test函数时,将不会等待promise返回,这样的话,测试结果输出时,then方法将不会执行。
6、Jset中的钩子函数
Jest的钩子函数类似于Vue的生命周期函数,会在在代码执行的特定时刻,自动运行一个函数。Jest中有4个核心的钩子函数,分别为beforeAll、beforeEach、afterEach、afterAll,钩子函数均接受回调函数作为参数
创建calculator.js,创建hook.test.js
6.1、beforeAll
该钩子函数会在所有测试用例执行之前执行,通常用于进行初始化。
6.2、beforeEach
该钩子函数会在每个测试用例执行之前执行。
6.3、afterEach
该钩子函数会在每个测试用例执行之后执行
6.4、afterAll
该钩子函数会在所有测试用例执行之后执行。
class Calculator {
constructor() {
this.num = 1;
}
add() {
this.num += 1;
}
minus() {
this.num -= 1;
}
multiply() {
this.num *= 2;
}
divide() {
this.num /= 2;
}
}
let calculator = null;
// beforeAll通常可用来初始化
beforeAll(() => {
calculator = new Calculator();
console.log("beforeAll执行");
});
beforeEach(() => {
console.log("beforeEach执行");
});
afterAll(() => {
console.log("afterAll执行");
});
afterEach(() => {
console.log("afterEach执行");
});
test("测试Calculator中的add方法", () => {
calculator.add();
expect(calculator.num).toBe(2);
});
test("测试Calculator中的minus方法", () => {
calculator.minus();
expect(calculator.num).toBe(0);
});
test("测试Calculator中的multiply方法", () => {
calculator.multiply();
expect(calculator.num).toBe(2);
});
test("测试Calculator中的divide方法", () => {
calculator.divide();
expect(calculator.num).toBe(0.5);
});
7、Jest中的mock
用于测试模块外部函数是否被正确调用,一般使用以下3个特性
- 擦除函数的实际实现
- 捕获函数调用情况
- 设置函数返回值
7.1 jest.fn()
首先在mock.js写一个函数:
const run = fn => {
return fn('this is run!')
}
在mock.test.js文件中,
test('测试 jest.fn()', () => {
const fn = jest.fn(() => {
return 'this is mock fn 1'
})
})
fn()函数可以接受一个函数作为参数,这个函数就是我们想要jest.fn()为我们mock的函数,而且jest.fn()可以初始化时候不传入参数.
7.1.1、mockImplementation
改变mock函数内容,只会改变一次
7.1.2、mockImplementationOnce
改变mock函数内容
test("测试 jest.fn()", () => {
const func = jest.fn();
func.mockImplementation(() => {
return "this is mock fn 3";
});
func.mockImplementationOnce(() => {
return "this is mock fn 1";
});
func.mockImplementationOnce(() => {
return "this is mock fn 2";
});
const a = run(func);
const b = run(func);
const c = run(func);
console.log(a);
console.log(b);
console.log(c);
});
我们可以看到,函数执行的结果第一次是this is mock fn 2,之后都是this is mock fn 1
7.1.3、mockReturnValueOnce()
改变函数的返回值,只会改变一次
7.1.4、mockReturnValue()
改变函数的返回值
test("测试 jest.fn()", () => {
const func = jest.fn();
func.mockImplementation(() => {
return "this is mock fn 1";
});
func.mockImplementationOnce(() => {
return "this is mock fn 2";
});
const a = run(func);
const b = run(func);
const c = run(func);
console.log(a);
console.log(b);
console.log(c);
});
7.1.5、toBeCalled()
测试函数是否被调用
// true
test("测试 jest.fn()是否被调用", () => {
const func = jest.fn();
run(func);
expect(func).toBeCalled();
});
7.1.6、toBeCalledWith()
测试函数的传参数是否符合预期
// true
test("测试 jest.fn()", () => {
const func = jest.fn();
const a = run(func);
expect(func).toBeCalledWith("this is run!");
});
7.2、mock接口
很多时候,我们在前端开发过程中,后端接口还没有提供,我们需要去mock接口返回的数据
现在按照下列3个步骤:
- 在测试文件中导入需要模拟的模块
- 使用jest.mock()方法mock一下模块
- 使用.mockResolvedValue()模拟数据返回
jest.mock("axios");
// true
test("测试request", () => {
const obj = {
a: 1,
b: 2,
};
axios.get.mockResolvedValue({
data: {
a: 1,
b: 2,
},
});
return request.requestPromise().then((res) => {
expect(res.data).toEqual(obj);
});
});
8、组件测试
8.1、组件的挂载
由于前端组件化,使得UI测试变得容易很多。每个组件都可以被简化为类似于UI=fn(data)的表达式,这个表达式是一个描述UI是什么样的虚拟DOM,给这个表达式输入一些参数,就会得到UI描述的输出。
但是经过这样的抽象后还是存在一个问题,由于DOM是一个树形的结构。越处于上层的组件,其复杂度必然会随之提高。对于最底层的子组件来说,我们可以很容易得将其进行渲染并测试其逻辑的正确与否,但对于较上层的父组件来说,通常来说就需要对其所包含的所有子组件都进行预先渲染,甚至于最上面的组件需要渲染出整个 UI 页面的真实 DOM 节点才能对其进行测试,这显然是不可取的。
在vue-unit-test中就提供了shallowMount和mount 两个方法来实现组件的挂载,其中shallowMount就可以解决这个问题,它只渲染组件本身,但会保留子组件在组件中的存根。
区别这两种方法的目的在于,当我们只想对某个孤立的组件进行测试的时候,一方面可以避免其子组件的影响,另一方面对于包含许多子组件的组件来说,完全渲染子组件会导致组件的渲染树过大,这可能会影响到我们的测试速度
在组件挂载后,我们可以通过wrapper.vm访问到组件的实例,通过wrapper.vm进而可以访问到组件所有的props、data和methods等等。
import { shallowMount, mount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";
describe("HelloWorld.vue 判断组件是否挂载", () => {
it("test shallowMount", () => {
const wrapper = shallowMount(HelloWorld);
// 判断组件是否挂载
expect(wrapper.exists()).toBe(true);
// 访问vm实例
console.log(wrapper.vm);
});
it("test mount", () => {
const wrapper = mount(HelloWorld);
// 判断组件是否挂载
expect(wrapper.exists()).toBe(true);
});
});
8.2、DOM测试
8.2.1 find
在组件挂载后,无论哪种渲染方式所返回的 wrapper 都有一个.find()方法,它接受一个 selector 参数,然后返回一个对应的 wrapper 对象。而.findAll()则会返回一个类型相同的 wrapper 对象数组,里面包含了所有符合条件的子组件。与jquery中的用法类似selector 可以是CSS 选择器,除此之外也可以是 Vue 组件 或是一个 option 对象,以便于在 wrapper 对象中可以轻松地指定想要查找的节点。
除此之外,返回的包裹器内还有attributes、classes、element.style、text、html等属性用于验证。依然以以上HelloWorld.vue组件为例,如果我们要测试span标签是否有.item样式,是否有id,可以进行如下测试:
describe("HelloWorld.vue 查找第一个span标签", () => {
it("test attribute and class", () => {
const wrapper = shallowMount(HelloWorld);
// 查找第一个span标签
const dom = wrapper.find("span");
expect(dom.classes()).toContain("item");
expect(dom.attributes().id).toBeFalsy();
});
});
8.2.2 trigger
通过find方法查找 DOM 元素之后,还可以通过trigger方法在组件上模拟触发某个 DOM 事件,比如 Click,Change 等等。如下例子所示,有一个点击后按钮计数的组件
<template>
<div class="container">
<button id="testClick" @click="changeText">{{btnText}}</button>
</div>
</template>
<script>
export default {
data () {
return {
btnText: 1,
}
},
methods: {
changeText () {
this.btnText++
}
}
}
</script>
那么我们可以在以上组件的基础上撰写测试click原生事件的单元测试:
describe("HelloWorld.vue 测试click原生事件", () => {
it("test trigger click", () => {
const wrapper = shallowMount(HelloWorld);
const dom = wrapper.find("#testClick");
expect(wrapper.vm.btnText).toBe(1);
dom.trigger("click");
expect(wrapper.vm.btnText).toBe(2);
// 测试dom是否更新
expect(dom.text()).toContain(2);
});
});
但是此时运行测试用例后发现,expect(dom.text()).toContain(2)用例报错了,接受到的值为1。这是由于Vue 会异步的将未生效的 DOM 更新批量应用,以避免因数据反复突变而导致的无谓的重新渲染。我们要用nextTick做一下延时,来等待 Vue 把实际的 DOM 更新做完。
describe("HelloWorld.vue 测试click原生事件 延迟更新", () => {
it("test trigger click", async () => {
const wrapper = shallowMount(HelloWorld);
const dom = wrapper.find("#testClick");
expect(wrapper.vm.btnText).toBe(1);
dom.trigger("click");
expect(wrapper.vm.btnText).toBe(2);
// 测试dom是否更新
await wrapper.vm.$nextTick();
expect(dom.text()).toContain(2);
});
});
8.3、组件间值的传递测试
vue的组件免不了值得组件间通信,如props,emit以及slot等。在挂载后的wrapper中也提供了相应的api来进行测试。在测试组件间的变化时,最直观的测试方法就是,就是在父组件进行相关的操作,然后在测试子组件是否进行了对应的变化。但是这显然违反了单元测试的理念,我们只关心组件的输入输出,不关心组件间的联动。比如有如下的一个组件
<template>
<div class="container">
<span>{{name}}</span>
<span id="age" @click="change">{{age}}</span>
</div>
</template>
<script>
export default {
props: ['name', 'age'],
data () {
return {
myAge: 1,
}
},
methods: {
change () {
this.$emit('change-age', this.myAge)
}
}
}
</script>
该组件接收由父组件传递的name,和age两个参数。点击age字段时向父组件传递change-age时间将myAge传递给父组件。
describe("HelloWorld.vue 测试组件的传值", () => {
it("test props", () => {
const wrapper = shallowMount(HelloWorld, {
propsData: {
name: "keng",
age: "100",
},
});
expect(wrapper.props().name).toBe("keng");
expect(wrapper.props("age")).toBe("100");
});
it("test emit", () => {
const wrapper = shallowMount(HelloWorld);
const ageDom = wrapper.find("#age");
ageDom.trigger("click");
console.log(wrapper.emitted());
expect(wrapper.emitted("change-age")[0]).toEqual([1]);
});
});
在测试props时我们在挂载时利用propsData给props赋值,利用wrapper.props测试是否可以得到目标值,wrapper.props().name等价于wrapper.props('name')在对emit测试时模拟事件触发,测试目标事件是否同时触发,同时值是否也向外传递。
8.4、组件状态改变
使用setData或setProps 方法 直接操纵组件状态
describe("HelloWorld.vue 测试组件状态的改变", () => {
it("test setData", async () => {
const wrapper = shallowMount(HelloWorld);
await wrapper.setData({ btnText :2});
const dom = wrapper.find("#testClick");
expect(dom.text()).toContain(2);
expect(wrapper.vm.$data.btnText).toBe(2);
});
it("test setProps", () => {
const wrapper = shallowMount(HelloWorld);
wrapper.setData({
name: "keng",
age: "100",
});
expect(wrapper.props().name).toBe("keng");
expect(wrapper.props("age")).toBe("100");
});
});
版权属于:小小窝/禾下月
本文链接:https://hxyxyz.top/index.php/Web/339.html
本站文章采用 知识共享署名4.0 国际许可协议 进行许可,请在转载时注明出处及本声明!