前端掌握单元测试-jest
一、前言
本文基于开源项目:
https://github.com/facebook/jest
https://www.jestjs.cn/
对于单元测试,可能小伙伴们的第一反应都是“难”,能不写一般就不去写了。广东靓仔也觉得写单元测试是个有挑战性,且有难度的任务,但广东靓仔觉得大家可以尽量去尝试写一写单元测试,在bug减少的同时,项目的质量也有很大的提升,对个人而言一定能提升我们自己的能力。
本文我们一起来看看Jest,Jest现在已经更新到了28~
二、what Jest
Jest 是一个令人愉快的 JavaScript 测试框架,专注于"简洁明快"。
这些项目都在使用 Jest:Babel、 TypeScript、 Node、 React、 Angular、 Vue 等等!
特点:
??????? 零配置:Jest 的目标是在大部分 JavaScript 项目上实现开箱即用, 无需配置。
??快照测试:能够轻松追踪大型对象的测试。快照可以与测试代码放在一起,也可以集成进代码行内。
???? 隔离:测试程序拥有自己独立的进程 以最大限度地提高性能。
??????? 优秀的api:从 it 到 expect - Jest 将整个工具包放在同一个 地方。好书写、好维护、非常方便。
三、入门
安装 Jest:npm / yarn
npm install --save-dev jest
# or
yarn add --dev jes
一般在选中哪个版本的时候,广东靓仔建议使用稳定的版本即可,不一定要最新。
(@27版本)初始化【@28可以省略这一步】
npx jest --init
执行完后能看到如下文件(翻译了一下):
export default {
// 测试中所有导入的模块都应该自动模拟
// automock: false,
// `n` 次失败后停止运行测试
// bail: 0,
// Jest 应该存储其缓存的依赖信息的目录
// 每次测试前自动清除模拟调用、实例、上下文和结果
// 开启覆盖率
clearMocks: true,
// 指示是否应在执行测试时收集覆盖率信息
collectCoverage: true,
// 一组 glob 模式,指示应为其收集覆盖信息的一组文件
// collectCoverageFrom: undefined,
// Jest 应该输出其覆盖文件的目录
coverageDirectory: "coverage",
// 用于跳过覆盖收集的正则表达式模式字符串数组
// coveragePathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// 指示应使用哪个提供程序来检测代码以进行覆盖
coverageProvider: "v8",
};
Demo
Tips: 一般单元测试建议写在utils文件夹下。
目录如下:
├── jest.config.js
├── package-lock.json
├── package.json
├── src
│ └── utils
│ └── sum.js
└── liangzai-tests
└── utils
└── sum.test.js
/utils/sum.js
// sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
/liangzai-utils/sum.test.js
const sum = require('../../utils/sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
然后执行
npm test
可以看到结果:
四、转译ts
Jest 本身不做代码转译工作:
安装ts
npm i -D typescript@4.6.3
初始化 TypeScript 的配置
npx tsc --init
执行后会看到tsconfig.json 文件:
{
"compilerOptions": {
"types": ["node", "jest"],
"target": "es2016",
/* 为发出的 JavaScript 设置 JavaScript 语言版本并包含兼容的库声明 */
"module": "commonjs",
/* 指定生成什么模块代码. */
"esModuleInterop": true,
/* 发出额外的 JavaScript 以简化对导入 CommonJS 模块的支持。这将启用 `allowSyntheticDefaultImports` 以实现类型兼容性. */
"forceConsistentCasingInFileNames": true,
/* 确保imports中的大小写正确 . */
"strict": true,
/* 启用所有严格的类型检查选项。 */
"skipLibCheck": true
/* 跳过类型检查所有 .d.ts 文件. */
}
}
修改.js为.ts,代码增加类型
const sum = (a: number, b: number) => {
return a + b;
}
export default sum;
安装Jest 类型声明包
npm i -D @types/jest@28.1.2
最后执行 npm run test,测试通过。
小优化
路径使用简写,修改 tsconfig.json 配置:
{
"compilerOptions": {
"paths": {
"@/*": ["src/*"]
}
}
}
jest.config.js修改moduleNameMapper
modulex.exports = {
"moduleNameMapper": {
"@/(.*)": "<rootDir>/src/$1"
}
}
五、其他知识点
setupFilesAfterEnv 和 setupFiles
简单来说:
setupFiles 是在 引入测试环境(比如下面的 jsdom)之后 执行的代码
setupFilesAfterEnv 可以指定一个文件,在每执行一个测试文件前都会跑一遍里面的代码。
具体应用场景是:在 setupFiles 可以添加 测试环境 的补充,比如 Mock 全局变量 abcd 等。而在 setupFilesAfterEnv 可以引入和配置 Jest/Jasmine(Jest 内部使用了 Jasmine) 插件。
jsdom 测试环境
jest 提供了 testEnvironment 配置:
module.exports = {
testEnvironment: "jsdom",
}
jsdom: 这个库用 JS 实现了一套 Node.js 环境下的 Web 标准 API。
添加 jsdom 测试环境后,全局会自动拥有完整的浏览器标准 API,不需要Mock了。
引入react/vue
step1: 安装Webpack 依赖
step2: 安装相应的Loader
step3: 安装React/vue 以及业务
这里列举下webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
index: './src/index.tsx'
},
module: {
rules: [
// 解析 TypeScript
{
test: /\.(tsx?|jsx?)$/,
use: 'ts-loader',
exclude: /(node_modules|tests)/
},
// 解析 CSS
{
test: /\.css$/i,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
]
},
// 解析 Less
{
test: /\.less$/i,
use: [
{ loader: "style-loader" },
{
loader: "css-loader",
options: {
modules: {
mode: (resourcePath) => {
if (/pure.css$/i.test(resourcePath)) {
return "pure";
}
if (/global.css$/i.test(resourcePath)) {
return "global";
}
return "local";
},
}
}
},
{ loader: "less-loader" },
],
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.less', 'css'],
// 设置别名
alias: {
utils: path.join(__dirname, 'src/utils/'),
components: path.join(__dirname, 'src/components/'),
apis: path.join(__dirname, 'src/apis/'),
hooks: path.join(__dirname, 'src/hooks/'),
store: path.join(__dirname, 'src/store/'),
}
},
devtool: 'inline-source-map',
// 3000 端口打开网页
devServer: {
static: './dist',
port: 3000,
hot: true,
},
// 默认输出
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
// 指定模板 html
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
package.json 添加启动命令
{
"scripts": {
"start": "webpack serve",
"test": "jest"
}
}
配置 tsconfig.json
{
"compilerOptions": {
"jsx": "react",
"esModuleInterop": true,
"baseUrl": "./",
"paths": {
"utils/*": ["src/utils/*"],
"components/*": ["src/components/*"],
"apis/*": ["src/apis/*"],
"hooks/*": ["src/hooks/*"],
"store/*": ["src/store/*"]
}
}
}
六、组件测试
Demo: 这里列举了一个简单的场景
user.ts: 获取用户角色身份
import axios from "axios";
// 类型:用户角色身份
export type UserRoleType = "user" | "admin";
// 接口:返回
export interface GetRoleRes {
userType: UserRoleType;
}
// 函数:获取用户角色身份
export const getUserRole = async () => {
return axios.get<GetRoleRes>("https://xxx.xx.com/api/role");
};
业务组件/Auth/Button/index.tsx(缩略代码)
import React, { FC, useEffect, useState } from "react";
...
// 身份文案 Mapper
const mapper: Record<UserRoleType, string> = {
user: "用户",
admin: "管理员",
};
const Button: FC<Props> = (props) => {
const { children, className, ...restProps } = props;
const [userType, setUserType] = useState<UserRoleType>();
// 获取用户身份,并设值
const getLoginState = async () => {
const res = await getUserRole();
setUserType(res.data.userType);
};
useEffect(() => {
getLoginState().catch((e) => message.error(e.message));
}, []);
return (
<Button {...restProps}>
{mapper[userType!] || ""}
{children}
</Button>
);
};
export default Button;
测试用例button.test.tsx
import { render, screen } from "@testing-library/react";
import Button from "components/Button";
import React from "react";
describe('Button', () => {
it('可以正常展示', () => {
render(<Button>登录</Button>)
expect(screen.getByText('登录')).toBeDefined();
});
})
上面这代码只是一个简单的Demo测试
测试组件功能
mockAxios.test.tsx
import React from "react";
import axios from "axios";
import { render, screen } from "@testing-library/react";
import Button from "components/Button";
describe("Button Mock Axios", () => {
it("可以正确展示用户按钮内容", async () => {
jest.spyOn(axios, "get").mockResolvedValueOnce({
// 其它的实现...
data: { userType: "user" },
});
render(<Button>你好</Button>);
expect(await screen.findByText("用户你好")).toBeInTheDocument();
});
it("可以正确展示管理员按钮内容", async () => {
jest.spyOn(axios, "get").mockResolvedValueOnce({
// 其它的实现...
data: { userType: "admin" },
});
render(<Button>你好</Button>);
expect(await screen.findByText("管理员你好")).toBeInTheDocument();
});
});
当然,我们也可以不mock,而是使用 Http Mock 工具:msw
Mock Http
代码如下:
/mockServer/handlers.ts
import { rest } from "msw";
const handlers = [
rest.get("https://xxx.xx.com/api/role", async (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
userType: "user",
})
);
}),
];
export default handlers;
/mockServer/server.ts
import { setupServer } from "msw/node";
import handlers from "./handlers";
const server = setupServer(...handlers);
export default server;
/jest-setup.ts
import server from "./mockServer/server";
beforeAll(() => {
server.listen();
});
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
server.close();
});
最后测试用例代码:
// 偏向真实用例
import server from "../../mockServer/server";
import { rest } from "msw";
import { render, screen } from "@testing-library/react";
import Button from "components/Button";
import React from "react";
import { UserRoleType } from "apis/user";
// 初始化函数
const setup = (userType: UserRoleType) => {
server.use(
rest.get("https://xxx.xx.com/api/role", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json({ userType }));
})
);
};
describe("Button Mock Http 请求", () => {
it("可以正确展示普通用户按钮内容", async () => {
setup("user");
render(<Button>广东</Button>);
expect(await screen.findByText("用户你好")).toBeInTheDocument();
});
it("可以正确展示管理员按钮内容", async () => {
setup("admin");
render(<Button>靓仔</Button>);
expect(await screen.findByText("管理员你好")).toBeInTheDocument();
});
});
setup 函数,在每个用例前初始化 Http 请求的 Mock 返回。
七、小结
Jest的功能远不止于此,还能做性能测试、自动化测试等等
在我们阅读完官方文档后,我们一定会进行更深层次的学习,比如看下框架底层是如何运行的,以及源码的阅读。
这里广东靓仔给下一些小建议:
在看源码前,我们先去官方文档复习下框架设计理念、源码分层设计
阅读下框架官方开发人员写的相关文章
借助框架的调用栈来进行源码的阅读,通过这个执行流程,我们就完整的对源码进行了一个初步的了解
接下来再对源码执行过程中涉及的所有函数逻辑梳理一遍
作者:广东靓仔
欢迎关注:前端早茶