前端单元测试:jest 与 react 使用,学习
为什么要学习单元测试,如何测试 react的组件
视频列表
- 为什么要写测试
- #00 熟悉工具箱jest 指令
- 前端單元測試 #01 第一個測試、快照
- 前端單元測試 #02 測試模板、props
- 前端單元測試 #03 mock function、fireEvent
- 前端單元測試 #04 mock 時間、act
- 前端單元測試 #05 styled components、debug
- 前端單元測試 #06 subtest、test each
- 前端單元測試 #07 mock api、waitFor
为什么写测试
- 让代码更加的健壮
- 让代码重构得更好
- 不会写测试的原因:自己的代码不能测试
- 这个时候,就需要先重构你的代码
- 让自己的代码可以测试
- 形成良好的代码书写习惯
几种测试的关系
- 单元测试: 应该是最多的,也是最简单的
- 集成测试: 组合单元测试
- E2E测试:成本最高,但很重要
#00 熟悉工具箱jest 指令
- jest: 启动测试
- jest --watch: 各种指令
- 测试单个文件:
npx jest ./src/2022-05/01-jest-watch/01.spec.js
几个官方参考文档
- https://github.com/testing-library/jest-dom
- https://jestjs.io/docs/tutorial-react
- https://create-react-app.dev/docs/running-tests/#docsNav
- https://testing-playground.com/
› Press a to run all tests.
› Press f to run only failed tests.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
› Press Enter to trigger a test run.
前端單元測試 #01 第一個測試、快照
小插曲,直接运行
npm run test
有报错githube有人讨论: https://github.com/facebook/create-react-app/issues/11792
解决方案:
npm i -D jest-watch-typeahead@0.6.5
Error: Failed to initialize watch plugin "node_modules/jest-watch-typeahead/filename.js":
● Test suite failed to run
file:///Users/aric.zheng/aric-notes/jest-notes/src/2022-05/02-react-cra/node_modules/jest-watch-typeahead/build/file_name_plugin/prompt.js:4
import { PatternPrompt, printPatternCaret, printRestoredPatternCaret } from 'jest-watcher';
^^^^^^^^^^^^^^^^^
SyntaxError: Named export 'printPatternCaret' not found. The requested module 'jest-watcher' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:
import pkg from 'jest-watcher';
const { PatternPrompt, printPatternCaret, printRestoredPatternCaret } = pkg;
at async requireOrImportModule (node_modules/jest-util/build/requireOrImportModule.js:65:32)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
开始测试之前,还要添加如下包<
React CRA官方已经带上了
>touch ./src/setupTests.js
# 添加包
npm i -D @testing-library/react @testing-library/jest-dom
# 准备setupTests.js 文件
import '@testing-library/jest-dom';
第一个 jest + react
- screen.getByXX
- toHave/toBe
import { render, screen } from '@testing-library/react';
import { Btn } from './btn';
test('btn should have hi text', () => {
render(<Btn />);
// screen.debug();
expect(screen.getByText('hi')).toBeInTheDocument();
expect(screen.getByText('hi')).toBeVisible();
expect(screen.getByRole('button')).toHaveTextContent('hi');
});
snapshot 测试
- 对于少互动的情况,可以直接用 snapshot
- 测试的时候,按
u
更新快照 - 两个关键方法
- toMatchSnapshot
- toMatchInlineSnapshot<这个有点问题>
前端單元測試 #02 測試模板、props(AAA的思路)
- 准备属性 Arrange-准备,安排一些数据
- 执行 render Act:执行一些操作
- 验证结果 Assert:验证结果
- AAA模板如下
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './04';
import { act } from 'react-dom/test-utils';
test('btn should have hi text', () => {
// 准备 Arrange
// 执行 Act
// 断言 Assert
});
import { render, screen } from '@testing-library/react';
import { BtnProps } from './btn-props';
test('btn should have hi text', () => {
// 准备 content
const content = 'ABC';
// 执行 render
render(<BtnProps content={content} />);
// 验证
expect(screen.getByText(content)).toBeInTheDocument();
});
前端單元測試 #03 mock function、fireEvent
- 准备一个mock的方法:
jest.fn()
- fireEvent:执行事件(userEvent也是同样的效果)
- userEvent 会更从
user
的角度(mousedown
代替也会成功) – more friendly - fireEvent 如果用
mouseDown代替了onClic
k 就会触发失败
- userEvent 会更从
toHaveBeenCalledTimes(1)
: 验证执行次数
import { render, screen, fireEvent } from '@testing-library/react';
import { Btn } from './03';
test('btn should have hi text', () => {
// 准备 Arrange
const content = 'ABC';
const handleClick = jest.fn();
// 执行 Act
render(<Btn content={content} onClick={handleClick} />);
fireEvent.click(screen.getByText('ABC'));
// 验证 Assert
expect(handleClick).toHaveBeenCalledTimes(1);
});
前端單元測試 #04 mock 時間、act
- 时间: jest.runAllTimers()、
jest.advanceTimersByTime(3000);
- act:针对有副作用 sideEffect 的场景
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './04';
import { act } from 'react-dom/test-utils';
test('btn should have hi text', () => {
// 准备 Arrange
jest.useFakeTimers();
// 执行 Act
render(<Button />);
fireEvent.click(screen.getByText('你好'));
// 验证 Assert
expect(screen.getByText('你好')).toBeDisabled();
act(() => {
// 等所有的 timer 运行完
// jest.runAllTimers();
// 精确运行3s
jest.advanceTimersByTime(3000);
});
expect(screen.getByText('你好')).toBeEnabled();
});
前端單元測試 #05 styled components、debug
- snapshot的时候,如果遇到 styled-component 这种情况,需要用 jest-styled-componet
import { render } from '@testing-library/react';
import { Button } from '../components/05';
test('btn should have hi text', () => {
// 准备 Arrange
// 执行 Act
const { container } = render(<Button />);
// 断言 Assert
expect(container).toMatchSnapshot();
});
先安装这个包
- jest-styled-components
- styled-components
// 1. 安装包
npm i -D jest-styled-components
npm i -S styled-components
// 2. 添加进入 setup.jest.js
前端單元測試 #06 subtest、test each
- test.each:特别适合,不同的参数,场景类似;
- 可以数组传参
- 可以对象传参
- describe/test: 这个是手动分组
- 可以用3种形式数据源
- array
- object
- markdown
// 用 test.each 来批量测试
describe.each([
['A', 'eb-bg-green-500'],
['B', 'eb-bg-blue-500'],
['C', 'eb-bg-red-500'],
['D', 'other'],
])('btn should have diff style when diff type', (type, className) => {
// 准备 Arrange
const t = type;
const c = className;
// 执行 Act
// 断言 Assert
test(`t will get style: ${c}`, () => {
render(<Button type={t}>{t}</Button>);
expect(screen.getByText('Hi')).toHaveClass(c);
});
});
// test.each use object as data source
describe.each([
{ type: 'A', className: 'eb-bg-green-500' },
{ type: 'B', className: 'eb-bg-blue-500' },
{ type: 'C', className: 'eb-bg-red-500' },
{ type: 'D', className: 'other' },
])('btn should have diff style when diff type', ({ type, className }) => {
// 准备 Arrange
const t = type;
const c = className;
// 执行 Act
// 断言 Assert
test(`t will get style: ${c}`, () => {
render(<Button type={t}>{t}</Button>);
expect(screen.getByText('Hi')).toHaveClass(c);
});
});
// test.each use markdown string as data source
describe.each`
type | className
${'A'} | ${'eb-bg-green-500'}
${'B'} | ${'eb-bg-blue-500'}
${'C'} | ${'eb-bg-red-500'}
${'D'} | ${'other'}
`('markdown as ds: btn should have diff style when diff type', ({ type, className }) => {
// 准备 Arrange
const t = type;
const c = className;
// 执行 Act
// 断言 Assert
test(`t will get style: ${c}`, () => {
render(<Button type={t}>{t}</Button>);
expect(screen.getByText('Hi')).toHaveClass(c);
});
});
前端單元測試 #07 mock api、waitFor
- msw: 拦截api,可以用测试、开发
- await findByText === (getByText + waitFor)
小知识 msw(一种可以用于开发/测试的库)
- Mock Server Worker
- 拦截网络请求用的
- https://mswjs.io/
- jest 会利用 msw node 的server 来完成
// src/setupTests.js
// eslint-disable-next-line jest/no-mocks-import
import { server } from '../__mocks__/server.js';
import { Button } from '../components/07';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
// Establish API mocking before all tests.
beforeAll(() => server.listen());
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers());
// Clean up after the tests are finished.
afterAll(() => server.close());
test('api sucess, will get item1/2/3', async () => {
// https://api.uomg.com/api
render(<Button />);
userEvent.click(screen.getByText('請按'));
expect(screen.getByText('Loading........')).toBeInTheDocument();
await waitFor(() => expect(screen.getByText('item1')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('item2')).toBeInTheDocument());
await waitFor(() => expect(screen.getByText('item3')).toBeInTheDocument());
});
test.only('api failed', async () => {
// mock server error
server.use(
rest.get('http://my-backend/fake-data', (req, res, ctx) => {
return res(ctx.status(500), ctx.json('ERROR_MSG'));
}),
);
render(<Button />);
userEvent.click(screen.getByText('請按'));
// await waitFor(() => expect(screen.getByText('ERROR_MSG')).toBeInTheDocument());
// screen.debug();
expect(await screen.findByText('ERROR_MSG')).toBeInTheDocument();
});
前端單元測試 #08 react hook
- 解决的问题:useHook 不用先用一个组件来测试
- 直接用 renderHook 来完成
- 重点是这一句:
const { result } = renderHook(() => useCounter());
import { render, screen, fireEvent, renderHook } from '@testing-library/react';
import DemoCounter, { useCounter } from '../components/08';
import { act } from 'react-dom/test-utils';
test('counter hook testing', () => {
// 准备 Arrange
// 执行 Act
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
// 断言 Assert
expect(result.current.count).toBe(1);
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(3);
});
前端單元測試 #09 mock component、整合測試
- mock 一个组件
jest.mock
- mock Math.random 函数
jest.spyOn(global.Math, 'random').mockReturnValue(0.1);
- jest.spy 针对特定的方法,进行mock
jest.mock('../components/09/Trade', () => () => 'FakeTradeComponent123');
import { screen, render } from '@testing-library/react';
import Trade from '../components/09/Trade';
test('<Trade />', () => {
// Arrange
// randomMock = jest.spyOn(global.Math, "random");
jest.spyOn(global.Math, 'random').mockReturnValue(0.1);
// jest.spyOn(global.Math, 'floor')
// Act
render(<Trade wood={50} />);
// Assert
// 😱
// expect(screen.getByText(/賣完,得到 \$ 7000/i)).toBeInTheDocument();
expect(screen.getByTestId('sell').textContent).toBe('賣完,得到 $7000');
});
jest 与 react 冲突问题
参考
- https://www.youtube.com/watch?v=1ah2jZ9FeQs&list=PLsKJIR9go2Rne6kzKftZxeQHH9HvR0RdV
- https://create-react-app.dev/docs/running-tests/#docsNav
- https://mp.weixin.qq.com/s/pgdcDNjDGPgNq76Zh_dZxg
- https://mswjs.io/docs/api/rest
- https://github.com/Yang-025/react-typescript-testing
- https://codeyourgreens.com/jest/mock-functions
- https://jestjs.io/zh-Hans/docs/jest-object#jestspyonobject-methodname