umi框架UI测试实践(一)

umi 内置了jest 的测试框架,并且内置的 jest.config.js 也配置了 typescriptbabel。因此我们不需要再另外设置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.ENVundefinedENV 会被忽略,导致全局变量声明没导出 *

      {
        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(
          
            
          
          container,
        );
      });
    
    • 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(
            
              
            ,
            container,
          );
        });
      
    • TypeError: Cannot read property 'userInfoModel' of undefined

      userInfoModelumi 中的 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(
            
              
                
              
            ,
            container,
          );
        });
      
        expect(order.getOrderDetail).toBeCalled();
        expect(order.getOrderDetail).toBeCalledWith({
          orderId: 123456,
        });
      });
      
    • ✕ 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(
          
            
              
            
          ,
          container,
        );
      });
    
      expect(order.getOrderDetail).toBeCalled();
      expect(order.getOrderDetail).toBeCalledWith({
        orderId: 123456,
      });
    });
    

你可能感兴趣的:(umi框架UI测试实践(一))