前端自动化测试
前端自动化测试在提高代码质量、减少错误、提高团队协作和加速交付流程方面发挥着重要作用。它是现代软件开发中不可或缺的一部分,可以帮助开发团队构建可靠、高质量的应用程序
单元测试(Unit Testing)和端到端测试(End-to-End Testing)是两种常见的测试方法,它们在测试的范围、目的和执行方式上有所不同。单元测试和端到端测试不是相互排斥的,而是互补的。它们在不同的层面和阶段提供了不同的价值,共同构成了一个全面的测试策略
单测和端测区别
单元测试(Unit)
- 单元测试关注于最小的可测试单元,如函数、方法或模块
- 目的是验证代码中的每个独立单元(如函数)是否按照预期工作
- 通常是自动化的、快速执行的,且不依赖于外部资源或其他模块
- 验证单个代码单元的行为,提供快速反馈,并帮助捕获和修复问题
端到端测试(End-to-End)
- 从用户角度出发,测试整个应用程序的功能和流程
- 模拟真实的用户交互和场景,从应用程序的外部进行测试。跨多个模块、组件和服务进行,以确保整个应用程序的各个部分正常协同工作
- 涉及用户界面(UI)交互、网络请求、数据库操作等,以验证整个应用程序的功能和可用性
总之,单元测试主要关注代码内部的正确性,而端到端测试关注整体功能和用户体验。结合使用这两种测试方法可以提高软件的质量和可靠性。在项目中尤其是公共依赖如组件库至少都需要单测,端测相对来说比较繁琐点,但是也是程序稳定的重要验证渠道
单元测试 - Jest
这里使用Jest作为单元测试工具,Jest 是一个用于 JavaScript 应用程序的开源测试框架。它是由 Facebook 开发和维护的,通常用于单元测试。Jest 具有简单易用的 API、丰富的功能和强大的断言库,广泛应用于前端开发和 Node.js 环境中
安装
➜ npm install jest -D
初始化
使用npx进行交互式生成默认的配置文件,它会提示你每步的选择:
➜ npx jest --init
The following questions will help Jest to create a suitable configuration for your project
✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Would you like to use Typescript for the configuration file? … no
✔ Choose the test environment that will be used for testing › jsdom (browser-like)
✔ Do you want Jest to add coverage reports? … yes
✔ Which provider should be used to instrument code for coverage? › v8
✔ Automatically clear mock calls, instances, contexts and results before every test? … yes
✏️ Modified test/package.json
📝 Configuration file created at test/jest.config.js
2
3
4
5
6
7
8
9
10
11
12
13
默认配置文件大概是下面的内容:配置中有很多注释提供我们参考,对于默认的配置就不用删除多语的注释了,方便参考。通常都是对需要的配置项做修改即可
const config = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// The test environment that will be used for testing
testEnvironment: "jsdom",
// 省略其他...
};
module.exports = config;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
常用的配置:
collectCoverage:boolean值,用来生成覆盖率报告,通常也可以使用命令行
--coverage
参数生成moduleFileExtensions:对于引入文件可以省去文件后缀,jest会根据规则一一匹配
moduleNameMapper:模块匹配规则,告诉jest改模块的匹配路径
json{ moduleNameMapper: { // 当匹配到 .css 等结尾的文件时对应 /__mocks__/style-mock.ts 文件 "\\.(css|less|scss|sass)$": "<rootDir>/__mocks__/style-mock.ts", // 当匹配 @ui 开头的的对应到 src 文件夹 "@ui/(.*)": "<rootDir>/src/$1", }, }
1
2
3
4
5
6
7
8setupFiles:在测试环境准备后和安装jest框架前做一些配置,常用来添加一些全局环境模拟数据
setupFilesAfterEnv:在安装jest框架后对jest做一些扩展,相比setupFiles更加通用
testEnvironment:jest模拟的环境,可以选择node、jsdom来模拟node和浏览器环境
testMatch:指定要测试哪些文件
transform:使用一些插件对代码进行转义以便jest可以理解,如设置tsx转义
以上是最基本的配置,jest的配置还是很多的,还要官方有列举了一个表可以随时翻阅不用死记
转译器
Jest中有转义器的概念来帮助它理解编写的代码,可以比做babel对代码做一些转换来兼容浏览器,差不多一样的道理
模块引用转换
在单个测试文件中都会引入我们编写的代码,然后对此代码的功能进行测试,而前端通常都是以esmodule的形式进行函数的导出,jest默认使用的是commonjs,对于module语法jest不认识就会报错
tsimport { sum } from "../core"; // 报错 describe("第一个测试", () => { // ... })
1
2
3
4
5那么可以对jest添加转义器将esmodule模块的代码转换成commonjs就可以了。打开配置文件:
json// jest.config.js { transform: { "^.+\\.(ts|tsx|js|jsx)$": [ "babel-jest", { presets: [["@babel/preset-env", { targets: { node: "current" } }]] }, ], }, }
1
2
3
4
5
6
7
8
9
10
11上面使用了
babel-jest
和@babel/preset-env
的依赖包需要安装下:sh➜ npm i babel-jest @babel/preset-env -D
1这样就可以解决esmodule语法不识别的问题
转换typescript:目前项目中的文件都是以ts编写的,而默认情况下jest只识别js文件的,那么就需要对ts进行转译让jest识别
json// jest.config.js { transform: { "^.+\\.(ts|tsx|js|jsx)$": [ "babel-jest", { presets: [/* 其它... */["@babel/preset-typescript"]] }, ], }, }
1
2
3
4
5
6
7
8
9
10
11需要安装对应的
@babel/preset-typescript
;除了使用ts转义器也可以使用ts-jest
直接运行ts代码得益于ts的转译插件可以让jest的测试文件(或配置文件)都写成ts类型的,而在ts文件中对于不识别的jest工具会报错或者没有提示,安装jest的类型文件包
@types/jest
来告诉ts对应的jest类型,然后进行配置:json// tsconfig.json { "types": ["jest"] }
1
2
3
4转换jsx:假如项目中使用了jsx那么也要对jsx进行转义,这里以vue jsx为例
json// jest.config.ts { transform: { "^.+\\.(ts|tsx|js|jsx)$": [ "babel-jest", { // 省略其他 plugins: ["@vue/babel-plugin-jsx"], }, ], }, }
1
2
3
4
5
6
7
8
9
10
11
12
基本断言
基本环境配置好后,就到了测试的时间了,我们先来最简单的配置用起
// __tests__/demo.spec.ts
import { sum } from "src/utils";
describe("第一个测试", () => {
it("分组1", () => {
expect(sum(1, 2)).toBe(3);
});
});
// 或者不用分组
test("第一个测试", () => {
expect(sum(1, 2)).toBe(3);
});
2
3
4
5
6
7
8
9
10
11
12
13
这里介绍下几个关键字基本概念:
- describe:用来描述当前测试的整体内容
- it:用来分组测试
- test:用来描述当前测试,无分组
- expect:判断参数的值,其的返回值有多个断言方法,上面使用了
toBe
也就是等于的意思。除了次此断言有很多断言的条件,你可以点击这里阅读官方文档
执行测试
# 现在package中配置 jest 脚本,然后执行测试
➜ npm run test # npx jest
PASS __tests__/demo.spec.ts
第一个测试
✓ 分组1 (2 ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
utils.ts | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.519 s
Ran all test suites.
✨ Done in 1.02s.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
可以看到对应的测试文件、分组以及测试覆盖率
路径映射
上面在测试代码时会先引入对应的工具代码,如果都使用相对路径引入会显得很麻烦。在项目中通常都喜欢使用@
这种方式引入文件,在测试环境依然可以使用,这样也可以和项目中的文件路径保持一致
配置路径映射需要满足两个条件:
- jest识别路径映射
- ts识别路径映射(如果项目中用了ts)
配置jest路径映射
// jest.config.ts
{
moduleNameMapper: {
"@/(.*)": "<rootDir>/src/$1",
},
}
2
3
4
5
6
配置tsconfig
// tsconfig.json
{
"paths": {
"@/*": ["src/*"]
}
}
2
3
4
5
6
这样在测试文件中就可以使用路径映射降低心智负担
// __tests__/demo.spec.ts
import { sum } from "@/utils";
2
除了手动设置外还可以将tsconfig中的path直接作为路径映射,这样就减少了多处的修改。实现这一功能需要借助ts-jest
工具包,不同这个自己也可以写个逻辑实现
// jest.config.ts
const { pathsToModuleNameMapper } = require('ts-jest/utils')
const { compilerOptions } = require('./tsconfig')
export default {
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: "<rootDir>/",
}),
}
2
3
4
5
6
7
8
9
dom测试
jest支持浏览器环境,使用浏览器环境时需要安装对应的包,请根据版本可以选择jsdom
或jest-environment-jsdom
包进行安装,这里jest版本为28+
使用后者。测试文件修改如下:
// __tests__/demo.spec.ts
describe("第一个测试", () => {
it("分组1", () => {
// 使用 localStorage API
localStorage.setItem('a', '1');
expect(localStorage.getItem(('a'))).toBe('1')
});
});
2
3
4
5
6
7
8
运行测试用例:
➜ npm run test
PASS __tests__/demo.spec.ts
第一个测试
✓ 分组1 (2 ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 0 | 0 | 0 | 0 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.701 s, estimated 1 s
Ran all test suites.
✨ Done in 1.13s.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
异步测试
jest可以使用多种方式进行异步代码测试,通常使用promise、async就可以了
- 使用promise
- async/await
- 回调
这里模拟一个异步方法,通过上面的三种方式进行测试
// src/utils
export function getUser(name: string) {
return new Promise((resolve) => {
setTimeout(() => resolve(name), 1000);
});
}
2
3
4
5
6
使用Promise
// __tests__/demo.spec.ts
import { getUser } from "@/utils";
describe("测试异步代码", () => {
it("promise调用方式测试", () => {
const user = "小明";
// 使用then
getUser(user).then((res) => {
expect(res).toBe(user);
});
});
it("使用resolves测试promise", () => {
const user = "小李";
// 使用 .resolves 方式,注意这里要 return
return expect(getUser(user)).resolves.toBe(user);
})
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用async测试
// __tests__/demo.spec.ts
import { getUser } from "@/utils";
describe("测试异步代码", () => {
it("使用async测试", async () => {
const user = "小明";
const res = await getUser(user)
expect(res).toBe(user);
})
});
2
3
4
5
6
7
8
9
10
使用回调函数
回调函数默认通常是以前那种回调写法,这里需要对以上的异步函数进行调整,让其换成回调函数模式
// 接受一个cb,这里固定返回的值为true,没有错误
export function getUser(cb: (error: any, data: any) => void) {
setTimeout(() => {
cb(null, true);
}, 500);
}
// 定义测试
describe("测试异步代码", () => {
it("使用回调函数", (done) => {
function cb(error: any, data: any) {
if (error) {
done(error);
return;
}
try {
expect(data).toBe(true);
done();
} catch (err) {
done(err); // 这里一定要使用try catch,防止出错时没有执行done
}
}
getUser(cb);
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
回调模式一定要执行done
函数,如果没有执行则会被认为超时错误
模拟函数
假设要模拟一个工具函数的内部实现,可以使用mock函数来判断函数内部的值是否达到预期
定义个待测试的函数forEach
export function forEach(items: number[], callback: (num: number) => void) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
2
3
4
5
添加测试用例:
// __tests__/demo.spec.ts
import { forEach } from "@/utils";
// 模拟函数
const mockFn = jest.fn((x: number) => x + 1);
test("模拟函数", () => {
forEach([0, 1], mockFn);
expect(mockFn.mock.calls).toHaveLength(2);
expect(mockFn.mock.calls[0][0]).toBe(0);
expect(mockFn.mock.calls[1][0]).toBe(1);
expect(mockFn.mock.results[0].value).toBe(1);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
定时器
Jest可以通过一个函数转换计时器以便允许你控制时间流量
假设测试如下定时器代码:
export function useTimer(cb?: Function) {
setTimeout(() => cb && cb(), 1000);
}
2
3
编写测试用例:
import { useTimer } from "@/utils";
jest.useFakeTimers();
jest.spyOn(global, "setTimeout");
test("test timer", () => {
const cb = jest.fn();
useTimer(cb);
expect(cb).not.toBeCalled();
// 执行所有的定时器
jest.runAllTimers();
expect(cb).toBeCalled();
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
更多见官方文档
setup配置
写测试的时候你经常需要在运行测试前做一些准备工作,和在运行测试后进行一些收尾工作。 Jest 提供辅助函数来处理这个问题
这其中包括beforeEach、afterEach、beforeAll、afterAll,其中前两者在每个测试前都会执行一次,后者在文件中只会执行一次
覆盖率
除了对程序进行断言外,jest还收集代码的测试覆盖率并生成对应的报告,包括:某个函数内部的测试覆盖率、整个文件的覆盖率,要想达到覆盖率100%,就要测试到每个文件的所有代码、每个函数内部的所有分支条件
开启覆盖率
可以通过配置文件
// jest.config.ts
// 主要涉及到这两个配置
export default {
collectCoverage: true, // 启用
coverageDirectory: "coverage", // 报告生成位置
}
2
3
4
5
6
通过cli,执行脚本时带上参数
➜ npx jest --coverage
测试覆盖率
假设我们有这么一个函数
export function whatType(arg: any) {
const type = Object.prototype.toString.call(arg)
if (type === '[object String]') {
return 'string';
} else if (type === '[object Boolean]') {
return 'boolean';
}
}
2
3
4
5
6
7
8
添加测试用例
import { whatType } from "@/utils";
describe("测试覆盖率", () => {
it("函数条件覆盖率", () => {
expect(whatType(true)).toBe("boolean");
});
});
2
3
4
5
6
7
执行测试用例
➜ npm run test
PASS __tests__/demo.spec.ts
测试覆盖率
✓ 函数条件覆盖率 (1 ms)
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 96.77 | 50 | 100 | 96.77 |
index.ts | 96.77 | 50 | 100 | 96.77 | 4
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.486 s, estimated 1 s
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- File:测试的文件
- Stmts:测试中被执行的代码语句的比例
- Branch:测试代码条件分支比例
- Funcs:测试中被执行函数比例
- Lines:测试中被执行代码行数比例
- Uncovered Line:没有测试到的行数
除了查看终端的表格外,还可以使用更直观的报告,文件报告的结构大概如下:
coverage
├── clover.xml # xml格式
├── coverage-final.json # json格式
├── lcov-report # html格式
│ ├── base.css
│ ├── block-navigation.js
│ ├── favicon.png
│ ├── index.html # 主页面入口
│ ├── index.ts.html
│ ├── other.ts.html
│ ├── prettify.css
│ ├── prettify.js
│ ├── sort-arrow-sprite.png
│ └── sorter.js
└── lcov.info
2
3
4
5
6
7
8
9
10
11
12
13
14
15
一般都来查看HTML报告,打开报告页面
可以点击对应的文件查看更详细的报告
Vue组件测试
jest也可以对vue组件进行测试,vue官方提供了 vue2版本工具包(vue-test) 和 vue3版本工具包(@vue/test-utils),这里基于vue3组件进行测试
安装对应的依赖:
➜ npm install @vue/test-utils -D
对于Jestv28+以上版本还需要添加以下配置:
// jest.config.ts
export default {
testEnvironmentOptions: {
customExportConditions: ["node", "node-addons"],
},
}
2
3
4
5
6
创建一个简单的Button组件:
import { defineComponent } from "vue";
export default defineComponent({
render(){
return <button>按钮</button>
}
})
2
3
4
5
6
7
添加测试用例:
import { mount } from "@vue/test-utils";
import Button from "@/components/Button";
test("测试vue组件", () => {
const wrapper = mount({
setup() {
return () => {
return <Button />;
};
},
});
expect(wrapper.text()).toBe('按钮')
})
2
3
4
5
6
7
8
9
10
11
12
13
运行测试
➜ npm run test
PASS __tests__/demo.spec.tsx
✓ 测试vue组件 (9 ms)
------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
Button.tsx | 100 | 100 | 100 | 100 |
------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.633 s
2
3
4
5
6
7
8
9
10
11
12
13
14
添加全局组件,当单测某个组件时,组件内部引用的其它组件会因为没有引用而报错,定义全局组件可以解决这个问题
// jest.setup.ts
import { config } from "@vue/test-utils";
import Button from "@/button/src/button";
import Icon from "@/button/src/icon";
config.global.components = {
Button,
Icon,
};
2
3
4
5
6
7
8
9
配置jest
// jest.config.ts
export default {
setupFiles: ["<rootDir>/jest.setup.ts"],
}
2
3
4
这里不对vue工具包API过多的解释,更多的API使用可以查看官方文档,vue2版本的可以查看这里
端到端测试 - Cypress
这里选择Cypress作为端测框架,Cypress 是一个用于前端端到端(End-to-End,E2E)测试的开源测试框架。它被设计用于对 Web 应用程序进行自动化测试,可以模拟用户与应用程序进行交互的行为,并验证应用程序的功能和用户体验
安装使用
你可以选择自己的npm包管理器进行安装
➜ npm install cypress -D
配置npm脚本打开cypress
{
"scripts": {
"test:e2e": "cypress open"
}
}
2
3
4
5
或者直接使用./node_modules/.bin/cypress open
。启动后出弹出cypress的界面,可以进行端到端测试和组件测试。从下图界面可以看出不管是端测还是组件测试都没有配置(Not Configured),这里我们选择第一个端到端测试:
进去后由于是初次使用cypress会引导对项目做初始化配置,点击Continue
会在项目中生成对应的文件:
端到端测试cypress会检测本地计算机安装的浏览器,你可以选择要用哪个浏览器进行测试,或者使用electron。这里我选择使用electron
进来后就到了测试用例界面,初次下载不会有任何测试用例的。界面会引导你去创建第一个测试用例,左侧用来生成官方的测试例子,其可以作为用例的参考;右侧自己来创建测试用例,初次使用我们先点击生成官方例子:
下图便是官方的测试例子,可以看到有很多,对应的文件路径在cypress/e2e
下
这些单测例子中随便点击一个就会测试对应的用例,这里我们点击todo.cy.js
就可以开始测试了
可以看到测试界面主要包含两大板块,左侧主要是测试日志、统计、快照之类,右侧便是web界面。cypress会根据测试用例模拟用户行为,并生成对应的日志;web界面会显示每个步骤的形态,就好像是用户在操作一样
除了使用open打开对应的界面外,也可以使用终端进行测试不用打开界面,只需要执行cypress run
命令即可
配置
cypress配置包括全局、端测、组件测试几个方面,可以通过cypress.config.js
配置文件进行配置:
const { resolve } = require("path");
const { defineConfig } = require("cypress");
module.exports = defineConfig({
// 端测配置
e2e: {},
// 组件测试配置
component: {},
/* 其他的全局配置 */
});
2
3
4
5
6
7
8
9
10
以下是一些常见的配置:
- baseUrl:端测访问的基本地址,比如测试本地的web服务
http://localhost:8080
,这样在测试用例中就可以使用相对路径了 - specPattern:测试用例文件,包含单测和组件测试,支持glob形式。这样就可以自定义测试用例的位置了
- fixturesFolder:用来存放测试mock数据,默认读取根路径下的
cypress/fixtures
路径下的文件,你可以通过此属性修改对应的测试数据路径 - supportFile:自定义全局配置、功能函数、自定义命令、环境变量等等,在每个测试文件执行前都会先加载执行全局的配置文件。默认的路径为
cypress/support/**
,可以自行修改,如果没有全局的配置请设置成false
- experimentalRunAllSpecs:值为true时测试界面会有一个测试全部用例的按钮,默认会false
- video:当执行
cypress run
进行测试后生成对应的视频文件,设置成false不会生成 - screenshotsFolder:快照保存路径,默认路径
cypress/screenshots
- videosFolder:视频保存路径,默认路径
cypress/videos
- screenshotOnRunFailure:当
cypress run
失败时生成对应的快照 - downloadsFolder:当测试用例中下载文件时保存的路径
- viewportWidth / viewportHeight:设置界面视口大小,可以模拟不同尺寸的屏幕
有的配置对于 e2e和Component 都有,可以在全局配置,也可以在不同类型的测试中进行覆盖,基本配置就说这么多,更多配置参考👉 官方文档
大概的配置结构如下:
<root>
├── cypress.config.js # 配置文件
├── cypress
│ ├── fixtures # 测试数据
│ │ └── user.json
│ └── supports # 全局配置
│ └── index.js
2
3
4
5
6
7
基本端测
这里我们尝试使用端测来测试Vue3的组件,端测我们要提供页面真实的模拟用户的交互等等,所以需要一个web服务应用,内部使用我们待测试的vue组件,然后模拟用户行为
- 搭建web服务,假设这里使用vite搭建一个web应用
- 配置cypress的单测
baseUrl
,这样在用例中就可以使用相对地址了tsmodule.exports = defineConfig({ e2e: { baseUrl: "http://localhost:10020", // 配置web基本地址 video: false, # 不生成视频 screenshotOnRunFailure: false, # 不生成快照 supportFile: false, # 不设置全局配置 specPattern: "src/**/*.e2e.(tsx|ts)", # 设置端测用例的位置 }, });
1
2
3
4
5
6
7
8
9 - 新建button组件的测试用例:ts
// src/button/__tests__/button.e2e.tsx /// <reference types="cypress" /> describe("首页测试", () => { it("测试外观", () => { cy.visit("/button"); cy.get(".i-btn").contains("按钮").trigger("click"); }); });
1
2
3
4
5
6
7
8 - 启动测试:
npm run test:e2e
- 可以看到我们的界面已经有了刚刚新建的测试用例文件了
- 点击测试用例开始测试:
因为比较简单执行的比较快。左侧的测试日志鼠标移到对应的位置会显示对应的快照
以上便是一个最简单上手的端测用例,比较简单相应你已经也成功了。其他更多的测试用例api这里不做过多介绍,其实和jest差不多,你可以通过 官方文档 了解更多
代码覆盖率
cypress也支持代码覆盖率的报告,通过安装对应的插件就可以轻松搞定
➜ npm i @cypress/code-coverage babel-plugin-istanbul -D
添加babel配置
// .babelrc
{
"plugins": ["istanbul"]
}
2
3
4
添加全局功能配置:
// cypress/support/index.js
import '@cypress/code-coverage/support';
2
对cypress进行配置:
// cypress.config.ts
const { resolve } = require("path");
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
require('@cypress/code-coverage/task')(on, config);
on('file:preprocessor', require('@cypress/code-coverage/use-babelrc'));
return config;
},
supportFile: "cypress/supports/index.js",
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
更多请参考对应的👉 官方文档
钩子函数
在 Cypress 测试框架中,有一些钩子函数可以用于在测试执行的不同阶段执行额外的操作或设置。这些钩子函数是 Cypress 提供的全局函数,可以在测试文件中使用
- before: 函数在每个测试套件(describe 块)运行之前执行,并且只会执行一次。它可以用于设置测试套件级别的准备工作,例如初始化测试数据、登录用户等
- after: 函数在每个测试套件(describe 块)运行完毕之后执行,并且只会执行一次。它可以用于清理测试套件级别的资源,例如清除测试数据、退出登录等
- beforeEach: 函数在每个测试用例(it 块)运行之前执行。它通常用于设置每个测试用例的前置条件,例如重置状态、模拟请求等
- afterEach: 函数在每个测试用例(it 块)运行完毕之后执行。它通常用于清理每个测试用例的后续操作,例如清除临时数据、还原状态等
常见问题
项目中可能会使用Jest进行单元测试、cypress进行端到端测试,由于jest和cypress有着形同的api,如:describe、it等等,会出现类型冲突的错误。这里我是在tsconfig
中屏蔽掉cypress测试文件就可以了
{
"compilerOptions": {},
"exclude": ["src/**/*.e2e.tsx"] // 屏蔽掉cypress测试用例
}
2
3
4
测试前端组件
这里我们先定义一个LazyComponent
组件,主要就是来懒加载目标组件,根据网络情况呈现最终的显示结果:
// 参考代码
export const LazyComponent = function (opts: ILazyComponent) {
let delay = 300; // 默认300ms
let LoadingComponent = LazyLoading;
let errorComponent = LazyError;
let loader: () => Promise<any>;
let requiredCache = true;
if (isFunction(opts)) {
loader = opts;
} else {
loader = opts.loader;
LoadingComponent = opts.loadingComponent || LoadingComponent;
errorComponent = opts.errorComponent || errorComponent;
delay = opts.delay ?? delay;
requiredCache = opts.cache ?? requiredCache;
}
const isSpin = isFunction(opts) ? true : opts?.isSpin ?? true;
const minSkeleton = isFunction(opts) ? 2 : opts?.minSkeleton ?? 2;
const withCard = isFunction(opts) ? true : opts?.withCard ?? true;
return defineComponent({
render() {
const Component: any = defineAsyncComponent({
loader: () =>
new Promise(async (resolve, reject) => {
try {
const cacheKey = loader?.toString();
const isCached = lazyComponentsCache.has(cacheKey);
if (isCached) return resolve(lazyComponentsCache.get(cacheKey));
await sleep(delay);
const res = await loader();
requiredCache && lazyComponentsCache.set(cacheKey, res);
if (lazyComponentsCache.size > MAX_CACHE_SIZE) {
lazyComponentsCache.delete(lazyComponentsCache.keys().next().value);
}
resolve(res);
} catch (err) {
console.error("【LazyComponent Fail】", err);
reject(err);
}
}),
loadingComponent: () => (
<LoadingComponent
minSkeleton={minSkeleton}
withCard={withCard}
isSpin={isSpin}
/>
),
errorComponent,
delay: 0,
});
return <Component />;
},
});
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
然后我们创建一个组件测试用例文件lazy-component.cy.tsx
,然后根据不同的条件去渲染预期的页面效果
import { CustomLoading } from "~/client/components/common/loading";
import { LazyComponent } from "~/client/layout/page/lazy-component";
import AnalysisPage from "~/client/views/dashboard/analysis";
describe("测试组件 LazyComponent", () => {
it("LazyComponent 默认卡片加载", () => {
cy.mount(
LazyComponent({
loader: () => Promise.resolve(<span>我是最终组件1</span>),
withCard: true,
isSpin: false,
minSkeleton: 5,
delay: 3000,
})
);
cy.wait(4000);
});
it("LazyComponent 默认Spin加载", () => {
cy.mount(
LazyComponent({
loader: () => Promise.resolve(<span>我是最终组件2</span>),
withCard: false,
isSpin: true,
delay: 3000,
})
);
cy.wait(5000);
});
it("LazyComponent 测试缓存(当前组件不会loading加载,直接立即显示)", () => {
cy.mount(
LazyComponent({
loader: () => Promise.resolve(<span>我是最终组件2</span>),
withCard: true,
delay: 3000,
})
);
cy.wait(5000);
});
it("LazyComponent成功", () => {
cy.mount(
LazyComponent({
loader: () => Promise.resolve(<AnalysisPage />),
loadingComponent: () => (
<CustomLoading>
<strong>Loading...</strong>
</CustomLoading>
),
delay: 3000,
})
);
cy.wait(5000);
});
it("LazyComponent失败", () => {
cy.mount(
LazyComponent({
loader: () => Promise.resolve(<h3>出错了...</h3>),
loadingComponent: () => <div style={{ color: "red", "font-size": "24px", textAlign: "center" }}>请稍后...</div>,
delay: 2000,
})
);
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
测试流程概览: