theme: devui-blue
前端自动化测试
1. 自动化测试基本概念介绍
前言
一般我们实现功能,基本都是两部分写代码,调试/测试。写代码和测试的时间,基本都是55分。如果是测试逻辑,可能要打一下log,或者打断点去调试。测试ui和逻辑,可能要点击界面进行交互,然后再测试逻辑,总之都是在手动测试。今天我给大家唠唠自动化测试
1.1黑盒测试和白盒测试
- 黑盒测试一般也被称为功能测试,黑盒测试要求测试人员将程序看作一个整体,不考虑其内部结构和特性,只是按照期望验证程序是否能正常工作(点点点,达到预期就行)
- 白盒测试是基于代码本身的测试,一般指对代码逻辑结构的测试(自动化化测试或者说自己测自己)
1.2测试分类
单元测试(Unit Testing)
单元测试是指对程序中最小可测试单元进行的测试,例如测试一个函数
、一个模块
、一个组件
…
- tip: 所以我们的组件库,特别适合单测。一些重要的模块,比如用户管理,支付管理,购物车管理…
集成测试(Integration Testing)
将已测试过的单元测试函数进行组合集成暴露出的高层函数或类的封装,对这些函数或类进行的测试。
- tip:集成测试通常在单元测试完成后进行。常见的集成测试包括:增量式测试,自顶向下测试
端到端测试(E2E Testing)
打开应用程序模拟输入,检查功能以及界面是否正确,比如puppeteer,Selenium,Cypress
1.3 TDD & BDD
TDD是测试驱动开发(Test-Driven Development)
TDD的原理是在开发功能代码之前,先编写单元测试用例代码
BDD是行为驱动开发(Behavior-Driven Development)
开会出需求,制作ui,开发,QA测试
总结: TDD是先写测试在开发 (一般都是单元测试,白盒测试),而BDD则是按照用户的行为来开发,再根据用户的行为编写测试用例 (一般都是集成测试,黑盒测试)
1.4 测试框架
- Karma Karma为前端自动化测试提供了跨浏览器测试的能力,可以在浏览器中执行测试用例
- Mocha 前端自动化测试框架,需要配合其他库一起使用,像chai、sinon…
- Jest Jest 是facebook推出的一款测试框架,集成了 Mocha,chai,jsdom,sinon等功能。
- Vitest Vitest 是一个相对较新的测试框架,专为 Vue (包括 Vue 2 和 Vue 3) 和 Vite 项目而生。它使用了 Vite 的优势,创建了一个更快、更现代的、易于设置的测试环境。
- …
2. Jest核心应用
比如我们现在要实现一个url的解析参数你会想到几种方式 ?
- 先来体验下手动测试
我们每写完一个功能,会先手动测试功能是否正常,测试后可能会将测试代码注释起来。这样会产生一系列问题,因为会污染源代码,所有的测试代码和源代码混合在一起。如果删除掉,下次测试还需要重新编写。
所以测试框架就帮我们解决了上述的问题
2.1 分组、用例
Jest 是基于
模块
的,我们需要将代码包装成模块的方式,分别使用export
将parser ,stringify 这两个方法导出
安装jest
npm init -y # 初始化pacakge.json
npm i jest
我们建立一个qs.test.js
来专门编写测试用例,这里的用例你可以认为就是一条测试功能 (后缀要以.test.js结尾,这样jest测试时默认会调用这个文件)
常用API: describe,it,expect,匹配器
describe
:describe
函数用于将一组相关的测试用例组织在一起。它接受两个参数:一个描述性的字符串,用于描述测试组的目的;一个回调函数,包含我们要运行的测试代码。回调函数中通常包含一个或多个it
函数。describe('这是一个测试组的描述', () => { // 这里放置测试代码 });
2.
it
:it
函数(也可以使用test
函数)表示一个具体的测试用例。它同样接受两个参数:一个描述性的字符串,用于描述测试用例的目的;一个回调函数,包含我们要验证的代码块。回调函数中通常包含一个或多个expect
函数。it('这是一个测试用例的描述', () => { // 这里放置要验证的代码 });
3.
expect
:expect
函数用于实际执行断言,检查代码的实际输出是否符合预期。它接受一个待测试的值,然后可以与一个“匹配器”(matcher)一起使用来检查预期结果。expect(实际值).匹配器(预期值);
通过配置scripts
来执行命令
"scripts": {
"test": "jest"
}·
这个时候会报错,因为node
环境下不支持es6模块
的语法,需要babel
转义,当然你也可以直接使用commonjs规范来导出方法,因为大多数现在开发都采用es6模块,所以就安装一下~
# core是babel的核心包 preset-env将es6转化成es5
npm i @babel/core @babel/preset-env --save-dev
并且配置.babelrc
文件,告诉babel用什么来转义
{
"presets":[
[
"@babel/preset-env",{
"targets": {"node":"current"}
}
]
]
}
2.2 matchers匹配器
- 主要三部分: 相等、不等、是否包含
2.3 测试操作节点方法
export const removeNode = (node) => {
node.parentNode.removeChild(node)
};
import { removeNode } from "../dom";
it("测试删除节点- 创建一个元素,期望不是Null,删除。重新获取,断言是他是null", () => {
document.body.innerHTML = '<div id="test"><div id="test1"></div>';
let div = document.getElementById("test");
expect(div).not.toBeNull();
removeNode(div);
// removeNode(document.body);
div = document.getElementById("test");
expect(div).toBeNull();
});
常用命令
- 只测试变化的文件 watchAll
- 只测试哪一个 only
"scripts": {
"test": "jest --watchAll"
}
3. Jest进阶
2.1 异步函数的测试
异步函数的两种情况: 回调函数 和 promise
// 异步代码测试
export const getDateCallback = (cb) => {
setTimeout(() => {
cb({ name: "wd" });
}, 3000);
};
export const getDataPromise = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ name: "wd" });
}, 3000);
});
};
test
import { getDataPromise, getDateCallback } from "../3.async";
describe("description", () => {
it.only("测试传入回调函数 获取异步返回结果", () => {
getDateCallback((data) => {
expect(data).toEqual({ name: "wd" });
});
});
});
result:
明明设置的三秒,为什么感觉没走? 因为不支持异步,希望等待结果完成再调用 done
import { getDataPromise, getDateCallback } from "../3.async";
describe("description", () => {
it.only("测试传入回调函数 获取异步返回结果", (done) => {
getDateCallback((data) => {
expect(data).toEqual({ name: "wd" });
done()
});
});
});
jest 中 promsie 该如何处理异步调用呢
方式一
it("测试promise done", (done) => {
return getDataPromise().then((data) => {
expect(data).toEqual({ name: "wd" });
done();
});
});
方式二
it("测试promise async + await", async () => {
let data = await getDataPromise();
expect(data).toEqual({ name: "wd" });
});
3.2 Jest 的 mock
看一个demo,将异步的时间调长
mock: 真函数不靠谱,用假函数。
3.2.1 模拟Timer
jest.useFakeTimers(); // 创建出一个模拟的timer
jest.runAllTimers(); // 运行所有的定时器
jest.advanceTimersByTime(5000); // 跳过多少毫秒
jest.runOnlyPendingTimers(); // 只运行当前等待的timer ‼️
我们这里用
虽然设置了很长的时间,但是我们使用mock 来模拟timer ,所以就直接变成同步执行了
4. Jest钩子函数
为了测试的便利,Jest中也提供了类似于Vue一样的钩子函数,可以在执行测试用例前或者后来执行
- beforeAll 在所有测试用例执行前执行
- afteraAll 在所有测试用例执行后
- beforeEach 在每个用例执行前
- afterEach 在每个用例执行后
5. Jest配置文件,测试覆盖率
npx jest --init
"scripts": {
"test": "jest --coverage"
}
可以直接执行npm run test
,此时我们当前项目下就会产生coverage报表来查看当前项目的覆盖率
- Stmts表示语句的覆盖率
- Branch表示分支的覆盖率(if、else)
- Funcs函数的覆盖率
- Lines代码行数的覆盖率
5. Vue 中集成Jest
在测试数据为空时,测试用例没有进行等待,直接断言了结果。由于 Vue.js 异步更新 DOM 的特性,如果不等待更新完成,测试的结果可能是不准确的。
5.1 测试HellowWorld组件
HellowWorld
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
test
import Vue from "vue";
import HelloWorld from "@/components/HelloWorld.vue";
describe("测试HelloWolrd 组件", () => {
it("传入 msg 属性看能否渲染到h1标签内", () => {
const baseExtend = Vue.extend(HelloWorld);
// 获取当前组件的构造函数,并且挂载此组件
const vm = new baseExtend({
propsData: {
msg: "hello"
}
}).$mount();
expect(vm.$el.innerHTML).toContain("hello");
});
});
是不是有点麻烦,我们可以使用 Vue Test Utils ,提供的mount/shallowMount 来代替Vue.extend,它用于安装和渲染我们的组件。我们将组件的属性传递给 mount
函数。另外,我们使用 wrapper
查询组件结构,并确保其 HTML 内容包含传递的 msg
属性。
describe("HelloWorld.vue", () => {
it("renders props.msg when passed", () => {
const msg = "new message";
const wrapper = shallowMount(HelloWorld, {
props: { msg }
});
expect(wrapper.text()).toMatch(msg);
expect(wrapper.find("h1").text()).toMatch(msg);
});
});
测试Todo组件
我们来采用TDD
的方式来测试,也就是先编写测试用例
先指定测试的功能: 我们要编写个Todo组件
- 当输入框输入内容时会将数据映射到组件实例上
it("当输入框输入内容时会将数据映射到组件实例上", () => {
// 1) 渲染Todo组件
let wrapper = shallowMount(Todo);
let input = wrapper.find("input");
// 2.设置value属性 并触发input事件
input.setValue("hello world");
// 3.看下数据是否被正确替换
expect(wrapper.vm.value).toBe("hello world");
});
- 如果输入框为空则不能添加,不为空则新增一条
it("如果输入框为空则不能添加,不为空则新增一条", async () => {
let wrapper = shallowMount(Todo);
let button = wrapper.find("button");
// 点击按钮新增一条
wrapper.setData({ value: "" }); // 设置数据为空
button.trigger("click");
console.log("len", wrapper.findAll("li").length);
expect(wrapper.findAll("li").length).toBe(0);
wrapper.setData({ value: "hello" }); // 写入内容
await button.trigger("click");
expect(wrapper.findAll("li").length).toBe(1);
});
- 增加的数据内容为刚才输入的内容
it("增加的数据内容为刚才输入的内容", async () => {
let wrapper = shallowMount(Todo);
let input = wrapper.find("input");
let button = wrapper.find("button");
input.setValue("hello world");
button.trigger("click");
await wrapper.vm.$nextTick();
expect(wrapper.find("li").text()).toMatch(/hello world/);
});
根据测试用例,写出代码
<template>
<div>
<input type="text" v-model="value" />
<button @click="addTodo">addTodo</button>
<ul>
<li v-for="(todo, index) in todos" :key="index">{{ todo }}</li>
</ul>
</div>
</template>
<script>
export default {
name: "todo",
methods: {
addTodo() {
this.value && this.todos.push(this.value);
}
},
data() {
return {
value: "",
todos: []
};
}
};
</script>
使用Jest钩子函数将创建包裹器和移除
describe("测试Todo组件", () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(Todo);
});
afterEach(() => {
wrapper.unmount();
});
})
好处
- 创建一个 Todo 组件的浅渲染 Wrapper实例,并将其保存为变量wrapper
- 在每个测试用例执行前调用 beforeEach 钩子函数,以确保每个测试用例都拥有一个统一的测试环境
- 在每个测试用例执行后调用 afterEach 钩子函数,以确保在一个测试用例执行完毕后清除其对应的测试环境
这样做能够避免不同测试用例之间的状态污染及其它副作用,确保测试用例的独立性。
总结
我们可能将这个Todo
组件进行拆分,拆分成TodoInput
组件和TodoList
组件和TodoItem
组件,如果采用单元测试的方式,就需要依次测试每个组件(单元测试是以最小单元来测试) 但是单元测试无法保证整个流程是可以跑通的,所以我们在单元测试的基础上还要采用集成测试
单元测试可以保证测试覆盖率高,但是相对测试代码量大,缺点是无法保证功能正常运行
2.集成测试粒度大,普遍覆盖率低,但是可以保证测试过的功能正常运行
3.一般业务逻辑会采用BDD方式使用集成测试(像测试某个组件的功能是否符合预期)一般工具方法会采用TDD的方式使用单元测试
4.对于 UI 组件来说,我们不推荐一味追求行级覆盖率,因为它会导致我们过分关注组件的内部实现细节,从而导致琐碎的测试