umi
内置了jest
的测试框架,并且内置的 jest.config.js
也配置了 typescript
、babel
。因此我们不需要再另外设置babel.config.js
。
自定义 jest.config.js
-
做
UI测试
的话,框架内置的jest.config.js
还不能完全满足我们的需求,因此需要额外配置。配置的合并规则如下:const config = mergeConfig( createDefaultConfig(cwd, args), // jest内置配置 packageJestConfig, // package.json 自定义 userJestConfig, // jest.config.js 自定义 );
-
自定义配置踩坑
-
全局变量应用报错:
ReferenceError: ENV is not defined
,需要在globals
中设置{ globals: { ENV: process.env.ENV, } }
但是设置后还是报错,原因是*如果
process.env.ENV
是undefined
则ENV
会被忽略,导致全局变量声明没导出 *{ globals: { ENV: process.env.ENV || 'test', // undefined 导致全局变量未声明 } }
-
路径别名报错:
Cannot find module '@/services/...
或Cannot find module '@@/...
{ moduleNameMapper: { '^@/(.*)$': '
/src/$1', // 导入语句使用的 @/xx,jest 无法识别 '^@@/(.*)$': ' /src/.umi/$1', // umi 内部的导入语句使用的 @@/xx,jest 无法识别 } }
-
jest.config.ts
导致的自定义配置无法被识别
*.ts
后缀的文件 umi
不识别,所以一定要特别注意⚠️
-
最终
jest.config.js
如下module.exports = { moduleNameMapper: { '^@/(.*)$': '
/src/$1', '^@@/(.*)$': ' /src/.umi/$1', }, globals: { ENV: process.env.ENV || 'test', }, };
UI测试
实践
测试用例:路径为
**/cancel-order/:orderId
的取消订单页面将从路由上获取orderId
并将参数传给getOrderDetail
接口。-
React Test 准备
import { unmountComponentAtNode } from "react-dom"; let container = null; beforeEach(() => { // setup a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container); }); afterEach(() => { // cleanup on exiting unmountComponentAtNode(container); container.remove(); container = null; }); it("renders with or without a name", () => { act(() => { // render components }); // make assertions });
-
cancelOrder.test.tsx
import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { act } from 'react-dom/test-utils'; import CancelOrder from './index'; import * as order from '@/services/order'; let container: HTMLElement | null = null; beforeEach(() => { container = document.createElement('div'); document.body.appendChild(container); }); afterEach(() => { unmountComponentAtNode(container as HTMLElement); (container as HTMLElement).remove(); container = null; }); it(`getOrderDetail will get params.id from url`, () => { act(() => { render(
, container); }); expect(order.getOrderDetail).toBeCalled(); expect(order.getOrderDetail).toBeCalledWith({ orderId: 123456 }); });
-
TypeError: Cannot read property 'match' of undefined
const { id } = useParams<{ id: string }>();
可以通过局部
mock
useParams
来设置orderId
jest.mock('umi', () => { const originalModule = jest.requireActual('umi'); return { __esModule: true, ...originalModule, useParams: () => ({ id: 123456 }), }; });
-
Invariant failed: You should not use
outside a 页面中的组件有引用
import { withRouter } from 'react-router-dom'
,可以通过添加Router
解决act(() => { render(
-
TypeError: Cannot read property 'location' of undefined
需要传入history
属性,生成history
的方式有:1. `const history = createBrowserHistory();`, 或`const history = useHistory();` 2. [`useHistory`的 `history` 对象来源于 `Router.history` 的设置][**useHistory** will work on any child component or components which you have declared in your **Router** but it won't work on **Router**'s parent component or **Router** component itself.],因此它的结果值不能作用于`Router`。这里我们选择使用 `createBrowserHistory` 来生成 `history`。
act(() => { const history = createBrowserHistory(); render(
-
TypeError: Cannot read property 'userInfoModel' of undefined
userInfoModel
是umi
中的models
, 它是一个hook model
。底层的实现使用的是useRouter
+Context
。因此我们需要找到它的Provider
,并用它包裹组件。import Provider from '@@/plugin-model/Provider'; it('getOrderDetail will get params.id from url', () => { act(() => { const history = createBrowserHistory(); render(
-
✕ getOrderDetail params is 123456 (29 ms)
Matcher error: received value must be a mock or spy function Received has type: function Received has value: [Function getOrderDetail]
getOrderDetail
方法需要被mock
jest.mock('@/services/order', () => { const originalModule = jest.requireActual('@/services/order'); return { __esModule: true, ...originalModule, getOrderDetail: jest.fn(() => Promise.resolve({})), }; });
✓ getOrderDetail params is 123456 (23 ms)
-
-
第一个
UI测试
的完整代码import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { act } from 'react-dom/test-utils'; import Provider from '@@/plugin-model/Provider'; import { Router, createBrowserHistory } from 'umi'; import CancelOrder from './index'; import * as order from '@/services/order'; let container: HTMLElement | null = null; beforeEach(() => { container = document.createElement('div'); document.body.appendChild(container); }); afterEach(() => { unmountComponentAtNode(container as HTMLElement); (container as HTMLElement).remove(); container = null; }); jest.mock('umi', () => { const originalModule = jest.requireActual('umi'); return { __esModule: true, ...originalModule, useParams: () => ({ id: 123456 }), }; }); jest.mock('@/services/order', () => { const originalModule = jest.requireActual('@/services/order'); return { __esModule: true, ...originalModule, getOrderDetail: jest.fn(() => Promise.resolve({})), }; }); it('getOrderDetail will get params.id from url', () => { act(() => { const history = createBrowserHistory(); render(