nextjs + react +React Testing Library 项目单元测试

970 阅读5分钟

背景

react官方推荐RTL(react testing library)

为什么不用 Enzyme,因为Enzyme shallow对react hooks的支持真的是一言难尽

并且enzyme-adapter-react只更新到16

创作者之一更是直言如果要支持react18的话,不仅adapter,连enzyme都需要重写,这对于三年没更新的enzyme来说基本是不可能的

目前react18刚更新的问题: next/jest 需要nextjs 12

@testing-library/react-hooks 目前只支持到 react 17,未来估计会适配react18

配置 jest

首先安装一下jest相关的依赖

npm install --save-dev jest @types/jest @jest/types jest-environment-jsdom

安装好了以后,初始化jest配置

npx jest --init

image.png

  • 是否为package.json中添加test脚本,这个选yes就可以了
  • 是否用ts,按照你的项目选
  • 单测环境(jsdom):因为我们会涉及到 dom 的单测,不仅仅是纯逻辑,如果是纯逻辑的选 node。
  • 是否需要覆盖率报告(no):暂时用不上,后面覆盖率章节会着重介绍。
  • 编译代码(babel): 可以转 ES5,避免一些兼容性问题。
  • 每次测试完是否清理 mock、实例等结果(yes): 每次测试完成后会清理 mock 等上次测试的结果,可以避免用例之间的互相影响

然偶在根目录已经生成了对应的 jest.config.ts 文件,大家也可以根据自己的需要增加额外的自定义配置,具体可以参考 Configuring

因为选用的是babel,所以我们还要做一下相关配置

npm install --save-dev babel-jest 

// 按需安装
@babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript ts-node

在babel.config中添加自动引用依赖, 为它加上 runtime: "automatic"的配置,这样每个单测文件可以自动引用react依赖,(如果你指向配置next/babel,那么后面那些除了ts-node的插件可以不安装)

// ./babel.config.js
module.exports = {
    presets: [
      ['next/babel'],
      ["@babel/preset-env", { targets: { node: "current" } }],
      ["@babel/preset-react",{ runtime: "automatic" }], // 自动导入react
      "@babel/preset-typescript",
    ],
}

这样初步配置就基本完成了,我们可以在根目录创建一个test文件夹,新建一个最简单的单测文件(一定要带上.test这个后缀才会被jest识别成要执行的单测文件)

// App.test.ts
import React from "react";

test("test", () => {
  expect(1 + 1).toBe(2);
});

然后运行

yarn test

即可得到结果

image.png

jest只会识别js,jsx文件,所以想要让他识别更多的文件,我们需要添加mock

npm install --save-dev identity-obj-proxy
// jest.config.ts
moduleNameMapper: {
  ".(css|less|sass|scss)$": "identity-obj-proxy"
},
transform: {
  "^.+.(js|ts|tsx)$": "<rootDir>/node_modules/babel-jest"
},

配置 next/jest

next项目还是和正常的react项目有所不同的,具体可以参考next.js官方文档如何配置next+react testing library (ps next12有next/jest,11.0.0没有,所以找不到依赖可以升级一下)

// jest.config.js
import nextJest from 'next/jest'

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
})

const customJestConfig =  {
    // 这里面写你正常的jest配置
    ...
}

export default createJestConfig(customJestConfig)

配置 React Testing Library

安装依赖

  • @testing-library/jest-dom:用于 dom、样式类型等元素的选取。
  • @testing-library/react:提供针对 React 的单测渲染能力。
  • @testing-library/user-event:用于单测场景下事件的模拟。
npm install --save-dev @testing-library/jest-dom @testing-library/react
// 按需安装
npm install --save-dev @testing-library/user-event

先在test/config文件夹下面配置文件

// test/config/jest-setup.js
import '@testing-library/jest-dom'

然后jest.config中全局引入环境jest-dom

// jest.config.ts
setupFilesAfterEnv: ["<rootDir>/test/config/jest-setup.js"],

然后在test文件夹下创建一个最简单的tsx单测文件

// test/AppReact.test.tsx
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import App from "@/pages/_app";

describe("test", () => {
  test("unit test1", () => {
    render(<App />);
    expect(screen.getByText("渠道管理平台")).toBeInTheDocument();
  });
});
// src/pages/_app.tsx
function CustomApp({ Component, pageProps }: any) {
  return (
    <div>管理平台</div>
  );
}

export default CustomApp;

这时候你会发现这个报错

image.png 这是因为你在tsconfig.json和next.config.js里面的path(路径替换)都不会被jest识别,如果还想让路径替换生效,需要在jest.config.js里重新声明一下。声明规则可以参考moduleNameMapper

改为

// jest.config.js
moduleNameMapper: {
    '\\.(css|less|sass|scss)$': 'identity-obj-proxy',
    '@/(.*)': '<rootDir>/src/$1',
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/test/__mocks__/fileMock.js',
},

其他文件的Mock用的是第三行

// /test/__mocks__/fileMock.js
module.exports = 'test-file-stub';

然后就可以运行出正确结果

image.png

模拟next/config和next/router

在next项目中,免不了要使用next/config去调用当前配置,router去查看当前路径/获取query,这一部分其实官网有给出解决方案

image.png

点击前往next-router-mock

但是这个解决方案要安装很多依赖,在安@testing-library/react-hooks这个库时候发现他只支持react版本16和17,但是我们项目用的是18,所以只能另辟蹊径

经过多年的单测经验,只要直接粗暴的mock掉这两个依赖就好了,代码如下

import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import App from "@/pages/_app";

// mock next/config 
jest.mock('next/config', () =>  jest.fn().mockImplementation(() => {
  return {
    publicRuntimeConfig: {
      API_HOST: 'api/',
      STATIC_ASSETS_URL: '/static/assets'
    }
  };
}))

// mock next router
jest.mock('next/router', () => ({
  useRouter: jest.fn().mockImplementation(() => {
    return {
      pathname: '/'
    };
  })
}))



describe("test", () => {
  test("测试container是否正常运行", () => {
  // 这里是为了防止传参类型AppProps报错所以传了Components和pageProps,如果没有这个问题可以不传
    const wrapper = render(<App Component={() => <div>111</div>} pageProps={{}}/>);
    expect(screen.getByText("管理平台")).toBeInTheDocument();
    // screen.getByRole('button')
    // expect(wrapper).toMatchSnapshot()
  });
});

然后就可以正常运行了,运行后的图有很多项目路径,就不放了哈 当然老在Terminal中看覆盖很费劲,我们也可以在package.json中添加

"test:coverage": "jest --coverage"

然后运行

yarn test:coverage

在根目录会生成coverage文件夹,点里面随意的.html文件,然后点左上的ALL files你就能找到所有测到文件的覆盖率

image.png

点到指定文件,红色的就是没有覆盖到的行,黄色的就是分支没有覆盖的地方

image.png

如果遇到window.matchMedia is not a function这个报错 那是jsdom没有模拟到,可以参照jest官网的解决方案

参考文献

桢民老师的自动化测试文章

下一篇文章会写一下怎么去写测试用例