做一个有温度和有干货的技术分享作者 ------ Qborfy
背景
开发公共平台项目,测试资源相对比较少,因此对开发者自身而言,为了维护项目的稳定性,需要对平台做各类测试,即使有测试环境,但是也很容易缺乏测试场景导致带着bug上线的情况。
因此我们需要做完整自动化测试方案,来避免这类常规错误,提高平台的可用性和稳定性。
这里先简单描述自动化测试的分类:
- 单元测试,验证独立的单元模块代码或函数是否正常工作
- 集成测试,验证多个单元模块间的协同工作
- UI 测试,只针对前端UI部分测试,后端数据采用mock方式
- 端到端测试,从用户的角度,通过机器来模仿用户在真实浏览器中验证应用交互
- 快照测试,验证程序的UI变化
接下来我们将根据这些测试类如何在项目中落地完整方案。
单元测试
前端项目主要用的单元测试框架为Jest
和Mocha
,下面就Jest
框架如何实现一个单元测试。
实现步骤
- 安装依赖
shell
npm i jest --save-dev
# 如果是typescript还需要安装 ts
npm i ts-jest babel-jest --save-dev
# 安装类型
npm i @types/jest --save-dev
- 新增测试命令,在
package.json
中scripts
新增代码
js
{
...
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
},
}
- 新增配置文件
jest.config.js
,参考配置如下:
js
module.exports = {
"testEnvironment": "node",
testMatch: [ //匹配测试用例的文件
'<rootDir>/test/**/*.test.ts'
],
transform: {
'^.+\\.js$': '<rootDir>/node_modules/babel-jest', // babel编译
'^.+\\.ts$': '<rootDir>/node_modules/ts-jest', // typescript编译
},
"collectCoverage": true
}
- 编写单元测试代码,在根目录下新建测试文件
test/sum.test.js
,标识对a.ts
文件做测试,代码如下:
js
//sum.ts
export const sum = (a, b) => {
return a + b;
}
//sum.test.ts
import { sum } from '../src/sum'
test("test two num sum", async () => {
const res = sum(5, 6);
expect(typeof res).toBe("number")
expect(res).toBe(11)
})
- 开始自动化测试
yarn test
或者npm run test
,然后会出现如下:
shel
PASS ./sum.test.ts
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
sum.ts | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.394 s, estimated 3 s
简单说明一下上面的表格几个字段作用:
- File,标识当前测试的文件
- Stmts,语句覆盖率(statement coverage):是不是每个语句都执行了
- Branch,分支覆盖率(branch coverage):是不是每个if代码块都执行了
- Funcs,函数覆盖率(function coverage):是不是每个函数都调用了
- Lines,行覆盖率(line coverage):是不是每一行都执行了
怎么提高单元测试覆盖率
分为两个部分:
- 提高代码质量,减少代码块的大小,减少各类复杂逻辑判断,不去测试有依赖性函数,如:需接口、数据库等
- 提高开发意愿,一自动化生成单元测试,二采用设置覆盖率指标,三是确定单元测试规范(哪些模块需要写,哪些不需要写)
集成测试
集成测试主要是测试当单元模块组合到一起之后是否功能正常。
相比较单元测试只是针对某个函数或方案做单一功能测试,集成测试是针对某个功能模块做完整的测试,因此在测试粒度上的选择,需要开发自己去衡量,但是一般的选择如下几种:
- 如果有UI展示的,一般通过router分割页面粒度去进行测试,尽可能的少mock依赖,尽量的渲染全子组件
- 如果是库不含JS的,则以功能模块为粒度进行测试,测试数据尽量丰富且贴近真实数据
具体实现步骤其实和单元测试一样,只是所写的测试功能比单元测试范围要大且完整。
UI 测试
相比较后面
端到端测试
,UI测试只是对于前端的测试,是脱离真实后端环境的,仅仅只是将前端放在真实环境中运行,而后端和数据都应该使用 Mock 的。
UI 测试在前端上,也可以叫组件测试 那么如何实现UI测试,其实依旧可以使用Jest
、Enzyme
selenium
Vitest
等框架,目前都支持将Vue
、React
等组件进行模拟渲染完成。
利用Jest
实现对React UI组件测试,代码如下:
js
// React Home.jsx
const Home = ()=>{
return (
<div>
<h1>Home</h1>
</div>
)
};
// home.test.js
import React from "react";
import { createRoot } from "react-dom/client"; // createRoot 是React18 新出的特性
import { act } from "react-dom/test-utils"; // react支持测试动作
import Home from "../src/pages/home";
global.IS_REACT_ACT_ENVIRONMENT = true
let root = null;
let container = null;
beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container)
});
afterEach(() => {
// 退出时进行清理
// root.unmount(container);
container.remove();
container = null;
});
it("渲染有或无名称", () => {
act(() => {
root.render(<Home />);
});
expect(container.textContent).toBe("Home");
});
当然你依然可以使用Jest
去做Vue UI 测试,但是Vitest
实现在vite项目中更加好用,代码如下:
js
// Home.vue
<script setup lang="ts">
defineProps<{ msg: string }>()
</script>
<template>
<h1>{{ msg }}</h1>
</template>
//home.test.js
import { mount } from '@vue/test-utils'
import Home from '../src/components/Home.vue'
test('mount component', async () => {
expect(Home).toBeTruthy()
const wrapper = mount(Home, {
props: {
msg: 'Home',
},
})
expect(wrapper.html()).toContain('Home')
})
快照测试
快照测试是属于UI测试的一种分类,主要用于区分同样的数据下,页面UI展示是否发生变化,如果不一样则比较测试结果失败,有异常或者功能迭代。针对快照测试详细说明如下:
快照测试类似于"找不同"游戏。快照测试会给运行中的应用程序拍一张图片,并将其与以前保存的图片进行比较。如果图像不同,则测试失败。这种测试方法对确保应用程序代码变更后是否仍然可以正确渲染很有帮助。
当然,在前端中,其实并不是比较图片,而是比较前后生成的html结构,本质上是一个字符串的比较。
同理,如果一个功能模块,针对同样的输入,得出的结果是不一样,那么也是一种快照测试。
利用Jest
实现快照测试代码如下(基本和UI测试一样):
js
// home.test.js
import React from "react";
import { createRoot } from "react-dom/client";
import { act } from "react-dom/test-utils";
import pretty from "pretty";
import Home from "../src/pages/home";
global.IS_REACT_ACT_ENVIRONMENT = true
let root = null;
let container = null;
beforeEach(() => {
// 创建一个 DOM 元素作为渲染目标
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container)
});
afterEach(() => {
// 退出时进行清理
// root.unmount(container);
container.remove();
container = null;
});
it("渲染有或无名称", () => {
act(() => {
root.render(<Home />);
});
expect(container.textContent).toBe("Home");
// 快照对比 这里你可以先把html结构存储一份,然后再拿出来对比
expect(
pretty(container.innerHTML)).
toMatchInlineSnapshot(`
"<div>
<h1>Home1</h1>
</div>"
`);
});
E2E测试
E2E测试,也叫端到端测试,就是模拟真实环境下,用户不同操作行为的测试。
目前主要进行E2E测试框架有如下几种:
- puppeteer 一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome
- cypress 现代网络构建的下一代前端测试工具, 编写更快、更容易和更可靠的测试
- Selenium 是开源的自动化测试工具,它主要是用于Web 应用程序的自动化测试,不只局限于此,同时支持所有基于web 的管理任务自动化
- NightWatch 是一个用于web应用和网站上执行自动化端到端(end-to-end)测试的集成框架,用于主流浏览器中,简化编写和执行多种类型地测试程序
这几者的区别后面会有专门文章去描述,现在我们先利用cypress
实现E2E测试代码, 以Vue项目为主,如下: 步骤一,安装依赖
csharp
yarn add cypress -D
步骤二,新增脚本,package.json
json
{
"scripts": {
"cypress": "cypress open"
}
}
步骤三,后面按照其提示去添加测试文件,比如:index.cy.ts
,然后修改代码如下:
js
describe('template spec', () => {
it('has home text', () => {
// 访问首页
cy.visit('http://localhost:5173/')
// 断言是否有 Home 文字
cy.contains('Home')
})
})
具体效果如下图:
当然E2E没有这么简单,还有一些点击、输入等事件,甚至可以模拟登录等,这些详细操作放在后面cypress实战篇去讲解。
测试覆盖率
了解自动化测试后,我们还需要对测试覆盖率进行一个完整的了解。
测试覆盖率(test coverage)是衡量软件测试完整性的一个重要指标。掌握测试覆盖率数据,有利于客观认识软件质量,正确了解测试状态,有效改进测试工作。
其实上面提到单元测试的时候已经有讲过一些,这里再做一个完整都介绍。
覆盖率主要分为以下几种:
- 代码覆盖率, 如上述所说分为几种,如:行覆盖率、函数覆盖率等
- 需求覆盖率,测试所覆盖的需求数量与总需求数量的比值
总结
自动化测试在前端开发是必不可少的一个环节,因为前端是直接面向用户的,即使有测试团队支持,也难免会出现测试遗漏的场景,或者加大测试人力成本。
当然不同项目的自动化测试所需要的环节是不一样的,根据个人经验,建议不同场景采用自动化测试如下:
- 开发纯函数库,建议写更多的单元测试 + 少量的集成测试
- 开发组件库,建议写更多的单元测试、为每个组件编写快照测试、写少量的集成测试 + 端到端测试
- 开发业务系统,建议写更多的集成测试、为工具类库、算法写单元测试、写少量的端到端测试
- 开发公共平台项目,建议写更多的集成测试和完整的端到端测试