前言

本文将在项目中增加Vitest用于单元测验,并将React Testing Library集成到Vitest中,用于测验React组件。相同,这套方案能够被集成到任意React项目中,如Next.js

经过5分钟阅览10分钟实践把握解决方案,后文附有完好源码

Vitest

Vitest是由Vite提供支撑的极速单元测验框架,最近非常火热,首要是因为以下Vitest具有以下几个优点:

  1. Vitest为单元测验提供更快的运转速度
  2. 能够无缝的将 Jest 替换成 Vitest
  3. 开箱即用的 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 + React Testing Library进行单元测试

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 + React Testing Library进行单元测试

这时在控制台报错,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

使用Vitest + React Testing Library进行单元测试

编写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 + React Testing Library进行单元测试

假如测验没有经过,vitest也会提示出详细的期望和实践以及过错的行数,用于修复

使用Vitest + React Testing Library进行单元测试

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 思否