使用的库package.json
"scripts": {
"test": "vitest --ui --coverage",
}
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5", --测试工具包
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@vitest/coverage-v8": "latest", --覆盖率工具
"@vitest/ui": "latest", --测试结果页面展示
"jsdom": "^22.1.0",
"msw": "^1.2.1", --mock后端API工具
"vitest": "latest" --核心测试框架
}
vite.config.ts配置
/// <reference types="vitest"/>
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from "path"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve:{
alias:{
"@":path.resolve(__dirname,'./src')
}
},
test:{
globals:true,
environment:"jsdom",
setupFiles:['./src/__test__/setup.ts'],
css:false,
coverage:{
provider:"v8",
reporter:['html']
}
}
})
setup.ts
import { rest } from "msw";
import { setupServer } from "msw/node";
import {beforeAll,afterEach,afterAll} from 'vitest'
import "@testing-library/jest-dom"
import { cleanup } from "@testing-library/react";
export const server = setupServer(
//默认mock的api返回值
rest.get("http://127.0.0.1:8000/api/v1/records/pages", (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
{
id: 1,
category: "1",
amount: 1,
registrationDate: "2023-06-06",
type: "1",
},
{
id: 2,
category: "2",
amount: 1,
registrationDate: "2023-05-06",
type: "2",
},
])
);
}),
rest.get("http://127.0.0.1:8000/api/v1/records/pages/:date", (req, res, ctx) => {
const date = req.params.date
if(date === '2023-05'){
return res(
ctx.status(200),
ctx.json([
{
id: 2,
category: "1",
amount: 1,
registrationDate: "2023-05-06",
type: "1",
},
])
);
}else{
return res(ctx.status(500));
}
}),
rest.post("http://127.0.0.1:8000/api/v1/records/:id", (_req, res, ctx) => {
return res.once(
ctx.status(200),
);
}),
rest.post("http://127.0.0.1:8000/api/v1/records", (_req, res, ctx) => {
return res.once(
ctx.status(200),
);
}),
);
beforeAll(() => server.listen({onUnhandledRequest:'error'}));
afterAll(() => server.close());
beforeEach(() => {
cleanup();
});
afterEach(() => {
server.resetHandlers();
cleanup();
});
单元测试文件Test.test.tsx
import {
render,
screen,
fireEvent,
act,
} from "@testing-library/react";
import Home from "@/views/Home";
import { rest } from "msw";
import { server } from "./setup";
import { expect } from "vitest";
import App from "@/App";
import { BrowserRouter } from "react-router-dom";
it("test save button disable", async () => {
render(<Home />);
const button = screen.getByText("保存");
await act(() => { //wait request be processed,因react是异步渲染,需要等待渲染完成才可继续
fireEvent.click(button);
});
expect(button).toHaveAttribute("disabled");
});
it("test category change", async () => {
//这里测选项,因选择框超出组件本身范围,需要拿到App层级的对象来查找
const {container, getByRole} = render(<App />, {wrapper: BrowserRouter});
//default is income category
const button = container.querySelector("#select-item") as HTMLElement;
fireEvent.mouseDown(button);
let list =await getByRole("listbox");
expect(list.children).toHaveLength(8);
act(() => {
const options=list.querySelectorAll("li")[0] as HTMLElement;
options.click();
});
expect(button).toHaveTextContent("1");
//change to outcome
const outButton = screen.getByText("支出");
fireEvent.click(outButton);
fireEvent.mouseDown(button);
list =await getByRole("listbox");
expect(list.children).toHaveLength(12);
act(() => {
const options=list.querySelectorAll("li")[0] as HTMLElement;
options.click();
});
expect(button).toHaveTextContent("食費");
});
it('test amount input', () => {
const container = render(<Home />).container;
const amount = container.querySelector("#amount-textField") as HTMLElement;
fireEvent.change(amount, { target: { value: "" } });
//1以上数値。
expect(container).toHaveTextContent("1以上数値。");
fireEvent.change(amount, { target: { value: "11111111111" } });
//10位以下数値。
expect(container).toHaveTextContent("10位以下数値。");
});
it('test save success', async () => {
const {container, getByRole} = render(<App />, {wrapper: BrowserRouter});
//set category
const button = container.querySelector("#select-item") as HTMLElement;
fireEvent.mouseDown(button);
const list =await getByRole("listbox");
expect(list.children).toHaveLength(8);
act(() => {
const options=list.querySelectorAll("li")[0] as HTMLElement;
options.click();
});
expect(button).toHaveTextContent("給与");
//set amount
const amount = container.querySelector("#amount-textField") as HTMLElement;
fireEvent.change(amount, { target: { value: "1" } });
//set date
const dateInput = container.querySelector("#dayYear-textField") as HTMLElement;
fireEvent.change(dateInput, { target: { value: "2023-06-14" } });
//save button
const saveButton = screen.getByText("保存");
await act(() => { //wait request be processed
fireEvent.click(saveButton);
});
//保存成功。
expect(container).toHaveTextContent("保存成功。");
});
it('test save failed', async () => {
const {container, getByRole} = render(<App />, {wrapper: BrowserRouter});
//set category
const button = container.querySelector("#select-item") as HTMLElement;
fireEvent.mouseDown(button);
const list =await getByRole("listbox");
expect(list.children).toHaveLength(8);
act(() => {
const options=list.querySelectorAll("li")[0] as HTMLElement;
options.click();
});
expect(button).toHaveTextContent("1");
//set amount
const amount = container.querySelector("#amount-textField") as HTMLElement;
fireEvent.change(amount, { target: { value: "1" } });
//set date
const dateInput = container.querySelector("#dayYear-textField") as HTMLElement;
fireEvent.change(dateInput, { target: { value: "2023-06-14" } });
//save button
const saveButton = screen.getByText("保存");
server.use(
rest.post("http://127.0.0.1:8000/api/v1/records", (_req, res, ctx) => {
return res.once(
ctx.status(500),
ctx.json([
{
code: "Internal ERROR",
},
])
);
})
);
await act(() => {
fireEvent.click(saveButton);
});
//保存failed。
expect(container).toHaveTextContent("CODE:");
});