blog icon indicating copy to clipboard operation
blog copied to clipboard

Jest单元测试配置和所遇问题解决办法

Open yinxin630 opened this issue 5 years ago • 6 comments

Jest(https://jestjs.io/) 是由 Facebook 推出的一款优秀的测试框架, 它集成了断言+测试的功能, 无须组合其他工具即可实现单元测试

上手体验

首先需要安装 Jest. npm i -D jest

创建源码目录 src, 编写将要测试的方法

// src/add.js
module.exports = function add(a, b) {
    return a + b;
}

创建测试目录 __test__, 编写第一个测试用例

// __test__/add.test.js
const add = require('../src/add');
test('add()', () => {
    expect(add(1, 2)).toBe(3);
});

在 package.json 中, 添加 scripts 测试命令, "test": "jest"

执行 npm test 运行单元测试, 结果如下 image

__test__ 是 Jest 默认测试目录, 如需使用其他目录可以在配置文件中修改

查看测试覆盖率

可以修改 Jest 配置启用覆盖率输出, 在根目录创建配置文件 jest.config.js, 添加如下内容

module.exports = {
    collectCoverage: true,
}

重新执行单元测试, 结果如下 image

同时在你的项目中会生成 coverage 目录, 这里面是 web 版的详细覆盖率报告

我们先在 package.json 新增一个命令, 来快捷打开 web 版覆盖率报告
添加 "coverage": "open ./coverage/lcov-report/index.html"
执行 npm run coverage 查看报告 image

添加 TypeScript 支持

首先, 将 add.js 修改为 add.ts

// src/add.ts
export default function add(a: number, b: number) {
    return a + b;
}

add.test.js 修改为 add.test.ts

// __test__/add.test.ts
import add from '../src/add';
test('add()', () => {
    expect(add(1, 2)).toBe(3);
});

新增 tsconfig.json 添加 TypeScript 配置

// tsconfig.json
{
    "compilerOptions": {
        "target": "es5",
        "strict": true,
    },
    "include": [
        "src/**/*",
        "__test__/**/*"
    ],
    "exclude": [
        "node_modules",
    ]
}

使用 Jest 测试 TypeScript 代码需要借助 ts-jest 解析器
安装依赖 npm i -D ts-jest typescript @types/jest

修改 Jest 配置文件, 将 ts 文件解析器设置为 ts-jest

// jest.config.js
module.exports = {
    collectCoverage: true,
    transform: {
        '^.+\\.tsx?$': 'ts-jest',
    },
}

重新执行 npm test 查看结果

添加 React 支持

首先安装相关依赖
npm i --save react react-dom
npm i -D @types/react @types/react-dom

修改 tsconfig.json 添加 tsx 支持

// tsconfig.json
{
    "compilerOptions": {
        ...
        "jsx": "react",
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true
    },
    ...
}

测试 React 代码, 还需要借助 enzyme(https://airbnb.io/enzyme/), 这是由 Airbnb 出的一个 React 测试工具

安装依赖
npm i -D enzyme enzyme-adapter-react-16 @types/enzyme @types/enzyme-adapter-react-16

新增一个 React 组件 Example

// src/Example.tsx
import React from 'react';
export default function Example() {
    return (
        <div>Example</div>
    )
}

新增 Example 组件测试用例

// __test__/Example.test.tsx
import React from 'react';
import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

import Example from '../src/Example';

configure({ adapter: new Adapter() });

test('<Example>', () => {
    const example = mount(<Example/>);
    expect(example).toMatchSnapshot({});
    expect(example.html()).toBe('<div>Example</div>');
})

执行 npm test -- __test__/Example.test.tsx 单独测试 Example 组件, 结果如下 image

React 常用测试场景

传递和更新 props

新增一个有 props 的组件 Message

// src/Message.tsx
import React from 'react';
interface MessageProps {
    msg: string;
}
export default function Message(props: MessageProps) {
    return (
        <div>{props.msg}</div>
    )
}

编写 Message 组件的测试用例

// __test__/Message.test.tsx
...
test('<Message>', () => {
    const message = mount(<Message msg="初始消息" />);
    expect(message.html()).toBe('<div>初始消息</div>');
    // 更新 props
    message.setProps({ msg: '更新消息' });
    expect(message.html()).toBe('<div>更新消息</div>');
})

模拟触发事件

新增一个监听点击事件的组件 Count

// src/Count.tsx
import React, { useState } from 'react';
export default function Count() {
    const [count, setCount] = useState(0);
    return (
        <div>
            <span>{count}</span>
            <button onClick={() => setCount(count + 1)}>+1</button>
        </div>
    )
}

编写 Count 组件的测试用例

// __test__/Count.test.tsx
...
test('<Count>', () => {
    const count = mount(<Count/>);
    expect(count.find('span').text()).toBe('0');
    // 模拟 click 事件
    count.find('button').simulate('click');
    expect(count.find('span').text()).toBe('1');
})

实践中遇到的问题和解法

浏览器 API 不支持的情况

Jest 默认下是用 jsdom(https://github.com/jsdom/jsdom) 这个虚拟环境来运行测试的, 它是一个仿浏览器环境, 但是并不支持所有的浏览器 API, 比如 URL.createObjectURL 就是不支持的

对于不支持的 API, 需要我们对其添加 faker function

// @ts-ignore
global.URL.createObjectURL = jest.fn(() => 'faker createObjectURL');

注意, 一定要保证在调用 API 之前就已经注入了 polyfill, 比如某些模块可能包含自执行代码, 在 import 该模块的时候, 就开始调用 API 了, 所以需要将 polyfill 放在 import 之前

调用 setTimeout 的代码

还是以 Count 组件为例, 修改一下逻辑, 不再点击按钮时自增, 修改为 mounted 1000ms 后自增一次

// src/Count.tsx
import React, { useState, useEffect } from 'react';
export default function Count() {
    const [count, setCount] = useState(0);
    useEffect(() => {
        setTimeout(() => setCount(count + 1), 1000);
    }, []);
    return (
        <div>
            <span>{count}</span>
        </div>
    )
}

要测试 setTimeout, 需要使用 Jest 提供的 faker timer

同时还要注意, 使用 faker timer 后, react-dom 要求将更新 state 的逻辑包在 act 方法中, 否则就会出现如下的警告信息
Warning: An update to Count inside a test was not wrapped in act(...).

完整的测试用例如下所示

// __test__/Count.test.tsx
import React from 'react';
import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { act } from 'react-dom/test-utils';
import Count from '../src/Count';
configure({ adapter: new Adapter() });
// 注入 faker timers
jest.useFakeTimers();

test('<Count>', () => {
    const count = mount(<Count/>);
    expect(count.find('span').text()).toBe('0');
    act(() => {
        // 等待 1000ms
        jest.advanceTimersByTime(1000);
    })
    expect(count.find('span').text()).toBe('1');
})

yinxin630 avatar Aug 01 '19 03:08 yinxin630

你好,请问用过axios请求过 http://host:port/api 这样的API吗? 使用http协议得时候,jest会报 Timeout - Async callback was not invoked within the 5000ms timeout specified by jes timeout specified by jest.setTimeout.Error 使用https得时候,会正常通过,请问又配置可以解决这个问题吗?

zy-zero avatar Nov 19 '19 07:11 zy-zero

你好,请问用过axios请求过 http://host:port/api 这样的API吗? 使用http协议得时候,jest会报 Timeout - Async callback was not invoked within the 5000ms timeout specified by jes timeout specified by jest.setTimeout.Error 使用https得时候,会正常通过,请问又配置可以解决这个问题吗?

是不是接口只能通过 HTTPS 请求呢, 有示例代码吗

yinxin630 avatar Nov 19 '19 08:11 yinxin630

image image 是这样的,我这里的request本地使用了http 然后线上测试使用了https 跑jest的时候, https可以通过,但是http就会报timeout 后面是我react-redux的代码和jest: image

image

zy-zero avatar Nov 19 '19 08:11 zy-zero

Uploading image.png…

liuzhongbao avatar Nov 12 '20 02:11 liuzhongbao

@liuzhongbao 你的图没传上来...

yinxin630 avatar Nov 12 '20 06:11 yinxin630

@zy-zero 推荐 mock request 方法, 单测的时候不必真的发请求, 那样太慢了. 看下 jest.mock()

yinxin630 avatar Nov 12 '20 06:11 yinxin630