前言
本文将在项目中增加Vitest用于单元测验,并将React Testing Library集成到Vitest中,用于测验React组件。相同,这套方案能够被集成到任意React项目中,如Next.js
经过5分钟阅览,10分钟实践把握解决方案,后文附有完好源码
Vitest
Vitest是由Vite提供支撑的极速单元测验框架,最近非常火热,首要是因为以下Vitest具有以下几个优点:
- Vitest为单元测验提供更快的运转速度
- 能够无缝的将 Jest 替换成 Vitest
- 开箱即用的 TypeScript / JSX 支撑
React Testing Library
React Testing Library是用于 DOM 和 UI 组件测验的一系列工具,首要 API 包括 DOM 查询。UI 测验工具还有 Airbnb 的enzyme(opens new window)。后文简称RTL。
经过本人在团队业务中的亲身实践,放弃enzyme的原因有:
- Enzyme 依赖 React 的内部完成,React 团队不鼓励运用它
- Enzyme 不适用于函数式组件,因为函数式组件没有实例,所以无法经过类似 instance.xxx 的办法来对状态进行验证,也无法经过 instance.method的办法来获取组件实例的办法
- React 17现在只能运用非官方的 Enzyme 适配器(比方 @wojtekmaj/enzyme-adapter-react-17)
- RTL 能够作为 Enzyme 的替代品。RTL 不直接测验组件的完成细节,而是从一个 React 使用的视点去测验。
代码装备
装置依赖
首要咱们在项目中装置Vitest
yarn add vitest -D
然后在package.json中增加如下代码
"scripts": {
...
"test": "vitest"
...
}
然后在项目文件夹内新增__test__文件夹
mkdir __test__
在__test__文件夹中新建test.test.ts
import { describe, it, expect } from "vitest";
describe("suite", () => {
it("test true", async () => {
expect(true).toBe(true);
});
it("test false", async () => {
expect(false).toBe(false);
});
});
这部分的describe, it, expect跟jest里的运用办法一样,唯一的区别在于需求从vitest里导出
Vitest装备
在项目文件夹内新增vite.config.js装备
项目内不存在vite
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// ...
},
})
项目内存在vite
我的项目内选用的是vite,所以后文的所有装备基于此装备进行
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
export default defineConfig({
plugins: [react()],
});
React Testing Library装备
接下来咱们将装置RTL,用于React组件的测验。
yarn add @testing-library/react @vitejs/plugin-react jsdom -D
jsdom是许多Web规范的纯JavaScript完成,特别是WHATWGDOM和HTML规范,用于Node.js。此处需求注意开发机器上的node版别,能够考虑选用nvm切换不同的node版别。
jsdom的根本用法如下:
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
console.log(dom.window.document.querySelector("p").textContent); // "Hello world"
const { window } = new JSDOM(`...`);
// or even
const { document } = (new JSDOM(`...`)).window;
Viteset集成RTL的装备
接下来咱们将RTL集成到vitest中
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
// <https://vitejs.dev/config/>
export default defineConfig({
plugins: [react()],
});
测验App组件
接下来咱们进行React组件的测验,首要在test目录下新建一个app.test.ts文件,导入以下代码。这段的判别是是否能渲染APP组件,而且APP组件dom的文字节点内包括「vitest」字符串,用RTL文档内的描绘便是:这将查找具有与给定 Text 匹配的 textContent 的文本节点的所有元素。
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { App } from "../src/App";
import React from "react";
describe("App", () => {
it("it should be render", () => {
render(<App />);
expect(screen.getByText("vitest")).toBeInTheDocument();
});
});
运转yarn test
这时在控制台报错,vitest 默认没有 toBeInTheDocument
办法, toBeInTheDocument 是 RTL中的断语办法。因此咱们需求新增一个装备文件,让vitest去承继RTL的断语库。新建一个RTLInVitest.setup.ts文件
import { expect, afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
import matchers, {
TestingLibraryMatchers,
} from "@testing-library/jest-dom/matchers";
declare global {
namespace Vi {
interface JestAssertion<T = any>
extends jest.Matchers<void, T>,
TestingLibraryMatchers<T, void> {}
}
}
// 承继 testing-library 的扩展 except
expect.extend(matchers);
// 全局设置整理函数,防止每个测验文件手动整理
afterEach(() => {
cleanup();
});
在vite.config.js中引入该ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
// <https://vitejs.dev/config/>
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
setupFiles: "./RTLInVitest.setup.ts",
},
});
运转yarn test
编写React组件
咱们简略完成一个DataList的组件,其中经过数据map多个DataItem组件。DataList组件支撑传入data,createable和flex参数,
DataList
import { DataItem } from "./DataItem";
interface Props {
createable?: boolean;
flex?: boolean;
data: DataItemType[];
}
export type DataItemType = { name: string; id: string };
export function DataList({ data, createable, flex }: Props) {
return (
<div>
{data.map((item) => (
<DataItem
key={item.id}
createable={createable}
item={item}
flex={flex}
/>
))}
</div>
);
}
DataItem
import { DataItemType } from "./DataList";
interface Props {
createable?: boolean;
flex?: boolean;
item: DataItemType;
}
export function DataItem({ item, createable, flex }: Props) {
return (
<div
className="dataItem"
key={item.id}
style={{ display: flex ? "flex" : "block" }}
>
<div>
<span>id:{item.id}</span>
<span>name:{item.name}</span>
</div>
{createable && (
<div>
<button>create</button>
<button>delete</button>
</div>
)}
</div>
);
}
测验自定义的React组件
接下来咱们进行React组件的测验,首要在__test__目录下新建一个data-list.test.ts文件。
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import { DataList } from "../src/component/DataList";
import React from "react";
const mockDataLength = 3;
const mockData = Array.from({ length: mockDataLength }, (_, index) => ({
name: `${index}`,
id: `${index}`,
}));
describe("DataList", () => {
it("it should be render", () => {
const { container } = render(<DataList data={mockData} />);
expect(container).toMatchSnapshot();
});
it("it should be createable with createable props", () => {
const { container } = render(<DataList data={mockData} createable />);
expect(screen.queryAllByText("create")).toHaveLength(mockData.length);
expect(screen.queryAllByText("delete")).toHaveLength(mockData.length);
});
it("it should not be createable without createable props", () => {
const { container } = render(<DataList data={mockData} />);
expect(screen.queryAllByText("create")).toHaveLength(0);
expect(screen.queryAllByText("delete")).toHaveLength(0);
});
it("it should be flex with flex props", () => {
const { container } = render(<DataList data={mockData} flex />);
const targets = document.querySelectorAll(".dataItem");
const matchs = [...targets].filter((item) =>
item.getAttribute("style")?.includes("flex")
);
expect(matchs).toHaveLength(mockData.length);
});
it("it should not be flex with flex props", () => {
const { container } = render(<DataList data={mockData} />);
const targets = document.querySelectorAll(".dataItem");
const matchs = [...targets].filter((item) =>
item.getAttribute("style")?.includes("block")
);
expect(matchs).toHaveLength(mockData.length);
});
});
接下来运转yarn test
假如测验没有经过,vitest也会提示出详细的期望和实践以及过错的行数,用于修复
TDD 测验驱动开发
关于TDD和BDD网上有各种不同的说法,个人了解的TDD,便是首要明确自己想要写什么代码,尽量从最简略的开始,写一个测验,再去详细地完成这小段代码,以达到这个测验经过的作用,之后再持续写测验、完成、测验经过,这样不停循环重构。
例如写一个函数,咱们能够首要写函数的调用的测验,然后完成代码并测验成功后,再测验函数的返回值,再持续完成函数…
TDD的优点便是会削减程序逻辑的过错,尽可能地削减bug。而缺陷便是假如更改了代码的完成逻辑,就需求修正测验,可能会使得测验代码难以保护。
BDD是TDD的一种弥补,在TDD的基础上,选用了更详细的功用描绘,经过考虑用户的行为、组件的功用,来编写测验。
考虑到项目的复杂度和依赖性,TDD需求简略和易于测验的代码,否则需求开发人员花较多的时间进行一些桥接的工作。因此个人比较推荐BDD的测验形式,经过功用和行为的描绘,即行为/条件…结果…,进行测验。
但是详细的形式能够根据实践开发工作进行挑选。
TDD和BDD的比较能够参阅:关于TDD和BDD
后记
demo代码:uWydnA/TestingLibraryInVitestSetup: TestingLibraryInVitestSetup (github.com)
参阅文档:
React单元测验战略及落地 – 简书 (jianshu.com)
node.js – 关于TDD和BDD – 个人文章 – SegmentFault 思否