ReactWheels05-单元测试

单元测试

测试我们的classes.tsx

1
# lib/helpers目录下 新建 __test__/classes.unit.jsx

classes.unit.jsx

1
2
3
4
5
6
7
import classes from '../classes'
describe('classes', () => {
it('接受 1 个 className', () => {
const result = classes('a')
expect(result).toEqual('a')
})
})

运行 yarn test 突然报错了,说缺少模块 babel-preset-react-app

1
2
3
4
5
6
7
8
9
# 我的是这个版本
yarn add babel-preset-react-app@7.0.2 --dev

# 继续 运行测试
yarn test

# 每次改都运行 yarn test 很麻烦 怎么办

yarn test --watch
查看 npm包版本信息
1
npm view jquery versions

完整测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import classes from '../classes'
describe('classes', () => {
it('接受 1 个 className', () => {
const result = classes('a')
expect(result).toEqual('a')
})
it('接受 2 个 className', ()=>{
const result = classes('a', 'b')
expect(result).toEqual('a b')
})
it('接受 undefined 结果不会出现 undefined', ()=>{
const result = classes('a', undefined)
expect(result).toEqual('a')
})
it('接受各种奇怪值', ()=>{
const result = classes(
'a', undefined, '中文', false , null
// 'a', undefined, '中文'
)
console.log(result)
expect(result).toEqual('a 中文')
})
it('接受 0 个参数', ()=>{
const result = classes()
expect(result).toEqual('')
})
})

测试我们的 Icon

1
2
3
4
5
6
7
8
9
10
11
# 新建lib/icon/__tests__/icon.unit.jsx
import * as renderer from 'react-test-renderer'
import React from 'react'
import Icon from '../icon'

describe('icon', () => {
it(' xxx ', () => {
const json = renderer.create(<Icon/>).toJSON()
expect(json).toMatchSnapshot()
})
})

运行 yarn test,报错了,意思是测试的文件里引入了 sass文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Test suite failed to run

Configuration error:

Could not locate module ./icon.scss mapped as:
/Users/huangjiaxi/Desktop/react-gulu-test-3/test/__mocks__/object-mock.js.

Please check your configuration for these entries:
{
"moduleNameMapper": {
"/\.(css|less|sass|scss)$/": "/Users/huangjiaxi/Desktop/react-gulu-test-3/test/__mocks__/object-mock.js"
},
"resolver": null
}

1 | import React from 'react';
2 | import './importIcons';
> 3 | import './icon.scss';
| ^
4 | import classes from '../helpers/classes';

解决方案就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 根目录 新建test/__mocks__/file-mock.js
//内容如下
module.exports = 'test-file-stub';


// 如果是 scss 我们要用另外的mock
// 新建test/__mocks__/object-mock.js
//内容如下
module.exports = {}

// 配置 jest

moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/file-mock.js",
"\\.(css|less|sass|scss)$": "<rootDir>/test/__mocks__/object-mock.js",
},

继续 yarn test,又报错了

1
2
3
4
5
6
7
8
9
10
11
12
13
TypeError: require.context is not a function
at Object.<anonymous> (/Users/huangjiaxi/Desktop/react-gulu-test-3/lib/icon/importIcons.js:3:23)


# 因为 importIcons.js里 有个 try catch ,catch里的 console.log(error)
# 删除 console.log(error) 即可

let importAll = (requireContext) => requireContext.keys().forEach(requireContext)
try {
importAll(require.context('../icons/', true, /\.svg$/))
} catch (error) {
console.log(error)
}

继续, yarn test 成功

解释测试代码里的toMatchSnapshot是啥意思

  • 运行文件产生的快照
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
运行 yarn test之后
# 目录 __tests__/__snapshots__/icon.unit.jsx.snap
文件内容如下

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`icon xxx 1`] = `
<svg
className="fui-icon"
>
<use
xlinkHref="#undefined"
/>
</svg>
`;

仔细发现我们没有传递 name

修改 icon.unit.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
describe('icon', () => {
it(' xxx ', () => {
const json = renderer.create(<Icon name="alipay"/>).toJSON()
expect(json).toMatchSnapshot()
})
})

// 再次运行 yarn test ,又报错了,代表这次修改后和上次快照的对比,我们发现这次是正确的
// 区别就是 undefined 变成了 alipay

FAIL lib/icon/__tests__/icon.unit.jsx
icon
✕ xxx (13ms)

● icon › xxx

expect(received).toMatchSnapshot()

Snapshot name: `icon xxx 1`

- Snapshot
+ Received

<svg
className="fui-icon"
>
<use
- xlinkHref="#undefined"
+ xlinkHref="#alipay"
/>
</svg>

6 | it(' xxx ', () => {
7 | const json = renderer.create(<Icon name="alipay"/>).toJSON()
> 8 | expect(json).toMatchSnapshot()
| ^
9 | })
10 | })
11 |

at Object.toMatchSnapshot (lib/icon/__tests__/icon.unit.jsx:8:18)

› 1 snapshot failed.
Snapshot Summary
› 1 snapshot failed from 1 test suite. Inspect your code changes or run `yarn test -u` to update the
m.

最后一句说了,如果你想更新快照运行 yarn test -u

运行 yarn test -u 代表我们把正确的快照保存了下来

此时快照内容如下:

1
2
3
4
5
6
7
8
9
10
11
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`icon xxx 1`] = `
<svg
className="fui-icon"
>
<use
xlinkHref="#alipay"
/>
</svg>
`;

好处是什么,如果你以后手误把 icon.tsx里name的“#”号删除,你运行 yarn test的时候就能得到上次运行成功的快照和本次的对比

  1. 你保存了一个 001版本的运行成功的快照
  2. 下次增加需求的时候,测试时,保证功能还是正确的。
  3. 001版本不通过只有两种可能,一个是版本太老了,另一个是你改出bug了

这就是Snapshot的工作原理

用 jest.fn() 测试点击事件

  • 使用enzyme库
    1
    yarn add --dev enzyme

测试点击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import * as renderer from 'react-test-renderer'
import React from 'react'
import Icon from '../icon'
import {mount} from 'enzyme'

describe('icon', () => {
it('render successfully', () => {
const json = renderer.create(<Icon name="alipay"/>).toJSON()
expect(json).toMatchSnapshot()
})
it('onClick', () => {
let n = 1;
const fn = ()=>{
n = 2;
}
const component = mount(<Icon name="alipay" onClick={fn}/>)
component.find('svg').simulate('click');
expect(n).toEqual(2);
})
})

再次运行 yarn test ,又报错了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
✕ onClick (2ms)

● icon › onClick


Enzyme Internal Error: Enzyme expects an adapter to be configured, but found none.
To configure an adapter, you should call `Enzyme.configure({ adapter: new Adapter() })`
before using any of Enzyme's top level APIs, where `Adapter` is the adapter
corresponding to the library currently being tested. For example:

import Adapter from 'enzyme-adapter-react-15';

To find out more about this, see http://airbnb.io/enzyme/docs/installation/index.html

14 | n = 2;
15 | }
> 16 | const component = mount(<Icon name="alipay" onClick={fn}/>)
| ^
17 | component.find('svg').simulate('click');
18 | expect(n).toEqual(2);
19 | // const fn = jest.fn()



// 意思你该配置一下 Enzyme.configure({ adapter: new Adapter() })

修改 test/setupTests.js

1
2
3
4
const enzyme = require('enzyme')
const Adapter = require('enzyme-adapter-react-16')

enzyme.configure({adapter: new Adapter()})

再次运行 yarn test ,又报错了

1
2
3
4
5
6
7
8
9
10
11
12
# 意思你没有 enzyme-adapter-react-16 这个模块
FAIL lib/__tests__/hello.unit.tsx
● Test suite failed to run

Cannot find module 'enzyme-adapter-react-16' from 'setupTests.js'

1 | const enzyme = require('enzyme')
> 2 | const Adapter = require('enzyme-adapter-react-16')

# 安装它

yarn add --dev enzyme-adapter-react-16@1.11.2

再次运行 yarn test ,成功!!

如果你的测试第一次成功了,不代表成功,你一定要先失败一次,在成功才是真的成功!!!

先改成 333 然后失败,再改成 2 成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
it('onClick', () => {
let n = 1;
const fn = ()=>{
n = 2;
}
const component = mount(<Icon name="alipay" onClick={fn}/>)
component.find('svg').simulate('click');
expect(n).toEqual(333);
})

// 测试失败了

Expected: 333
Received: 2

// 此时修改为正确的,然后继续测试 yarn test
it('onClick', () => {
let n = 1;
const fn = ()=>{
n = 2;
}
const component = mount(<Icon name="alipay" onClick={fn}/>)
component.find('svg').simulate('click');
expect(n).toEqual(2);
})

// 此时成功了

但是,上面的方式有点傻!因为每次都要声明个 “n”

使用jest.fn()来模拟函数测试

1
2
3
4
5
6
it('onClick', () => {
const fn = jest.fn()
const component = mount(<Icon name="alipay" onClick={fn}/>)
component.find('svg').simulate('click')
expect(fn).toBeCalled()
})

期待fn被调用,这样虽然成功了,!但我们需要失败一次,才代表真的成功了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
it('onClick', () => {
const fn = jest.fn()
const fn2 = jest.fn()
const component = mount(<Icon name="alipay" onClick={fn}/>)
component.find('svg').simulate('click')
expect(fn2).toBeCalled()
})
// 期待 fn2被调用,明显会失败
// 报错如下

✕ onClick (37ms)

● icon › onClick

expect(jest.fn()).toBeCalled()

Expected mock function to have been called, but it was not called.

14 | const component = mount(<Icon name="alipay" onClick={fn}/>)
15 | component.find('svg').simulate('click')
> 16 | expect(fn2).toBeCalled()



然后在改回去,运行 yarn test 成功则真的成功了

it('onClick', () => {
const fn = jest.fn()
const component = mount(<Icon name="alipay" onClick={fn}/>)
component.find('svg').simulate('click')
expect(fn).toBeCalled()
})

一个小细节 为什么我点击了之后,fn马上就调用,会不会是异步的呢?

  • 基础知识,在html里事件是同步执行的没有异步,所以你点击之后fn应该马上调用没有异步的问题

IDE 提示找不到 describe 和 it 怎么办?

解决办法:

  1. yarn add -D @types/jest
  2. 在文件开头加一句 import ‘jest’

这是因为 describe 和 it 的定于位于 jest 的类型声明文件中,不信你可以按住 ctrl 并点击 jest 查看。

如果还不行,你需要在 WebStorm 里设置对 jest 的引用:

这是因为 typescript 默认排除了 node_modules 里的类型声明。

代码链接