下载
- 运行环境 node
npm install jest -D
- 将 package.json 中的命令改为
"test" : "jest"
支持的测试文件格式(命名规范)
- 两种方式
- 他会寻找这两种方法提供文件进行测试
- 两种方法二选一即可
测试文件格式 1
- 支持两种后缀(实际上使用一种即可,只是提供了两种)
测试文件格式 2
方法
it
||test
- 断言,用于测试,it 和 test 一样
- 两个参数
describe
expect
- 在上述文件中引入要测试的模块
const add = (a, b) => a + b;
module.exports = add;
const add = require("../index");
describe("add 方法测试-index", () => {
test("1+1应该等于2", () => {
const res = add(1, 1);
expect(res).toBe(2);
});
});
匹配器
有参数
- toBe
- 使用的是 Object.is 来判断的,所以
需要绝对相等
- toEqual
- 常用来检查引用数据类型是否相等,
不比对空间地址
,使用了递归,
- 他也可以用来检查基本数据数据类型
- 如果是函数的话,引用空间地址不相同也会导致比对失败
数字
- 不会进行数据类型转换
- toBeGreaterThanOrEqual
- toBeGreaterThan
- toBeLessThan
- toBeLessThanOrEqual
字符串
- toMatch
- 可以传递
正则
和字符串
- 传递字符串的话,是包含就可以,等同于 includes,但是
区别在于 toMatch 不进行数据类型转换
数组
报错
- toThrow
- 参数可选填
- 不填,就是比对是否有报错信息
- 可以填写类型,如 Error 或 TypeError,这样是检查报错类型
- 可填写字符串或正则,检查报错内容
无参数
- toBeNull
- toBeUndefined
- toBeDefined
- 不是 undefined,等同于
not.toBeUndefined
- toBeTruthy
- 与 if 判断相同,可以理解为
if(true) === toBeTruthy()
- toBeFalsy
- 与 if 判断相反,可以理解为
if(false) === toBeFalsy()
修饰符
异步
- 异步方法的测试(没有返回值)
expect.assertions(1)
表示异步任务执行次数(确保异步方法被执行)
expect(true).toBoe(true)
表示执行了(因为 true === true
所以执行到这一行,就代表执行了)
注意:
在在 test 函数的 callback 处,使用 done
变量接受参数,在异步任务最后执行 done,表示我们应该在这里结束异步任务
啦
const delay = (callback) => {
setTimeout(() => {
callback && callback();
}, 1000);
};
module.exports = delay;
const delay = require("../index");
test("callback 被执行", (done) => {
expect.assertions(1);
const callback = () => {
console.log("我执行");
expect(true).toBe(true);
done();
};
delay(callback);
});
- promise 的测试
- 在这里如果
不使用 done 的话,不要接收
(否则会报错)
- 最后需要将 promise 的执行给
return 出去(async的不用)
- 也可以使用 async 和 await 来测试
- 也可以使用 jest 自带的
resolves
和rejects
方法来处理 promise,用法在下面
const delayPromise = (callback) => {
return new Promise((resolve, reject) => {
try {
setTimeout(() => {
const res = callback && callback();
resolve(res);
}, 1000);
} catch (e) {
reject(e);
}
});
};
test("promise 被执行", () => {
expect.assertions(1);
const callback = () => 1;
return delayPromise(callback).then((res) => {
expect(res).toBe(1);
});
});
test("promise使用jest自带的resolves方法", () => {
expect.assertions(1);
const callback = () => 1;
return expect(delayPromise(callback)).resolves.toBe(1);
});
test("promise async 被执行", async () => {
expect.assertions(1);
const callback = () => 1;
const res = await delayPromise(callback);
expect(res).toBe(1);
});
mock
创建一个 mock 函数
jest.fn()
- 用来创建一个 mock 函数
- 参数:选填(function)
- 不传递也会返回一个 mock 实例,可以利用实例上的一些方法来做一些事情
- 传递参数,mock 的就是这个函数,也可以调用实例方法
mock 函数的 mock 属性
- 所有的 mock 函数都有一个
.mock
属性(object 类型),通过这个属性可以查看这个函数是怎么调用等等,达到监控效果
calls
拿到函数的信息
results
拿到第指定索引(以 0 开始)执行的结果
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
const mockCallback = jest.fn((x) => 42 + x);
forEach([0, 1], mockCallback);
it("测试mock", () => {
expect(mockCallback.mock.calls.length).toBe(2);
expect(mockCallback.mock.calls[0][0]).toBe(0);
expect(mockCallback.mock.calls[1][0]).toBe(1);
expect(mockCallback.mock.results[0].value).toBe(42);
console.log(mockCallback.mock.instances);
});
it("测试mock的this指向集合", () => {
const myMock = jest.fn();
const a = new myMock();
const b = { b: 1 };
const bound = myMock.bind(b);
bound();
console.log(myMock.mock.instances);
});
mockReturnValue()
模拟函数返回值
- 只需要在 mockReturnValue 的括号内放入指定值即可
- mockReturnValue 会影响到下一个测试,需要使用
mockRestore
清除副作用
const callback = () => 1;
const callback = jest.fn().mockReturnValue(1);
spyOn
- 监听某个方法或值
- 两个参数
- 可以配合 mockReturnValue 来进行劫持
mockRestore
const getRandom = () => {
return Math.floor(Math.random() * 10);
};
test("随机数测试,小于10", () => {
expect(getRandom()).toBeLessThan(10);
});
test("随机数测试,大于等于0", () => {
expect(getRandom()).toBeGreaterThanOrEqual(0);
});
test("Math.random返回1,结果为10", () => {
const mockRandom = jest.spyOn(Math, "random");
mockRandom.mockReturnValue(1);
expect(getRandom()).toBe(10);
mockRandom.mockRestore();
});
test("测试上一步是否影响我,小于10", () => {
expect(getRandom()).toBeLessThan(10);
});
mockReturnValueOnce()
同上,区别在于只模拟一次(可以连续调用)
const myMock = jest.fn();
it("模拟返回值", () => {
myMock.mockReturnValueOnce(10);
console.log(myMock(), myMock());
});
it("试下是不是影响后面的one", () => {
console.log(myMock());
});
it("可以连续调用", () => {
myMock.mockReturnValueOnce(10).mockReturnValueOnce("x").mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
});
mockImplementationOnce
同上,只不过是用来模拟函数,也是一次
mockName
getMockName
const myMockFn = jest
.fn()
.mockReturnValue("default")
.mockImplementation((scalar) => 42 + scalar)
.mockName("add42");
myMockFn.getMockName();
it("语法糖", () => {
myMockFn(1);
myMockFn();
expect(myMockFn.mock.calls.length).toBeGreaterThan(1);
expect(myMockFn.mock.calls).toContainEqual([1]);
expect(myMockFn.mock.calls[myMockFn.mock.calls.length - 1]).toEqual([]);
expect(myMockFn.mock.calls[0][0]).toBe(1);
expect(myMockFn.getMockName()).toBe("add42");
});
模拟模块
import axios from "axios";
class Users {
static all() {
return axios.get("/users.json").then((resp) => resp.data);
}
}
export default Users;
import axios from "axios";
import Users from "../src/users";
jest.mock("axios");
test("should fetch users", () => {
const users = [{ name: "Bob" }];
const resp = { data: users };
axios.get.mockResolvedValue(resp);
return Users.all().then((data) => expect(data).toEqual(users));
});
jest.mock 也可以传递回调
- 导入模块的时候,希望对整个模块进行一定操作,就可以传入回调
jest.mock
的回调会在上面的 console.log 前执行
,且回调的 return 值会覆盖 import 接收的值
,不返回则为 undefined
- 在回调中使用的时候需要使用
jest.requireActual
方法来导入
export const mockObj = {
a: 1,
b: () => {
return "b";
},
};
export default () => "default";
import defaultFn from "../src/mockObj";
console.log(defaultFn, 2);
jest.mock("../src/mockObj", () => {
const old = jest.requireActual("../src/mockObj");
console.log(old, "1");
return {
a: 9090,
};
});
批量处理
beforeEach
/beforeAll
- beforeEach 在测试前执行,每个测试实例执行的时候都执行一次
- beforeAll 在测试前执行,但是只执行一次,作用到正规作用域
- 参数,callbak
afterEach
/afterAll
- 在测试后执行,其他同上
- 参数,callbak
- 使用场景
test("Math.random返回1,结果为10", () => {
const mockRandom = jest.spyOn(Math, "random");
mockRandom.mockReturnValue(1);
expect(getRandom()).toBe(10);
mockRandom.mockRestore();
});
test("Math.random返回0.1,结果为1", () => {
const mockRandom = jest.spyOn(Math, "random");
mockRandom.mockReturnValue(0.1);
expect(getRandom()).toBe(1);
mockRandom.mockRestore();
});
beforeEach(() => {
mockRandom = jest.spyOn(Math, "random");
});
afterEach(() => {
mockRandom.mockRestore();
});
test("Math.random返回1,结果为10", () => {
mockRandom.mockReturnValue(1);
expect(getRandom()).toBe(10);
});
test("Math.random返回0.1,结果为1", () => {
mockRandom.mockReturnValue(0.1);
expect(getRandom()).toBe(1);
});
快照(测试 UI 组件)
- 安装依赖
npm install @babel/core @babel/preset-env @babel/preset-react react-test-renderer -D
npm install react -S
- 创建
.babelrc
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
import React from "react";
const BaiduLink = () => {
const url = "https://www.baidu.com";
const text = "百度一下";
return <a href={url}>{text}</a>;
};
module.exports = BaiduLink;
- 创建测试文件
- 我们在这里使用了
react-test-renderer
这个库来帮助我们生成映射文件
- 使用
create
生成了快照
外联快照
- 使用
toMatchSnapshot
进行新老映射比较
- 外联会生成
__snapshots__
文件夹,里面是生成的映射文件(后缀是.snap),下面有写
import Link from "../BaiduLink";
import renderer from "react-test-renderer";
import React from "react";
it("我是外联快照", () => {
const tree = renderer.create(
<Link page="https://www.baidu.com/">百度一下</Link>
);
expect(tree).toMatchSnapshot();
});
- 开始测试,
npm test
- 第一次测试会在
__teste__
目录下生成__snapshots__
文件夹,里面是生成的映射文件(后缀是.snap)
- 假设我们生成这个映射文件以后,再次修改了
./BaiduLink.js
,再次执行npm test
的时候就会抛出异常
- 因为他会把本次的映射跟以前生成的映射进行对比(所以,如果生成以后,git add 的时候记得把生成的映射文件也提交了)
- 如果想要覆盖原有的映射,在 package.json 的命令添加
jest --updateSnapshot
然后执行即可(就会生成一个新的映射,然后覆盖以前的)
内联快照
toMatchInlineSnapshot
- 第一次使用的时候,这个方法不传递值
- 当我们执行
npm test
后,会在这个方法的括号内生成映射结构
- 他没有在外部生成文件,而是传递给本身了,这是他与外联的区别
it("我是内联快照", () => {
const tree = renderer.create(
<Link page="https://www.baidu.com/">百度一下</Link>
);
expect(tree).toMatchInlineSnapshot();
});
对一些不稳定的数据建立快照
- 假设存储了一个数据,是
new Date
,那它每次都不相同,所以每次都会报错
- 这时候就可以在 toMatchSnapshot 函数内进行处理,断言为 any 类,这样他就会将每次更新的快照保存,且不报错
it("测试对象快照", () => {
const user = {
createdAt: new Date(),
id: Math.floor(Math.random() * 20),
name: "LeBron James",
};
expect(user).toMatchSnapshot({
createdAt: expect.any(Date),
id: expect.any(Number),
});
});
开启测试覆盖率
- 在 package.json 的测试命令后加上
--coverage
- 在执行以后,会显示覆盖率表格,且会
自动生成coverage文件夹
,里面有详细的信息
- 我们可以打开
coverage/lcov-report/index.html
文件查看详情,点击 file
对对应的文件,可以看到具体代码
- 如果为红色背景就是没有覆盖到
- 代码行前面的数字代表的是本次测试这行代码执行了几次
react 测试
@testing-library/react
- 安装
npm install @testing-library/react @testing-library/jest-dom -D
- 使用前还需要
将jest的环境设置为jsdom
- 根目录下新建 jest.config.js,配置文件官方链接
https://jestjs.io/docs/configuration
- 内容
module.exports = {
setupFilesAfterEnv: ["@testing-library/jest-dom"],
testEnvironment: "jsdom",
};
import React from "react";
import { render } from "@testing-library/react";
test("component", () => {
const { getByLabelText } = render(<button aria-label="Button" />);
expect(getByLabelText("Button")).toBeEmptyDOMElement();
});
react-dom 自带了一个测试库react-dom/test-utils
- 可以通过解构拿到具体的方法
act
- 一个参数,callback
- 对 ui 组件进行渲染,不仅仅限于第一次渲染,也可进行状态更新操作渲染
- 在断言前使用,保证我们的组件已经渲染完成
it("使用act", () => {
act(() => {
ReactDOM.render(<组件 />, dom节点);
});
});
Simulate
- 用来测试事件,他要比原生的
MouseEvent
好用很多
- 用法:
Simulate.事件(node节点,{参数})
第二个参数为选填
import React, { useState } from "react";
const Input = () => {
const [val, setVal] = useState("12");
return (
<input
type="text"
className="input"
value={val}
onChange={(e) => {
setVal(e.target.value);
// setVal('100');
}}
/>
);
};
export default Input;
import React from "react";
import InputChange from "../src/component/InputChange";
import { act, Simulate } from "react-dom/test-utils";
import { render, unmountComponentAtNode } from "react-dom";
let container = null;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});
it("change事件", () => {
act(() => {
render(<InputChange />, container);
});
let input = container.querySelector(".input");
expect(input.value).toBe("12");
act(() => {
Simulate.change(input, { target: { value: "123" } });
});
console.log(input.value);
});
MouseEvent
- 原生模拟事件,不推荐使用
- 作用:测试 dom 事件
- 使用方法
dom.dispatchEvent(new MouseEvent("事件", { bubbles: true }));
注意
的是事件派发的时候需要{bubbles:true},这样 react 才可以监听到
- 在测试的时候
innerText
拿不到值,需要使用 textContent
import React, { useState } from "react";
const BtnClick = () => {
const [num, setNum] = useState(0);
return (
<>
<div className="num">当前数字{num}</div>
<div
className="Btn"
onClick={() => {
setNum((old) => {
return old + 1;
});
}}
>
点击+1
</div>
</>
);
};
export default BtnClick;
import React from "react";
import BtnClick from "../src/component/BtnClick";
import { render, unmountComponentAtNode } from "react-dom";
import { act, Simulate } from "react-dom/test-utils";
let container = null;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
});
it("点击事件", () => {
act(() => {
render(<BtnClick />, container);
});
let num = container.querySelector(".num");
console.log(num.textContent);
act(() => {
container.querySelector(".Btn").dispatchEvent(
new MouseEvent("click", {
bubbles: true,
})
);
});
console.log(num.textContent);
});
jest 结合 @testing-library/react 进行实战
fireEvent 属性
fireEvent.click(screen.queryByText("文字"), {
target: {
value: 1,
},
});
screen
render
- 渲染
- 渲染后,可以使用 screen 上的属性来进行 dom 操作等,也可以使用解构,两者相同
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import Click from "../src/pages/test/click";
test("测试一下不解构", () => {
render(<Click />);
screen.getByText(0);
fireEvent.click(screen.getByText("+"));
screen.getByText("1");
});
test("测试一下解构", () => {
const { getByText } = render(<Click />);
getByText(0);
fireEvent.click(getByText("+"));
getByText("1");
});
it("测试快照", () => {
const { asFragment } = render(<Click />);
expect(asFragment()).toMatchSnapshot();
});
测试 setTimeout
- 因为涉及到了异步,所以直接获取是拿不到值的
- 使用 waitFor 配合 async/await,在这里会等待不到 1 秒的时间,如果异步时间太长也会导致失败
import React, { useState } from "react";
export default () => {
const [count, setCount] = useState(0);
const sync = () => {
setTimeout(() => {
setCount(count + 1);
}, 0);
};
return (
<>
<button onClick={sync}>异步+</button>
<div data-testid="data">+{count}</div>
</>
);
};
import { render, fireEvent, cleanup, waitFor } from "@testing-library/react";
import Click from "./click";
it("测试定时器异步", async () => {
const { getByTestId, getByText } = render(<Click />);
fireEvent.click(getByText("异步+"));
const counter = await waitFor(() => getByText("+1"));
expect(counter).toHaveTextContent("+1");
});
测试请求,二次封装的 axios
import React, { useState } from "react";
import api from "../../common/api";
export default () => {
const [count, setCount] = useState(0);
const get = () => {
api.get({}).then((res: any) => {
setCount(res["count"]);
});
};
return (
<>
<div data-testid="data">+{count}</div>
<button onClick={get}>发请求</button>
</>
);
};
import React from "react";
import {
render,
screen,
fireEvent,
cleanup,
waitFor,
} from "@testing-library/react";
import Click from "../src/pages/test/click";
jest.mock("../src/common/api", () => {
return {
get: () => Promise.resolve({ count: 10 }),
};
});
it("测试axios请求", async () => {
const { getByText } = render(<Click />);
fireEvent.click(getByText("发请求"));
const counter = await waitFor(() => getByText("+10"));
expect(counter).toHaveTextContent("+10");
});