在 UMI 中使用 Jest + @testing-library 对 React 组件进行单元测试

4,458 阅读3分钟

工具集

Jest

测试框架,用来跑测试用例

@testing-library

编写测试用例的库

@testing-library/react

专门用于测试 React 组件,必用,常用的 API 有 render waitFor screen

@testing-library/user-event

用于模拟用户事件,必用

@testing-library/jest-dom

扩展断言的,必用,例如:expect().toBeInTheDocument

msw

模拟接口请求,必用

测试三步骤

渲染组件、模拟用户事件、断言

问题记录

import { xx } from umi xx is undefined

umi 内部的导入语句使用的 @@/xx,jest 无法识别,因此需要在 jest.config.js 中配置,代码如下:

moduleNameMapper: { 
    '^@/(.*)$': '<rootDir>/src/$1', 
    '^@@/(.*)$': '<rootDir>/src/.umi/$1', 
},

在 umi 配置文件中使用了 define 配置全局变量,jest 无法读取

umi 中的 define 配置,本质上是使用的 webpack.DefinePlugin 插件实现,因此这是一个 DefinePluginjest 的问题,通过 jest.config.js 配置解决,代码如下:

globals: {
    XXXX: 'xxxx',
}

组件使用到了 dva,该如何初始化 store 数据,以进行测试

使用类似 react-redux 提供的方法,在自定义 render 中使用 <Provider store={store}> 没成功。

目前是在自定义 render 中直接实例化 dva,但是需要手动引入全部 model,不多说,上代码。

// 自定义 render
const customRender = (ui, { initialState, ...options }) => {
  const app = dva({
    initialState
  });
  
  app.model(require('../pages/policy/model').default);
  app.model(require('../models/insurer').default);
  
  app.router(() => ui);
  
  const App = app.start();

  const AllTheProviders = () => {
    return (
        <IntlProvider
          locale={defaultLang}
          messages={locales[defaultLang]}
        >
            <LocaleProvider currentLang={defaultLang}>
              <App></App>
            </LocaleProvider>
        </IntlProvider> 
    )
  }

  return render(ui, { wrapper: AllTheProviders, ...options })
}

// index.test.js
test(xx, async () => {
   customRender(<XX />, {
      initilState: {
          user: {
              name: 'xy'
          }
      }
   })
})

每个测试用例执行后,出现类似没有清空 document.body 的情况

比如我有三个测试用例,但第一个测试用例中,弹出了一个模态框,在接下来的测试用例中,模态框依然存在与 document.body 中,可以通过 screen.debug() 查看可见内容证明,极大影响了后面测试用例中的断言。

@testing-library/react 文件中说如果测试框架是 jest,那么会在 afterEach 中自动调用 cleanup 方法。后面尝试手动调用 cleanup 也没成功,目前并不清楚问题出在哪里。

目前解决方法是在 afterEach 中调用 document.body.innerHTML = '' 清空每次测试用例中的渲染。我不清楚这是否会带来其他副作用,但至少解决了每个测试用例完毕后,渲染内容不清空的严重问题。

render 有一个 container 参数,表示组件内容被渲染到哪个元素中,默认值是 document.body,并且模态框总是渲染到 document.body 中,而不是指定的 container 中的。或许这是 cleanup 看起来没有生效的原因?

单元测试中常见需求介绍

jest 模拟本地文件模块

当本地文件模块中导出的内容无法在单元测试中运行,或者压根儿不是你想要的结果,那么可以选择手动模拟。方法如下:

首先在被模拟的文件所在目录中新建 __mocks__ 文件夹,并放入被模拟的同名文件,同名文件中自定义导出内容,最后添加以下配置。

// jest.config.js
setupFilesAfterEnv: [
    '<rootDir>/src/setupTests.js',
],
// setupTests.js
jest.mock(需导出的模块)

jest 中还有很多模拟相关内容,遇到了再添加。

模拟接口请求

@testing-library 文档示例中已经写得很请求了,照着抄就行了,nice。

异步测试

目前只遇到了请求接口异步的场景,也是相当常见的需求。

当模拟了接口请求后,需要获取相应内容时,不能直接通过 getByXXX 或者 queryByXXX,因为它们都是同步的。

可以通过 waitFor API 或者 findByXXX,上代码:

// 该用例中,异步操作后,获取必定出现的内容
const ele = await screen.findByXXX();
// or
const ele = await waifFor(() => screen.getByXXX());

// 该用例中,异步操作后,获取不应该出现的内容
const ele = await waifFor(() => screen.queryByXXX());
expect(ele).not.toBeInTheDocument();

其实就是对 queryByXXX findByXXX getByXXX waitFor 的合理应用

最后

欢迎指正