作者: 王振州 时间: 2020-12-24
目前开发大型应用,测试是一个非常重要的环节,但是大多数前端开发者对测试相关的知识是比较缺乏的,因为可能项目开发周期短根本没有机会写。所以你没有办法体会到前端自动化测试的重要性!
我们来看看我们在写代码的时候经常遇到的问题:
增加自动化测试后:
单元测试是指对程序中最小可测试单元进行的测试,例如测试一个函数
、一个模块
、一个组件
...
将已测试过的单元测试函数进行组合集成暴露出的高层函数或类的封装,对这些函数或类进行的测试
打开应用程序模拟输入,检查功能以及界面是否正确
Karma
为前端自动化测试提供了跨浏览器测试的能力,可以在浏览器中执行测试用例Facebook
推出的一款测试框架,集成了 Mocha,chai,jsdom,sinon
等功能。React
和 Vue
默认使用的是jest
, jest
也有一些缺陷就是不能像karam
这样直接跑早浏览器上,它采用的是jsdom
,优势是简单、0 配置! 后续我们通过 jest 来聊前端自动化测试
yarn init -y # 初始化package.json
yarn add jest
通过配置package.json
中的scripts
来执行命令
"scripts": {
"test": "jest"
}
但是一般我们的项目是使用ES6
语法来进行开发的,而node
默认是不支持的,所以需要 babel 进行转义
# core是babel的核心包 preset-env将es6转化成es5
yarn add @babel/core @babel/preset-env
配置.babelrc
文件
{
"presets": [
[
// https://www.babeljs.cn/docs/babel-preset-env
"@babel/preset-env",
// 预设options
{
// 描述项目支持的环境/目标。
"targets": {
// 如果希望根据当前node版本进行编译,可以指定“node”:true或“node”:“current”,这将与“node”:process.versions.node相同。
"node": "current"
}
}
]
]
}
默认jest
中集成了babel-jest
,运行时默认会调用.babelrc
进行转义,可以直接将ES6
转成ES5
语法, 这样我们就可以执行yarn jest
了
Jest
默认会调用执行以.test.js和.spec.js
结尾的文件
我们先写一个 qs.js 的文件包含两个待测试的函数
// qs.js
// 可以转换字符串变成对象 a=1&b=2 ==>> { "a": 1, "b": 2 }
export const parser = (str) => {
const obj = {};
// /([^&=?]+)=([^&=?]+)/g 全局匹配 中间是 = 左、右侧不是 = & ?
str.replace(/([^&=?]+)=([^&=?]+)/g, function () {
const [, key, value] = arguments;
// 看是否可以转换成
const valueN = +value;
// 赋值
obj[key] = isNaN(valueN) ? value : valueN;
});
return obj;
};
// 对象反转字符串 { "a": 1, "b": 2} ==>> a=1&b=2
export const stringify = (obj) => {
const arr = [];
for (const key in obj) {
arr.push(`${key}=${obj[key]}`);
}
return arr.join("&");
};
测试用例
// qs.test.js
import { parser, stringify } from "./qs";
describe("qs.spec", () => {
it("测试 parser 是否能正常解析结果", () => {
// expect 断言,判断解析出来的结果是否和 {name:'xw'}相等
expect(parser(`name=xw`)).toEqual({
name: "xw",
});
});
test("测试 stringify 是否能正常解析结果", () => {
expect(stringify({ name: "xw" })).toEqual(`name=xw`);
});
});
describe("qs.spec", () => {
it("测试 parser 是否能正常解析结果", () => {
// expect 断言,判断解析出来的结果是否和 {name:'xw'}相等
expect(parser(`name=xw`)).toEqual({
name: "xw",
});
});
test("测试 stringify 是否能正常解析结果", () => {
expect(stringify({ name: "xw" })).toEqual(`name=xw`);
});
});
decribe
会形成一个分组或作用域it
在写第一个测试用例时,我们一直在使用toEqual
其实这就是一个匹配器,那我们来看看jest
中常用的匹配器有哪些?因为匹配器太多了,所以我就说一些常用的!
测试的方法可以有 N 多中写法都可以达到效果,所以为了方便理解,可以把匹配器分为三类:判断相等、不等、是否包含
it("判断是否相等", () => {
expect(1 + 1).toBe(2); // 相等于 js中的===
expect({ name: "xw" }).toEqual({ name: "xw" }); // 比较内容是否相等
expect(true).toBeTruthy(); // 是否为 true / false 也可以用toBe(true)
expect(false).toBeFalsy();
expect(null).toBeNull(); // 是否为 null
expect(undefined).toBeUndefined(); // 是否为 undefined
});
it("判断不相等关系", () => {
expect(1 + 1).not.toBe(3); // not取反
expect(1 + 1).toBeLessThan(5); // js中的小于
expect(1 + 1).toBeGreaterThan(1); // js中的大于
});
it("判断是否包含", () => {
expect("hello world").toContain("hello"); // 是否包含
expect("hello world").toMatch(/hello/); // 正则
});
我们来写一个删除节点的方法,基于 jsdom,在 node 中模拟操作 dom 元素
// dom.js
export const removeNode = (node) => {
node.parentNode.removeChild(node);
};
核心就是测试传入一个节点,这个节点是否能从DOM
中删除
// dom.spec.js
import { removeNode } from "../dom";
describe("dom 测试", () => {
it("测试删除节点", () => {
// jest js-dom 可以在node环境下模拟一套dom结构
// 先创建一个节点 扔到页面中,调用删除方法 再去看一下这个节点是否消失了
document.body.innerHTML = `<div><button data-btn="btn"></button</div>`;
let btn = document.querySelector('[data-btn="btn"]');
expect(btn).not.toBeNull(); // 说明扔到页面中了
removeNode(btn); // 删除逻辑
btn = document.querySelector('[data-btn="btn"]'); // 这里没有dom映射
expect(btn).toBeNull();
});
});
当我们希望每次更改测试后,自动重新执行测试,修改执行命令:(但是要配合git
使用)
"scripts": {
"test": "jest --watchAll"
// "test": "jest --watch" // 检测变化的文件
}
重新执行 yarn test
,
› Press f to run only failed tests. // 运行只运行失败用例
› Press o to only run tests related to changed files. // 只运行有更改的文件 需要配合git 因为git知道哪些文件变化了
› Press p to filter by a filename regex pattern. // 匹配某个文件取运行
› Press t to filter by a test name regex pattern. // 用测试的名字去过滤
› Press q to quit watch mode. // 退出
› Press Enter to trigger a test run. // 触发运行
这时就会监控文件的修改重新执行
说到异步函数无非就两种情况,一种是回调函数的方式,一种就是目前比较流行的 promise 的方式
我们先写两个异步函数
// async.js
export const getDataThroughCallback = (fn) => {
setTimeout(() => {
fn({ name: "xw" });
}, 1000);
};
export const getDataThroughPromise = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ name: "xw" });
}, 1000);
});
};
编写async.spec.js
import { getDataThroughCallback, getDataThroughPromise } from "../async.js";
// 1. 默认测试用例不会等待测试完成,所以增加done参数,当完成时调用done函数
it("测试传入回调函数 获取异步返回结果", (done) => {
// 异步测试方法可以通过done
getDataThroughCallback((data) => {
expect(data).toEqual({ name: "xw" });
done();
});
});
// 2. 返回一个promise 会等待这个promise执行完成
it("测试promise 返回结果 1", () => {
return getDataThroughPromise().then((data) => {
expect(data).toEqual({ name: "xw" });
});
});
// 3. 直接使用async + await语法
it("测试promise 返回结果 2", async () => {
let data = await getDataThroughPromise();
expect(data).toEqual({ name: "xw" });
});
// 4. 使用自带匹配器
it("测试promise 返回结果 3", async () => {
expect(getDataThroughPromise()).resolves.toMatchObject({ name: "xw" });
});
通过 jest.fn 声明的函数可以被追溯, 可以记录查看具体的执行过程!
// map.js
export const myMap = (arr, fn) => {
return arr.map(fn);
};
import { myMap } from "../map";
// 通过jest.fn声明的函数可以被追溯
describe("jest.fn", () => {
it("测试 map 方法", () => {
let fn = jest.fn((item) => (item *= 2));
expect(myMap([1, 2, 3], fn)).toEqual([2, 4, 6]);
// 调用3次
expect(fn.mock.calls.length).toBe(3);
// 第一次调用函数时的第一个参数是 1
expect(fn.mock.calls[0][0]).toBe(1);
// 第二次调用函数时的第一个参数是 1
expect(fn.mock.calls[1][0]).toBe(2);
// 第一次函数调用的返回值是 2
expect(fn.mock.results[0].value).toBe(2);
// 每次函数返回的值是 2,4,6
expect(fn.mock.results.map((item) => item.value)).toEqual([2, 4, 6]);
});
it("测试 fn 是否执行 方法", () => {
let fn = jest.fn((item) => (item *= 2));
myMap([1], fn);
expect(fn).toHaveBeenCalled();
});
});
看下上面的 fn.mock 都有什么
{
"calls": [
[
// 函数参数
1,
0,
[1, 2, 3]
],
[2, 1, [1, 2, 3]],
[3, 2, [1, 2, 3]]
],
// ...
// 返回值
"results": [
{
"type": "return",
"value": 2
},
{
"type": "return",
"value": 4
},
{
"type": "return",
"value": 6
}
]
}
我们希望对接口进行 mock,可以直接在__mocks__
目录下创建同名文件,将整个文件 mock 掉,例如当前文件叫api.js
import axios from "axios";
export const fetchUser = () => {
return axios.get("/user");
};
export const fetchList = () => {
return axios.get("/list");
};
创建__mocks__/api.js
export const fetchUser = () => {
return new Promise((resolve, reject) => resolve({ user: "xw" }));
};
export const fetchList = () => {
return new Promise((resolve, reject) => resolve(["香蕉", "苹果"]));
};
开始测试
jest.mock("../api.js"); // 使用__mocks__ 下的api.js
import { fetchList, fetchUser } from "../api"; // 引入mock的方法
it("fetchUser测试", async () => {
let data = await fetchUser();
expect(data).toEqual({ user: "xw" });
});
it("fetchList测试", async () => {
let data = await fetchList();
expect(data).toEqual(["香蕉", "苹果"]);
});
接着来看下个案例,我们期望传入一个 callback,想看下 callback 能否被调用!
export const timer = (callback) => {
setTimeout(() => {
callback();
}, 2000);
};
因此我们很容易写出了这样的测试用例
import { timer } from "./timer";
it("callback 是否会执行", (done) => {
let fn = jest.fn();
timer(fn);
setTimeout(() => {
expect(fn).toHaveBeenCalled();
done();
}, 2500);
});
有没有觉得很愚蠢,如果时间很长呢? 很多个定时器呢?这时候我们想到了mock Timer
import { timer } from "../timer";
jest.useFakeTimers();
it("callback 是否会执行", () => {
let fn = jest.fn();
timer(fn);
// 运行所有定时器,如果需要测试的代码是个秒表呢?
// jest.runAllTimers();
// 将时间向后移动2.5s
// jest.advanceTimersByTime(2500);
// 只运行当前等待定时器
jest.runOnlyPendingTimers();
expect(fn).toHaveBeenCalled();
});
为了测试的便利,Jest 中也提供了类似于 Vue 一样的钩子函数,可以在执行测试用例前或者后来执行
class Counter {
constructor() {
this.count = 0;
}
add(count) {
this.count += count;
}
}
module.exports = Counter;
我们要测试Counter
类中add
方法是否符合预期,来编写测试用例
import Counter from "../hook";
it("测试 counter增加 1 功能", () => {
let counter = new Counter(); // 每个测试用例都需要创建一个counter实例,防止相互影响
counter.add(1);
expect(counter.count).toBe(1);
});
it("测试 counter增加 2 功能", () => {
let counter = new Counter();
counter.add(2);
expect(counter.count).toBe(2);
});
我们发现每个测试用例都需要基于一个新的counter
实例来测试,防止测试用例间的相互影响,这时候我们可以把重复的逻辑放到钩子中!
钩子函数
钩子函数可以多次注册,一般我们通过 describe 来划分作用域
import Counter from "../hook";
let counter = null;
beforeAll(() => console.log("before all"));
afterAll(() => console.log("after all"));
beforeEach(() => {
counter = new Counter();
});
describe("划分作用域", () => {
beforeAll(() => console.log("inner before")); // 这里注册的钩子只对当前describe下的测试用例生效
afterAll(() => console.log("inner after"));
it("测试 counter增加 1 功能", () => {
counter.add(1);
expect(counter.count).toBe(1);
});
});
it("测试 counter增加 2 功能", () => {
counter.add(2);
expect(counter.count).toBe(2);
});
// before all => inner before=> inner after => after all
// 执行顺序很像洋葱模型 ^-^
我们可以通过 jest 命令生成 jest 的配置文件
yarn jest --init
会提示我们选择配置项:
➜ yarn jest --init
The following questions will help Jest to create a suitable configuration for your project
# 使用jsdon
✔ Choose the test environment that will be used for testing › jsdom (browser-like)
# 添加覆盖率
✔ Do you want Jest to add coverage reports? … yes
# 每次运行测试时会清除所有的mock
✔ Automatically clear mock calls and instances between every test? … yes
在当前目录下会产生一个jest.config.js
的配置文件
一般项目中可能会配置下面的配置项
testMatch
匹配哪些文件进行测试
transformIgnorePatterns
不进行匹配的目录
moduleFileExtensions
告诉Jest
需要匹配的文件后缀
transform
匹配到 xx 文件 文件的时候用 xx 处理
moduleNameMapper
处理webpack
的别名,比如:将@
表示 /src
目录
snapshotSerializers
将保存的快照测试结果进行序列化,使得其更美观
刚才产生的配置文件我们已经勾选需要产生覆盖率报表,所有在运行时我们可以直接增加 --coverage
参数
"scripts": {
"coverage": "jest --coverage"
}
可以直接执行yarn coverage
,此时我们当前项目下就会产生coverage
报表来查看当前项目的覆盖率
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
async.js | 100 | 100 | 100 | 100 |
dom.js | 100 | 100 | 100 | 100 |
hook.js | 100 | 100 | 100 | 100 |
map.js | 100 | 100 | 100 | 100 |
qs.js | 100 | 100 | 100 | 100 |
timer.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 8 passed, 8 total
Tests: 17 passed, 17 total
Snapshots: 0 total
Time: 8.018 s
到此我们的Jest
常见的使用已经基本差不多了!下面我们来看下Vue
相关的组件等如何测试
如果是通过 vue-cli 创建的项目可忽略该步骤,因为在创建项目的时候可能就已经集成了相关环境配置;我是使用的是一个空项目并无 Vue 开发相关环境,因此要测试的话需要配置相关环境,安装 node 包:
vue
安装 vue 包
vue-template-compiler
vue 模板解析器
babel-core@^7.0.0-bridge.0
由于 vue-jest 使用的是 6.x 的版本,而前面使用的@babel/core 是 7.x 版本,所以要使用这个桥接包, 当然你也可以卸载@babel/core 7.x 版本,直接使用 6.x 的版本
@vue/test-utils
Vue.js 官方的单元测试实用工具库
vue-jest
处理.vue
文件
jest-transform-stub
处理图片、css 等
babel-jest
处理 jsx
jest-serializer-vue
处理 vue 组件快照
jest-watch-typeahead
watch 提示插件
jest.config.js 配置
module.exports = {
moduleFileExtensions: [
// 测试的文件类型
"js",
"jsx",
"json",
"vue",
],
transform: {
// 转化方式
"^.+\\.vue$": "vue-jest", // 如果是vue文件使用vue-jest解析
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$":
"jest-transform-stub", // 如果是图片样式则使用 jest-transform-stub
"^.+\\.jsx?$": "babel-jest", // 如果是jsx文件使用 babel-jest
},
transformIgnorePatterns: [
// 转化时忽略 node_modules
"/node_modules/",
],
moduleNameMapper: {
// @符号 表示当前项目下的src alias 别名
"^@/(.*)$": "<rootDir>/src/$1",
},
snapshotSerializers: [
// 快照的配置
"jest-serializer-vue",
],
testMatch: [
// 默认测试 /test/unit中包含.spec的文件 和__tests__目录下的文件
"**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)",
// 匹配所有spec.js/test.js结尾的文件
"**/*/*.(spec|test).js",
],
testURL: "http://localhost/", // 测试地址
watchPlugins: [
// watch提示插件
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname",
],
};
在 src/components/HelloWorld 目录下创建 HelloWorld 组件
<template>
<div class="hello">
<h1>{{ massage }}</h1>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
data() {
return {
massage: this.msg,
};
},
};
</script>
在 src/components/HelloWorld 目创建测试用例文件 HelloWorld.spec.js
import Vue from "vue";
import HelloWorld from "@/components/HelloWorld";
describe("测试HelloWolrd 组件", () => {
it("extend: 传入 msg 属性看能否渲染到h1标签内", () => {
const baseExtend = Vue.extend(HelloWorld);
// 获取当前组件的构造函数,并且挂载此组件
const vm = new baseExtend({
propsData: {
msg: "hello"
}
}).$mount();
expect(vm.$el.innerHTML).toContain("hello");
});
}
这样一个简单的 Vue 组件就测试成功了,但是写起来感觉不简洁也不方便!所以为了更方便的测试 Vue 官方提供给我们了个测试工具Vue Test Utils
import HelloWorld from "@/components/HelloWorld";
import { shallowMount } from "@vue/test-utils";
it("传入 msg 属性看能否渲染到h1标签内", () => {
const wrapper = shallowMount(HelloWorld, {
propsData: {
msg: "hello",
},
});
// 生成快照
// expect(wrapper).toMatchSnapshot();
expect(wrapper.find("h1").text()).toContain("hello");
});
可以直接渲染组件传入属性,默认返回wrapper
,wrapper
上提供了一系列方法,可以快速的获取 dom 元素! 其实这个测试库的核心也是在 wrapper
的方法上, 更多方法请看 Vue Test Utils
这里的shallowMount
被译为潜渲染,也就是说HelloWorld
中引入其他组件是会被忽略掉的,当然也有深度渲染mount
方法!
在测试 Vue 项目中,我们可能会在组件中发送请求,这时我们仍然需要对请求进行 mock
<template>
<ul>
<li v-for="(list, index) in lists" :key="index">{{ list }}</li>
</ul>
</template>
<script>
import axios from "axios";
export default {
data() {
return {
lists: [],
};
},
async mounted() {
let data = await axios.get("/list");
this.lists = data;
},
};
</script>
除了上面我们提到的 mock 掉 api 的方法,我们还可以 mock 掉 axios
// src/__mocks__/axios.js
export default {
get(url) {
return new Promise((resolve, reject) => {
if (url === "/list") {
return resolve(["香蕉", "苹果"]);
}
resolve({});
});
},
};
import List from "@/components/List";
import { shallowMount } from "@vue/test-utils";
jest.mock("axios");
describe("测试List组件", () => {
it("测试List组件 异步", (done) => {
let wrapper = shallowMount(List);
// 这里使用setTimeout的原因是我们自己mock的方法是promise,所以是微任务,
// 我们期望微任务执行后在进行断言,所以采用setTimeout进行包裹,保证微任务已经执行完毕!
// 如果组件中使用的不是 async、await形式,也可以使用 $nextTick,
// (新版node中await后的代码会延迟到下一轮微任务执行)
setTimeout(() => {
expect(wrapper.findAll("li").length).toBe(2);
done();
});
});
});
我们写了一个切换显示隐藏的组件,当子组件触发 change 事件时可以切换 p 标签的显示和隐藏效果
<template>
<div>
<Head @change="change"></Head>
<p v-if="visible">这是现实的内容</p>
</div>
</template>
<script>
// Modal.vue
import Head from "./Head";
export default {
components: {
Head,
},
data() {
return { visible: false };
},
methods: {
change() {
this.visible = !this.visible;
},
},
};
</script>
<template>
<div><Menu></Menu>Head</div>
</template>
<script>
// Head.vue
import Menu from "./Menu.vue";
export default {
name: "Head",
components: {
Menu,
},
};
</script>
<template>
<div>Menu</div>
</template>
<script>
// Menu.vue
export default {
name: "Menu",
data() {
return {};
},
methods: {},
};
</script>
下面我们编写测试用例,可以直接通过wrapper.findComponent
方法找到对应的组件来发射(emit)事件
import Modal from "@/components/Modal";
import Head from "@/components/Modal/Head";
import Menu from "@/components/Modal/Menu";
import { mount, shallowMount } from "@vue/test-utils";
describe("测试组件自定义事件", () => {
it("测试 触发change事件后 p标签是否可以切换显示", () => {
const wrapper = mount(Modal);
const headWrapper = wrapper.findComponent(Head);
// 验证 Head 组件是否存在
expect(headWrapper.exists()).toBeTruthy();
// 验证 p 标签是否被显示
expect(wrapper.find("p").exists()).toBeFalsy();
// 触发change事件
headWrapper.vm.$emit("change");
// console.log(wrapper.html());
// 检验方法是否被触发
expect(headWrapper.emitted().change).toBeTruthy();
// 由于vue的更新是异步的所以要使用$nextTick
wrapper.vm.$nextTick(() => {
// 验证 p 标签是否被显示
expect(wrapper.find("p").exists()).toBeTruthy();
});
});
});
到这里我们对vue
的组件测试已经基本搞定了,接下来我们再来看下如何对 Vue 中的vue-router
、vuex
进行处理
在你的组件中引用了全局组件 router-link
或者 router-view
组件时,我们使用shallowMount
来渲染会提示无法找到这两个组件,我们可以使用存根 studs 的方式mock
掉相关的组件
<template>
<div>
<h1>当前路由:{{ this.$route.path }}</h1>
<router-link to="/">首页</router-link>
<router-link to="/about">关于页面</router-link>
<router-view></router-view>
</div>
</template>
import Nav from "@/components/Nav";
import { shallowMount } from "@vue/test-utils";
it("测试Nav组件", () => {
let wrapper = shallowMount(Nav, {
// 忽略这两个组件
stubs: ["router-link", "router-view"],
mocks: {
// mock一些数据传入到Nav组件中
$route: { path: "/" },
},
});
expect(wrapper.find("h1").text()).toBe("当前路由:/");
});
我们可以也创建一个localVue
来安装 VueRouter,传入到组件中进行渲染。 安装 Vue Router
之后 Vue 的原型上会增加 $route
和 $router
这两个只读属性。所以不要挂载到基本的 Vue 构造函数上, 同时也不能通过mocks
参数重写这两个属性
// createLocalVue.spec.js
import Nav from "@/components/Nav";
import List from "@/components/List";
import { shallowMount, createLocalVue } from "@vue/test-utils";
import VueRouter from "vue-router";
const localVue = createLocalVue();
localVue.use(VueRouter);
it("安装vue-router: 测试Nav组件", async () => {
const router = new VueRouter({
routes: [
{ path: "/", component: List },
{ path: "/about", component: List },
],
});
const wrapper = shallowMount(Nav, {
localVue,
router,
});
router.push("/about");
wrapper.vm.$nextTick(() => {
expect(wrapper.find("h1").text()).toMatch(/about/);
});
});
我们通过一个计数器的例子来掌握如何测试 vuex
<template>
<div>
{{ this.$store.state.number }}
<button @click="add(3)">添加</button>
</div>
</template>
<script>
import { mapActions } from "vuex";
export default {
methods: {
...mapActions({ add: "increment" }),
},
};
</script>
编写store/index.js
import Vue from "vue";
import Vuex from "vuex";
import counter from "./counter";
Vue.use(Vuex);
export default new Vuex.Store(counter);
编写store/mutations.js
export default {
increment(state, count) {
state.number += count;
},
};
编写store/actions.js
export default {
increment({ commit }, count) {
setTimeout(() => {
commit("increment", count);
}, 1000);
},
};
编写store/counter.js
import mutations from "./mutations";
import actions from "./actions";
export default {
state: {
number: 0,
},
mutations,
actions,
};
下面我们开始测试
我们可以直接把 store 中的方法一一进行单元测试
就是一个个测试函数,但是需要 mock commit
和dispatch
方法
import mutations from "../mutations";
import actions from "../actions";
jest.useFakeTimers();
describe("单元测试store", () => {
it("测试mutation", () => {
const state = { number: 0 };
mutations.increment(state, 2);
expect(state.number).toBe(2);
});
it("测试action", () => {
const commit = jest.fn();
actions.increment({ commit }, 2);
jest.advanceTimersByTime(2000);
expect(commit).toBeCalled();
expect(commit.mock.calls[0][1]).toBe(2);
});
});
就是产生一个 store,进行测试 好处是不需要mock
任何方法
import Vuex from "vuex";
import { createLocalVue } from "@vue/test-utils";
import counter from "../counter";
jest.useFakeTimers();
const localVue = createLocalVue();
localVue.use(Vuex);
describe("测试运行中的store", () => {
it("测试是否可以异步增加 1", () => {
const store = new Vuex.Store(counter); // 创建一个运行store
expect(store.state.number).toBe(0);
store.dispatch("increment", 2);
expect(store.state.number).toBe(0);
jest.advanceTimersByTime(2000); // 前进2s
expect(store.state.number).toBe(2);
});
});
import Vuex from "vuex";
import Counter from "@/components/Counter";
import { createLocalVue, shallowMount } from "@vue/test-utils";
const localVue = createLocalVue();
localVue.use(Vuex);
let store;
let actions;
beforeEach(() => {
actions = {
increment: jest.fn(),
};
store = new Vuex.Store({
actions,
state: {},
});
});
describe("counter组件测试vuex", () => {
it("测试组件中点击按钮 是否可以 1", () => {
const wrapper = shallowMount(Counter, {
localVue,
store,
});
wrapper.find("button").trigger("click");
// 测试actions中的increment 方法是否能正常调用
expect(actions.increment).toBeCalled();
// 调用1次
expect(actions.increment.mock.calls.length).toBe(1);
// 第二个参数是3
expect(actions.increment.mock.calls[0][1]).toEqual(3);
});
});
到此关于 Vue 常见的测试就这些了,如果有遇到上面没提到的测试建议去jest 官网和@vue/test-utils官网上查找下
到此自动化测试相关的内容就到一段落了,我们来总结下:
1.单元测试可以保证测试覆盖率高,但是相对测试代码量大,缺点是无法保证功能正常运行
2.集成测试粒度大,普遍覆盖率低,但是可以保证测试过的功能正常运行
3.一般业务逻辑会采用 BDD 方式使用集成测试(像测试某个组件的功能是否符合预期)一般工具方法会采用 TDD 的方式使用单元测试
4.对于 UI 组件来说,我们不推荐一味追求行级覆盖率,因为它会导致我们过分关注组件的内部实现细节,从而导致琐碎的测试
{
"name": "nature-resource-cli3",
"version": "3.2.28",
"private": true,
"scripts": {
"serve": "vue-cli-service serve", // 开发环境
"build": "vue-cli-service build", // 生产环境
"report": "npm_config_report=true vue-cli-service build", // 依赖库分析 生产环境
"lint": "vue-cli-service lint", //格式化代码
"fix": "vue-cli-service lint --fix",
"commit": "git-cz", // 提交代码
"permission": "vue-cli-service serve --mode permission", // 开发环境 并设置mode为permission
"test:unit": "vue-cli-service test:unit", // 单元测试
"test:dev": "vue-cli-service test:unit --watchAll", // 开发环境 单元测试 文件发生变化自动触发
// 自定义脚本
"version": "node scripts/update-version.js",
"generate-router": "node scripts/generate-router.js",
"hjson": "node scripts/json-to-hjson"
},
// ...
"lint-staged": {
"src/**/*.{vue,js}": ["yarn lint", "git add"],
"src/**/*.{json,md,css,scss}": ["prettier --write", "git add"]
},
"husky": {
// husky 配置
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "yarn test:unit",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 5.0.0"
},
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
}
}