前端必备的测试

前端必备的测试

DellLee 老师的 前端要学的测试课 从Jest入门到TDD/BDD双实战学习记录。

知识点

Jest 基础

基础API异步测试Mock技巧快照timer测试Dom测试

实现项目

VueVue-test-utilsReactEnzymeTDD+单元测试BDD+集成测试

初识测试

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
49
50
51
52
53
54
55
56
57
58
59
// math.js
function add(a, b) {
return a + b;
}

function minus(a, b) {
return a - b
}

export {
add,
minus
}


// math.test.js
// 简陋测试
import { add, minus } from './math'
let result
let expected
result = add(3, 7)
expected = 10;

if (result !== 10) {
throw Error(`3 + 7 应该等于 ${expected},但结果却是 ${result}`)
}

result = minus(3, 3)
expected = 0;

if (result !== 0) {
throw Error(`3 - 3 应该等于 ${expected},但结果却是 ${result}`)
}
// 封装版
import { add, minus } from './math'
function expect(result) {
return {
toBe: function (actual) {
if (result !== actual) {
throw new Error(`预期值与实际值不相等 预期${actual} 结果却是 ${result}`)
}
}
}
}

function test(desc, fn) {
try {
fn();
console.log(`${desc}通过测试`)
} catch (e) {
console.log(`${desc}没有通过测试 ${e}`)
}
}
test('测试加法3 + 7', () => {
expect(add(3, 7)).toBe(6)
})
test('测试减法6 - 3', () => {
expect(minus(6, 3)).toBe(6)
})

自动化框架:Jest

优点:性能、功能、易用性、速度快、Api简单、易配置、隔离性好、监控模式、IDE整合、Snapshot、多项目并行、覆盖率、Mock丰富

简单配置

1
2
3
4
5
6
# 项目内部调用 jest
npx jest --init
# 选择
browser-like
# 随后选择自动生成报告,自动清除实例在每个test之前,就会生成一下配置文件
jest.config.js

jest.config.js

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html

module.exports = {
// All imported modules in your tests should be mocked automatically
// 自动模拟在测试用例中的所有导入模块,在 __mocks__ 文件夹中寻找
// automock: false,

// Stop running tests after `n` failures
// 默认情况下,Jest运行所有测试并在完成后将所有错误生成到控制台,bil 让 jest 在 n 失败后停止运行测试
// bail: 0,

// Respect "browser" field in package.json when resolving modules
// false => browser | true => node
// browser: false,

// The directory where Jest should store its cached dependency information
// 存放 jest 依赖信息缓存的目录
// cacheDirectory: "C:\\Users\\Administrator\\AppData\\Local\\Temp\\jest",

// Automatically clear mock calls and instances between every test
// 自动清除模拟调用和实例在每次测试之间
clearMocks: true,

// Indicates whether the coverage information should be collected while executing the test
// 是否收集测试时的覆盖率信息,因为要带上覆盖率搜集语句访问所有执行过的文件,这可能会让测试执行速度明显减慢
// collectCoverage: false,

// An array of glob patterns indicating a set of files for which coverage information should be collected
// 指示应收集覆盖率信息的全局模式一组文件,即使文件不存在测试,也将为其收集覆盖率信息,并且测试套件中不需要它
// collectCoverageFrom: null,

// The directory where Jest should output its coverage files
// jest 输出测速覆盖率文件的目录
// 运行 npx jest --coverage
coverageDirectory: "coverage",

// An array of regexp pattern strings used to skip coverage collection
// 忽略测试的文件路径的正则匹配
// coveragePathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],

// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],

// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: null,

// A path to a custom dependency extractor
// dependencyExtractor: null,

// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,

// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],

// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: null,

// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: null,

// A set of global variables that need to be available in all test environments
// globals: {},

// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",

// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],

// An array of file extensions your modules use
// 模块使用文件扩展名数组,当你导入的文件没有扩展名的时候,它会在这个数组里面去自动匹配
// moduleFileExtensions: [
// "js",
// "json",
// "jsx",
// "ts",
// "tsx",
// "node"
// ],

// A map from regular expressions to module names that allow to stub out resources with a single module
// 模块名映射,类 webpack alias 以及 jsconfig.js 的 compilerOptions.paths
// moduleNameMapper: {},

// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],

// Activates notifications for test results
// notify: false,

// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",

// A preset that is used as a base for Jest's configuration
// preset: null,

// Run tests from one or more projects
// projects: null,

// Use this configuration option to add custom reporters to Jest
// reporters: undefined,

// Automatically reset mock state between every test
// resetMocks: false,

// Reset the module registry before running each individual test
// resetModules: false,

// A path to a custom resolver
// resolver: null,

// Automatically restore mock state between every test
// restoreMocks: false,

// The root directory that Jest should scan for tests and modules within
// rootDir: null,

// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],

// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",

// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],

// A list of paths to modules that run some code to configure or set up the testing framework before each test
// 运行做测试的时候使用某些垫片为运行环境做兼容
// setupFilesAfterEnv: [],

// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// 快照格式化
// snapshotSerializers: [],

// The test environment that will be used for testing
// testEnvironment: "jest-environment-jsdom",

// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},

// Adds a location field to test results
// testLocationInResults: false,

// The glob patterns Jest uses to detect test files
// 匹配测试文件
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],

// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],

// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],

// This option allows the use of a custom results processor
// testResultsProcessor: null,

// This option allows use of a custom test runner
// testRunner: "jasmine2",

// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// 模拟浏览器的地址
// testURL: "http://localhost",

// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",

// A map from regular expressions to paths to transformers
// 不同文件类型对应不同的转换器
// transform: null,

// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],

// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,

// Indicates whether each individual test should be reported during the run
// verbose: null,

// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],

// Whether to use watchman for file crawling
// watchman: true,
};

babelrc

jest 未配置转换时,默认只支持 commonjs 语法。

1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}

jest转换(commonjs -> es module)大概工作原理:

  1. npm run jest
  2. jest(babel-jest)jest 内部的
  3. 检测是有 babel-core
  4. 拿到 .babelrc 配置
  5. 再运行测试之前,结合 babel 把代码做一次转化
  6. 运行转化过的测试用例

常用匹配器(Matchers)

数字

.toBe()

.toEqual(value)

.toBeGreaterThan(number)

.toBeGreaterThanOrEqual(number)

.toBeLessThan(number)

.toBeLessThanOrEqual(number)

.toBeCloseTo(number,numDigits?)

第二个参数为精度,代表几位小数点,默认为2位

.toBeNaN()

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
49
50
// 数字相关
test('匹配器:toBe-数字相等', () => {
// toBe 匹配器 matchers Object.is() ===
const a = 10
expect(a).toBe(10)
})

test('匹配器:toEqual-内容相等', () => {
// toEqual 匹配器 matchers
const a = { one: 1 }
expect(a).toEqual({ one: 1 })
})

test('匹配器:toBeGreaterThan-大于比较数', () => {
// toBeGreaterThan 匹配器 matchers
const a = 4
expect(a).toBeGreaterThan(3)
})

test('匹配器:toBeGreaterThanOrEqual-大于等于比较数', () => {
// toBeGreaterThanOrEqual 匹配器 matchers
const a = 4
expect(a).toBeGreaterThanOrEqual(4)
})

test('匹配器:toBeLessThan-小于比较数', () => {
// toBeLessThan 匹配器 matchers
const a = 4
expect(a).toBeLessThan(5)
})

test('匹配器:toBeLessThanOrEqual-小于等于比较数', () => {
// toBeLessThanOrEqual 匹配器 matchers
const a = 4
expect(a).toBeLessThanOrEqual(4)
})

// 浮点数
test('匹配器:toBeCloseTo-两个浮点数字相加', () => {
// toBeCloseTo 匹配器 matchers
const value = 0.1 + 0.2;
// expect(value).toBe(0.3); 这句会报错,因为浮点数有舍入误差
expect(value).toBeCloseTo(0.3); // 这句可以运行
});

// NaN
test('匹配器:toBeNaN-等于NaN', () => {
expect(NaN).toBeNaN();
expect(1).not.toBeNaN();
});

真假

  • toBeNull只匹配 null
  • toBeUndefined只匹配 undefined
  • toBeDefinedtoBeUndefined相反
  • toBeTruthy 匹配任何 if 语句为真
  • toBeFalsy匹配任何 if 语句为假

.toBeNull()

.toBeUndefined()

.toBeDefined()

.toBeTruthy()

.toBeFalsy()

在JavaScript中,有六个falsy值:false0''nullundefined,和NaN。其他一切都是真实的。

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
// 真假相关
test('匹配器:toBeNull-与null相等', () => {
// toBeNull 匹配器 matchers
const a = null
expect(a).toBeNull()
})

test('匹配器:toBeUndefined-与undefined相等', () => {
// toBeUndefined 匹配器 matchers
const a = undefined
expect(a).toBeUndefined()
})

test('匹配器:toBeDefined-被定义过的,非 undefined 的,可为 null', () => {
// toBeDefined 匹配器 matchers
const a = null
expect(a).toBeDefined()
})

test('匹配器:toBeTruthy-真值或者隐藏为true的', () => {
// toBeTruthy 匹配器 matchers
const a = 1
expect(a).toBeTruthy()
})

test('匹配器:toBeFalsy-真值或者隐藏为false的', () => {
// toBeFalsy 匹配器 matchers
const a = null
expect(a).toBeFalsy()
})

否定

.not

1
2
3
4
5
test('匹配器:not-不是xx', () => {
// not 匹配器 matchers
const a = 1
expect(a).not.toBeFalsy()
})

expect.not.arrayContaining(array)

匹配不是接收值的子集

1
2
3
4
5
6
7
8
describe('not.arrayContaining', () => {
const expected = [1];
it('如果接收的数组不包含1就通过测试', () => {
expect([2, 3, 4]).toEqual(
expect.not.arrayContaining(expected)
)
})
})

expect.not.objectContaining(object)

匹配不包含某个对象

1
2
3
4
5
6
7
8
describe('not.objectContaining', () => {
const expected = { haha: 'laibh.top' };
it('如果接收的对象不包含{ haha: laibh.top }就通过测试', () => {
expect({ haha: 'laibh.top1' }).toEqual(
expect.not.objectContaining(expected)
)
})
})

expect.not.stringContaining(string)

匹配不包含某个字符串

1
2
3
4
5
6
7
describe('not.stringContaining', () => {
const expected = '赖同学';

it('如果接收的字符串不完全等于赖同学就通过测试', () => {
expect('赖').toEqual(expect.not.stringContaining(expected));
});
});

expect.not.stringMatching(string|regexp)

同上,不过参数可以是正则表达式

1
2
3
4
5
6
7
describe('not.stringMatching', () => {
const expected = /赖同学/;

it('如果接收的字符串赖同学就通过测试', () => {
expect('赖').toEqual(expect.not.stringMatching(expected));
});
});

字符串

.toMatch(regexpOrString)

1
2
3
4
5
6
7
8
// 字符串
test('匹配器:toMatch-正则字符串匹配', () => {
// toMatch 匹配器 matchers
const str = 'http://laibh.top'
expect(str).toMatch('laibh')
// 使用正则
expect(str).toMatch(/laibh/)
})

expect.stringContaining(string)

匹配包含某个字符串

expect.stringMatching(string|regexp)

匹配字符串,可用正则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
describe('stringMatching in arrayContaining', () => {
const expected = [
expect.stringMatching(/^Alic/),
expect.stringMatching(/^[BR]ob/),
];
it('matches even if received contains additional elements', () => {
expect(['Alicia', 'Roberto', 'Evelina']).toEqual(
expect.arrayContaining(expected),
);
});
it('does not match if received does not contain expected elements', () => {
expect(['Roberto', 'Evelina']).not.toEqual(
expect.arrayContaining(expected),
);
});
})

数组

expect.toContain()

1
2
3
4
5
6
7
8
9

// 数组
test('匹配器:toContain-数组包含某项', () => {
// toContain 匹配器 matchers
const arr = ['lai', 'bin', 'hong']
const set = new Set(arr)
expect(set).toContain('lai')
expect(arr).toContain('lai')
})

expect.arrayContaining(array)

匹配子集

1
2
3
4
5
6
7
8
9
describe('arrayContaining', () => {
const expected = ['1', '2'];
it('即使接收值包含其他参数也匹配', () => {
expect(['1', '2', '3']).toEqual(expect.arrayContaining(expected));
});
it('只要接收值不包含期望的值就不匹配', () => {
expect(['2', '4']).not.toEqual(expect.arrayContaining(expected));
});
});

对象

objectContaining(object)

匹配任何递归预期属性的接收对象

1
2
3
4
5
6
7
8
9
10
test('测试onPress函数回调参数匹配对象', () => {
const onPress = jest.fn();
simulatePresses(onPress)
expect(onPress).toBeCalledWith(
expect.objectContaining({
x: expect.any(Number),
y: expect.any(Number)
})
)
})

.toHaveProperty(keyPath,value?)

检查对象中各种属性存在和值,第二个参数是可选的

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
// Object containing house features to be tested
const houseForSale = {
bath: true,
bedrooms: 4,
kitchen: {
amenities: ['oven', 'stove', 'washer'],
area: 20,
wallColor: 'white',
'nice.oven': true,
},
'ceiling.height': 2,
};

test('this house has my desired features', () => {
// Simple Referencing
expect(houseForSale).toHaveProperty('bath');
expect(houseForSale).toHaveProperty('bedrooms', 4);

expect(houseForSale).not.toHaveProperty('pool');

// 嵌套深层用 .
expect(houseForSale).toHaveProperty('kitchen.area', 20);
expect(houseForSale).toHaveProperty('kitchen.amenities', [
'oven',
'stove',
'washer',
]);

expect(houseForSale).not.toHaveProperty('kitchen.open');

// 嵌套深层用 []
expect(houseForSale).toHaveProperty(['kitchen', 'area'], 20);
expect(houseForSale).toHaveProperty(
['kitchen', 'amenities'],
['oven', 'stove', 'washer'],
);
expect(houseForSale).toHaveProperty(['kitchen', 'amenities', 0], 'oven');
expect(houseForSale).toHaveProperty(['kitchen', 'nice.oven']);
expect(houseForSale).not.toHaveProperty(['kitchen', 'open']);

// Referencing keys with dot in the key itself
expect(houseForSale).toHaveProperty(['ceiling.height'], 'tall');
});

.toMatchObject(object)

检查对象的属性的子集相匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const houseForSale = {
bath: true,
bedrooms: 4,
kitchen: {
amenities: ['oven', 'stove', 'washer'],
area: 20,
wallColor: 'white',
},
};
const desiredHouse = {
bath: true,
kitchen: {
amenities: ['oven', 'stove', 'washer'],
wallColor: expect.stringMatching(/white|yellow/),
},
};

test('the house has my desired features', () => {
expect(houseForSale).toMatchObject(desiredHouse);
});

.toBeInstanceOf(Class)

检查对象是一个类的实例

1
2
3
4
5
class A {}

expect(new A()).toBeInstanceOf(A);
expect(() => {}).toBeInstanceOf(Function);
expect(new A()).toBeInstanceOf(Function); // throws

异常

.toThrow(error?)

1
2
3
4
5
6
7
8
9
10
11
const throwNewErrorFunc = () => { throw new Error('this is a new error') }

// 异常
test('匹配器:toThrow-测试抛出异常', () => {
// toThrow 匹配器 matchers
expect(throwNewErrorFunc).toThrow()
// 测试抛出的内容
expect(throwNewErrorFunc).toThrow('this is a new error')
// 表达式也行
expect(throwNewErrorFunc).toThrow(/this is a new error/)
})

任意

expect.anything()

匹配除了 nullundefined的任意值,可以它使用在 toEqual或者 toBeCalledWith里面替代文字值

1
2
3
4
5
test('map 遍历一个非空的参数',()=>{
const mock = jest.fn();
[1].map(x => mock(x))
expect(mock).toBeCalledWith(expect.anything())
})

expect.any(constructor)

匹配任意构造器生成的实例

1
2
3
4
5
6
7
8
function randocall(fn){
return fn(Math.floor(Math.random()*6 + 1))
}
test('测试 randocall的回调函数调用了一个数字',()=>{
const mock = jest.fn();
randocall(mock)
expect(mock).toBeCalledWith(expect.any(Number))
})

.toHaveLength(number)

检查对象有个 length 属性并将设为某一数值。这对于检查数组或字符串大小特别有用。

1
2
3
expect([1, 2, 3]).toHaveLength(3);
expect('abc').toHaveLength(3);
expect('').not.toHaveLength(5);

.toContain(item)

检查项目在数组或者字符串是否是另一个字符串的子串

1
2
3
test('the flavor list contains lime', () => {
expect(getAllFlavors()).toContain('lime');
});

.toContainEqual(item)

检查具有特定结构和值的元素是否包含在数据中

1
2
3
4
it('test contain', () => {
const testValue = { bol: true }
expect([{ bol: true }, { sour: false }]).toContainEqual(testValue)
})

异步相关

expect.assertions()

匹配在测试用例里面使用断言的次数。确保在异步函数为了确保在回调函数里面断言被调用特别有用。

1
2
3
4
5
6
7
8
9
10
11
test('异步调用所有回调',()=>{
// 确保有两个断言被调用
expect.assertions(2)
function callback1(data){
expect(data).toBeTruthy()
}
function callback2(data){
expect(data).toBeTruthy();
}
doAsync(callback1,callback2)
})

expect.hasAssertions()

匹配在测试用例里面至少使用一次断言

.resolves

使用 resolves解开 fulfilled promise,如果 reject promise,则断言失败

1
2
3
4
test('resolves to lemon', () => {
// make sure to add a return statement
return expect(Promise.resolve('lemon')).resolves.toBe('lemon');
});

测试是异步的,需要告诉 jest 等待返回解开的断言

也可以用 async/await结合 .resolves

1
2
3
4
test('resolves to lemon', async () => {
await expect(Promise.resolve('lemon')).resolves.toBe('lemon');
await expect(Promise.resolve('lemon')).resolves.not.toBe('octopus');
});

.rejects

使用 rejects解开 rejected promise,如果 fulfilled promise,则断言失败

1
2
3
4
test('rejects to octopus', async () => {
// make sure to add a return statement
return expect(Promise.reject(new Error('octopus'))).rejects.toThrow('octopus');
});

测试是异步的,需要告诉 jest 等待返回解开的断言

同样使用 async/await结合 .rejects

1
2
3
4
test('rejects to octopus', async () => {
await expect(Promise.reject(new Error('octopus'))).rejects.toThrow('octopus');
await expect(Promise.reject(new Error('octopus'))).rejects.not.toThrow('lemon');
});

函数调用

.toHaveBeenCalled()|.toBeCalled()

确保模拟功能得到调用

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
function drinkAll(cb, flavour) {
if (flavour !== 'octopus') {
cb(flavour)
}
}

describe('test drinkAll function', () => {
test('drink somthing lemon-flavour', () => {
const drink = jest.fn();
drinkAll(drink, 'lemon')
expect(drink).toBeCalled();
})
})

describe('test drinkAll function', () => {
test('does not drink somthing octopus-flavour', () => {
const drink = jest.fn();
drinkAll(drink, 'octopus')
expect(drink).not.toBeCalled();
})
})

describe('test drinkAll function', () => {
test('drink somthing lemon-flavour', () => {
const drink = jest.fn();
drinkAll(drink, 'lemon')
expect(drink).toHaveBeenCalled();
})
})

describe('test drinkAll function', () => {
test('does not drink somthing octopus-flavour', () => {
const drink = jest.fn();
drinkAll(drink, 'octopus')
expect(drink).not.toHaveBeenCalled();
})
})

.toHaveBeenCallTimes(number)|.toBeCalledTimes(number)

确保模拟功能得到调用次数与指定数字一致

1
2
3
4
5
6
test('drinkEach drinks each drink', () => {
const drink = jest.fn();
drinkEach(drink, ['lemon', 'octopus']);
expect(drink).toHaveBeenCalledTimes(2);
expect(drink).toBeCalledTimes(2);
});

.toHaveBeenCalledWith(arg1,arg2,…)|.toBeCalledWith(arg1,arg2,…)

确保模拟功能被调用的具体参数

1
2
3
4
5
6
7
8
9
10
function calledWithArg(cb) {
cb('Arg')
}

test('test calledWithArg', () => {
const fn = jest.fn();
calledWithArg(fn);
expect(fn).toBeCalledWith('Arg');
expect(fn).toHaveBeenCalledWith('Arg');
});

.toHaveBeenLastCalledWith(arg1,arg2,…)|.lastCalledWith(arg1, arg2, …)

确保模拟功能被最后一次调用的具体参数

.toHaveBeenNthCalledWith(nthCall, arg1, arg2, ….)|.nthCalledWith(nthCall, arg1, arg2, ….)

确保模拟功能多次调用的顺序

1
2
3
4
5
6
7
8
test('drinkEach drinks each drink', () => {
const drink = jest.fn();
drinkEach(drink, ['lemon', 'octopus']);
expect(drink).toHaveBeenNthCalledWith(1, 'lemon');
expect(drink).toHaveBeenNthCalledWith(2, 'octopus');
expect(drink).nthCalledWith(1, 'lemon');
expect(drink).nthCalledWith(2, 'octopus');
});

n 必须是从 1开始的正整数

.toHaveReturned()|.toReturn()

测试模拟函数成功返回(即没有抛出错误)至少一次

1
2
3
4
5
6
test('test calledWithArg', () => {
const fn = jest.fn();
calledWithArg(fn);
expect(fn).toHaveReturned();
expect(fn).toReturn();
});

.toHaveReturnedTimes(number)|.toReturnTimes(number)

确保模拟函数返回成功的次数,抛出错误的模拟函数的任何调用都不计入函数返回的次数

1
2
3
4
5
6
7
8
test('drink returns twice', () => {
const drink = jest.fn(() => true);

drink();
drink();

expect(drink).toHaveReturnedTimes(2);
});

.toHaveReturnedWith(value)|.toReturnWith(value)

确保模拟函数返回特定的值

1
2
3
4
5
6
test('test Return 123', () => {
const fn = jest.fn(() => 123);
calledWithArg(fn);
expect(fn).toHaveReturnedWith(123)
expect(fn).toReturnWith(123)
});

.toHaveLastReturnedWith(value)|.lastReturnedWith(value)

确保模拟函数最后一次返回特定的值

.toHaveNthReturnedWith(nthCall, value)|.nthReturnedWith(nthCall, value)

确保模拟函数第n次调用返回特定的值

第n个参数必须是从1开始的正整数。

自定义扩展

expect.extend()

pass表示是否有匹配,message提供一个没有参数的函数,在出现错误的情况下返回消息。当pass:falsemessage返回 expect(x).matcher()失败的错误信息,pass:truemessage返回当expect(x).not.matcher()失败时的错误消息

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
// expect.extend(matchers)
expect.extend({
toBeWithinRange(reveived, floor, ceiling) {
const pass = reveived >= floor && reveived <= ceiling;
if (pass) {
return {
message: () => `期待 ${reveived} 不在范围${floor}-${ceiling}内`,
pass,
}
} else {
return {
message: () => `期待 ${reveived} 在范围${floor}-${ceiling}内`,
pass,
}
}
}
})

test('测试范围', () => {
expect(100).toBeWithinRange(90, 100)
expect(101).not.toBeWithinRange(0, 100)
expect({ apples: 6, bananas: 3 }).toEqual({
apples: expect.toBeWithinRange(1, 10),
bananas: expect.not.toBeWithinRange(11, 20),
})
})

异步扩展,需要结合 asyncawait函数来使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
expect.extend({
async toBeDivisibleByExternalValue(reveived){
// 异步获取的除数
const externalValue = await getExternalValueFromRemoteSource();
const pass = received % externalValue == 0;
if(pass){
return{
message:()=>`期待${received}不被${externalValue}整除`,
pass
}
}else{
return{
message:()=>`期待${received}${externalValue}整除`,
pass
}
}
}
})

更多

测试异步代码

回调形式

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
// fetchData.js
import request from '@/utils/request';

const fetchData = (cb) => {
request('http://www.dell-lee.com/react/api/demo.json').then(res => {
if (res.data) cb(res.data)
})
}

export default fetchData
// fetchData.test.js
import fetchData from './fetchData'

// 错误
test('fetch 返回结果为 {success: true}', () => {
fetchData(data => {
expect(data).toEqual({
success: true
})
})
})

// 回调类型异步函数
test('fetch 返回结果为 {success: true}', (done) => {
fetchData(data => {
expect(data).toEqual({
success: true
})
done();
})
})

Promise

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// fetchData.js
import request from '@/utils/request';
const fetchData = () => request('http://www.dell-lee.com/react/api/demo.json')
export default fetchData

//fetchData.test.js

import fetchData from './fetchData'

// Promise
test('fetch 返回结果为 {success: true}', () => {
return fetchData().then(res => {
expect(res.data).toEqual({ success: true })
})
})
// 测试404
test('fetchData 返回结果为404', () => {
// 要求至少跑一次 expect
expect.assertions(1);
return fetchData().catch(e => {
expect(e.toString().indexOf('404') > -1).toBe(true)
})
})

// .resolves / .rejects
test('fetch 返回结果为 {success: true}', () => {
return expect(fetchData()).resolves.toMatchObject({
data: {
success: true
}
})
})

test('fetchData 返回结果为404', () => {
return expect(fetchData()).rejects.toThrow()
})

// Async/Await
test('fetch 返回结果为 {success: true}', async () => {
await expect(fetchData()).resolves.toMatchObject({
data: {
success: true
}
})
})

test('fetchData 返回结果为404', async () => {
await expect(fetchData()).rejects.toThrow()
})
// 另一种 Async/Await
test('fetch 返回结果为 {success: true}', async () => {
const res = await fetchData()
expect(res.data).toEqual({
success: true
})
})

test('fetchData 返回结果为404', async () => {
expect.assertions(1);
try {
await fetchData()
} catch (e) {
await expect(e.toString()).toEqual('Error: Request failed with status code 404')
}
})

钩子函数

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// Couter.js
class Counter {
constructor() {
this.number = 0;
}

addOne() {
this.number += 1;
}

addTwo() {
this.number += 2;
}

minusOne() {
this.number -= 1;
}

minusTwo() {
this.number -= 2;
}

}

export default Counter;
// Couter.test.js

import Counter from './Couter';


describe('测试 Counter',()=>{
let couter;

// 所有测试开始之前
beforeAll(()=>{
couter = new Counter();
})

// 每个测试用例开始之前
beforeEach(()=>{
// 每次测试都会生成一个新的 couter
couter = new Counter();
})

// 每个测试用例结束之后
afterEach(()=>{

})

// 所有测试结束之后
afterAll(()=>{

})

describe('测试增加相关代码',()=>{
test('测试 Conter 中的 addOne 方法', () => {
couter.addOne();
expect(couter.number).toBe(1)
})
test('测试 Conter 中的 addTwo 方法', () => {
couter.addTwo();
expect(couter.number).toBe(2)
})
})

describe('测试减少相关代码',()=>{
test('测试 Conter 中的 minusOne 方法', () => {
couter.minusOne();
expect(couter.number).toBe(-1)
})

test('测试 Conter 中的 minusTwo 方法', () => {
couter.minusTwo();
expect(couter.number).toBe(-2)
})
})

})

describe 里面就是一个作用域,嵌套作用域可以有多个钩子函数,钩子函数执行顺序由外到内

test.only,只执行某个测试用例

1
2
3
4
5
6
7
8
9
10
11
describe('测试减少相关代码',()=>{
test.only('测试 Conter 中的 minusOne 方法', () => {
couter.minusOne();
expect(couter.number).toBe(-1)
})

test('测试 Conter 中的 minusTwo 方法', () => {
couter.minusTwo();
expect(couter.number).toBe(-2)
})
})

Mock

  1. 捕获函数的调用,this 指向,调用顺序
  2. 自由设置返回结果
  3. 改变内部函数的实现

基本模拟-导入函数

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
// demo
const runCallback = (cb) => {
cb();
}
export default runCallback

//demo.test.js
import runCallback from './demo';


describe('测试 runCallback 方法', () => {
test('runCallback被调用,并只调用了一次',()=>{
// mock 函数,捕获函数的调用
const func = jest.fn();
// 方法返回一次 值 Haha,mockReturnValue则是都返回,也可以在 jest.fn(()=>{return 'Haha'})定义
func.mockReturnValueOnce('Haha')
runCallback(func)
expect(func).toBeCalled()
expect(func.mock.calls.length).toBe(1)
expect(func.mock.results[0].value).toBe('Haha')
})
})
console.log(func.mock)
/**
● Console

console.log src/lesson2/demo.test.js:14
{ calls: [ [] ],
instances: [ undefined ],
invocationCallOrder: [ 1 ],
results: [ { type: 'return', value: 'Haha' } ] }
*/

修改原来函数的返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// xx.js
import axios from 'axios'

const fetchData = ()=>{
axios.get('/').then(res=>res.data)
}
// (function(){return '123' })()

export default fetchData;
// xx.test.js

import fetchData from './xx.js'
import axios from 'axios'

ject.mock('axios')

test('fetchData 测试',()=>{
axios.get.mockResolvedValue({
data:"(function(){return '123' })()"
})
return fetchData().then(data=>{
expect(eval(data)).toEqual('123')
})
})

__mocks__文件夹

1
2
3
4
5
6
// __mock__/xx.js
const fetchData = ()=>{
return new Promise((resolved,reject)=>{
resolve("(function(){return '123' })()")
})
}

接着改测试,模拟函数

1
2
3
4
5
6
7
8
9
10
11
12
// xx.test.js
jest.mock('./xx')
import fetchData from './xx.js'

test('fetchData 测试',()=>{
axios.get.mockResolvedValue({
data:"(function(){return '123' })()"
})
return fetchData().then(data=>{
expect(eval(data)).toEqual('123')
})
})

上述函数顶部那步模拟也可以通过在 jest.config.js 中修改配置 automock:true 改为自动模拟,那么引入对应的函数的时候就会自动去 __mocks__文件夹里面去寻找对应的模拟函数。注意一旦开启这个配置,需要启动才会生效,另外也会导致很多测试需要重新修改。

同时存在__mocks__以及导入函数

在原来 xx.js 基础上面新增 一个 getNumber函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xx.js
import axios from 'axios'

const fetchData = ()=>{
axios.get('/').then(res=>res.data)
}
// (function(){return '123' })()

const getNumber = ()=> 123

export {
fetchData,
getNumber
};

接着修改测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// xx.test.js
jest.mock('./xx')
import fetchData from './xx.js'

test('fetchData 测试',()=>{
axios.get.mockResolvedValue({
data:"(function(){return '123' })()"
})
return fetchData().then(data=>{
expect(eval(data)).toEqual('123')
})
})
test('getNumber 测试',()=>{
expect(getNumber()).toBe(123)
})

上述代码会报错,getNumebr 找不到对应的函数,因为测试用例还是会去__mocks__ 文件夹去寻找 getNumber。但是我们只希望模拟异步的函数,对于同步函数希望通过导入的方式来测试,那么就从原来的js文件中导入 getNumber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// xx.test.js
jest.mock('./xx')
import fetchData from './xx.js'
const { getNumber } = jest.requireActual('./xx')
test('fetchData 测试',()=>{
axios.get.mockResolvedValue({
data:"(function(){return '123' })()"
})
return fetchData().then(data=>{
expect(eval(data)).toEqual('123')
})
})
test('getNumber 测试',()=>{
expect(getNumber()).toBe(123)
})

Mock Timer

写一个 setTimeout 函数

1
2
3
4
5
6
// timer.js
export default (cb)=>{
setTimeout(()=>{
cb()
},3000)
}

测试用例

1
2
3
4
5
6
7
8
9
// timer.test.js
import timer from './timer'

test('timer 测试',(done)=>{
timer(()=>{
expect(2).toBe(1)
done();
})
})

上面的测试用例会直接通过, timer是一个异步函数,并不会执行函数体内的内容,需要像之前的异步函数一样,加个 done 参数

1
2
3
4
5
6
7
8
import timer from './timer'

test('timer 测试',(done)=>{
timer(()=>{
expect(2).toBe(1)
done();
})
})

接着测试用例便会运行,并报错

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
FAIL  src/lesson3/timer.test.js (7.997s)
● Console

console.error node_modules/_jsdom@11.12.0@jsdom/lib/jsdom/virtual-console.js:29
Error: Uncaught [Error: expect(received).toBe(expected) // Object.is equality

Expected: 1
Received: 2]
at reportException (F:\赖彬鸿\git-project\usual\Egret-Project\FontendTest\node_modules\_jsdom@11.12.0@jsdom\lib\jsdom\living\helpers\runtime-script-errors.js:66:24)
at Timeout.callback [as _onTimeout] (F:\赖彬鸿\git-project\usual\Egret-Project\FontendTest\node_modules\_jsdom@11.12.0@jsdom\lib\jsdom\browser\Window.js:680:7)
at ontimeout (timers.js:436:11)
at tryOnTimeout (timers.js:300:5)
at listOnTimeout (timers.js:263:5)
at Timer.processTimers (timers.js:223:10) { Error: expect(received).toBe(expected) // Object.is equality

Expected: 1
Received: 2
at toBe (F:\赖彬鸿\git-project\usual\Egret-Project\FontendTest\src\lesson3\timer.test.js:5:15)
at cb (F:\赖彬鸿\git-project\usual\Egret-Project\FontendTest\src\lesson3\timer.js:3:5)
at Timeout.callback [as _onTimeout] (F:\赖彬鸿\git-project\usual\Egret-Project\FontendTest\node_modules\_jsdom@11.12.0@jsdom\lib\jsdom\browser\Window.js:678:19)
at ontimeout (timers.js:436:11)
at tryOnTimeout (timers.js:300:5)
at listOnTimeout (timers.js:263:5)
at Timer.processTimers (timers.js:223:10)
matcherResult:
{ actual: 2,
expected: 1,
message: [Function],
name: 'toBe',
pass: false } }

也可以通过模拟 timer 这类异步函数,来达到目的

1
2
3
4
5
6
7
8
9
10
11
12
// timer.test.js

import timer from './timer'
// mock timer
jest.useFakeTimers()
test('timer 测试',()=>{
const fn = jest.fn();
timer(fn)
// 快速运行所有Timer
jest.runAllTimers();
expect(fn).toHaveBeenCalledTimes(1)
})

对于嵌套 timer 异步函数,jest.runOnlyPendingTimers可以让只最外层的第一个 timer 运行。

另外还有快进时间的api,jest.advanceTimersByTime(n)

1
2
3
4
5
6
7
8
9
10
// timer.test.js
import timer from './timer'

jest.useFakeTimers()
test('timer 测试',()=>{
const fn = jest.fn();
timer(fn)
jest.advanceTimersByTime(3000)
expect(fn).toHaveBeenCalledTimes(1)
})

嵌套 timer 的测试用例结合 钩子beforeEach以及两个上面讲的api的例子

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
// timer.js
export default (cb)=>{
setTimeout(()=>{
cb()
setTimeout(()=>{
cb()
},3000)
},3000)
}
// timer.test.js
import timer from './timer'

beforeEach(()=>{
jest.useFakeTimers()
})


test('timer 测试-runAllTimers',()=>{
const fn = jest.fn();
timer(fn)
jest.runAllTimers();
expect(fn).toHaveBeenCalledTimes(2)
})
test('timer 测试-advanceTimersByTime',()=>{
const fn = jest.fn();
timer(fn)
jest.advanceTimersByTime(3000)
expect(fn).toHaveBeenCalledTimes(1)
jest.advanceTimersByTime(3000)
expect(fn).toHaveBeenCalledTimes(2)
})

Mock Funtions

Mock 函数可以轻松测试代码之间的连接——实现方式包括:擦除函数实际实现、捕获对函数的调用(以及在这些调用中传递的参数)、在使用 new 实例化时捕获构造函数的实例,允许测试时配置返回值

模拟函数

测试函数 forEach的内部实现,这个函数为传入的数组中的每个元素调用一次回调函数

1
2
3
4
5
function forEach(items,callback){
for(let index=0;index<items.length;index+=1){
callback(item[index])
}
}

为了测试此函数,可以使用一个 mock 函数,然后检查 mock 函数的状态来确保回调函数如期调用

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
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback)

// 此mock函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2)
console.log(mockCallback)
/**
{
[Function: mockConstructor]
_isMockFunction: true,
getMockImplementation: [Function],
mock: [Getter/Setter],
mockClear: [Function],
mockReset: [Function],
mockRestore: [Function],
mockReturnValueOnce: [Function],
mockResolvedValueOnce: [Function],
mockRejectedValueOnce: [Function],
mockReturnValue: [Function],
mockResolvedValue: [Function],
mockRejectedValue: [Function],
mockImplementationOnce: [Function],
mockImplementation: [Function],
mockReturnThis: [Function],
mockName: [Function],
getMockName: [Function]
}
*/

// 第一次调用函数时的第一个参数是 0
expect(mockCallback.mock.calls[0][0]).toBe(0)
console.log(mockCallback.mock)
/**
{
calls: [ [ 0 ], [ 1 ] ],
instances: [ undefined, undefined ],
invocationCallOrder: [ 1, 2 ],
results:
[ { type: 'return', value: 42 }, { type: 'return', value: 43 } ]
}
*/
// 第二次调用函数时的第一个参数是1
expect(mockCallback.mock.calls[1][0]).toBe(1)

// 第一次函数调用的返回值是 42
expect(mockCallback.mock.results[0].value).toBe(42)

.mock属性

所有 mock 函数都有这个特殊的 .mock属性,它保存了关于此函数如何调用、调用时的返回值的信息。.mock属性还追踪每次调用时 this的值,所以我们同样也可以检视(inspect)this

1
2
3
4
5
6
7
8
const myMock = jest.fn();
const a = new myMock();
const b = {};
const bound = myMock.bind(b)
bound();

console.log(myMock.mock.instances)
// [ mockConstructor {}, {} ]

模拟返回值

1
2
3
4
5
6
7
8
9
10
const myMock = jest.fn();
console.log(myMock())
// undefined

myMock
.mockReturnValueOnce(10)
.mockReturnValueOnce('x')
.mockReturnValue(true)
console.log(myMock(), myMock(), myMock(), myMock());
// 10, 'x', true, true

结合一些函数灵活模拟

1
2
3
4
5
6
7
8
9
10
const filterTestFn = jest.fn();

filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false)

const result = [11,12].filter(filterTestFn)

console.log(result)
// [11]
console.log(filterTestFn.mock.calls)
// [[11],[12]]

模拟模块

1
2
3
4
5
6
7
8
9
// users.js
import axios from 'axios'

class Users {
static call(){
return axios.get('/user.json').then(res=>res.data)
}
}
export default Users

模拟 axios.get 返回一个假的 response

1
2
3
4
5
6
7
8
9
10
11
// users.test.js
import axios from 'axios';
import Users from './users';

jest.mock(axios);
test('should fetch users',()=>{
const users = [{name:'Bob'}]
const res = {data:users}
axios.get.mockResolvedValue(res)
return Users.all().then(data=>expect(data).toEqual(users))
})

模拟实现

通过模拟函数 jest.fn或者mockImplementationOnce方法来完成

1
2
3
4
const myMockFn = jest.fn(cb=> cb(null,true))

myMockFn((err,val)=>console.log(val))
// true

mockImplementation当您需要定义从另一个模块创建的模拟函数的默认实现时,该方法很有用

1
2
3
4
5
6
7
8
9
10
11
12
// foo.js
module.exports = function(){
// some implementation
}
// test.js

jest.mock('../foo') // 这个会自动模拟
const foo = require('../foo')

foo.mockImplementation(()=>42)
foo();
// 42

当需要重新创建模拟函数的复杂行为,以便多个函数调用产生不同的结果时,可以使用 mockImplementationOnce方法

1
2
3
4
5
6
7
8
const myMockFn = jest
.fn()
.mockImplementationOnce(cb => cb(null,true))
.mockImplementationOnce(cb => cb(null,fase))
myMockFn((err, val) => console.log(val));
// true
myMockFn((err, val) => console.log(val));
// false

.mockReturnThis()返回 this

1
2
3
4
5
6
7
8
9
const myObj = {
myMethod: jest.fn().mockReturnThis(),
}
// 与下面实现相同
const otherObj = {
myMethod: jest.fn(function(){
return this;
})
}

模拟函数名称

选择为模拟函数提供一个名称,改名称将在测试错误输出中显示,而不是 jest.fn(),使用这个可以快速识别在测试输出中报告错误的模拟函数

1
2
3
4
5
const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(x => x + 42)
.mockName('add42')

Snapshot快照

适合测试配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// xx.js
export const generateConfig = () =>{
return {
server:'http://localhost',
port:8080
}
}

//xx.test.js
import { generateConfig } from './snopshot';

test('测试 generateConfig 函数',()=>{
expect(generateConfig()).toMatchSnapshot();
expect(generateConfig()).toEqual({
server:'http://localhost',
port:8080
})
})

jest --watchAll里面出现了 ui模式分别对应 更新所有的快照跟更新单个快照

安装 prettier,运行 toMatchInlineSnapshot,会将 快照自动存到代码下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
test('测试 generateConfig 函数', () => {
expect(generateConfig()).toMatchInlineSnapshot(
{
time: expect.any(Date),
},
// 下面是自动生成的
`
Object {
"port": 8080,
"server": "http://localhost",
"time": Any<Date>,
}
`
);
});

ES6 类

1
2
3
4
5
6
7
8
9
10
11
12
13
// util.js

class Util {
init(){}
a(){
// 异常复杂
}
b(){
// 异常复杂
}
}

export default Util;

在别的函数里面使用这个类

1
2
3
4
5
6
7
8
9
10
// useUtil.js
import Util from './util'

const useUtil = (a,b) =>{
const util = new Util()
util.a(a)
util.b(b)
}

export default useUtil;

写这个使用类的函数的测试用例的时候,我们会发现这个函数因为使用到了类里面的函数,而函数又很复杂,直接调用会损耗性能。所以这里我们用几种方法来模拟

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
// useUtil.test.js
jest.mock('./util')
// jest.mock 发现 util 是一个类,会自动把类的构造函数方法变成 jest.fn()
// const Util = jest.fn();
// Util.a = jest.fn()
// Util.b = jest.fn()
import Util from './util'
import useUtil from './useUtil'

test('测试 useUtil',()=>{
useUtil();
expect(Util).toHaveBeenCalled();
console.log(Util.mock)
expect(Util.mock.instances[0].a).toHaveBeenCalled()
expect(Util.mock.instances[0].b).toHaveBeenCalled()
})

/**
● Console

console.log src/lesson3/useUtil.test.js:12
{ calls: [ [] ],
instances: [ Util { init: [Function], a: [Function], b: [Function] } ],
invocationCallOrder: [ 1 ],
results: [ { type: 'return', value: undefined } ] }
*/

另一种方法就是通过在 __mocks__文件夹中模拟

1
2
3
4
5
6
// __mocks__/util.js
const Util = jest.fn();
Util.prototype = jest.fn();
Util.prototype = jest.fn();

export default Util

还有一种写法,是在原来的测试用例修改

1
2
3
4
5
6
7
// useUtil.test.js
jest.mock('./util',()=> {
const Util = jest.fn();
Util.prototype = jest.fn();
Util.prototype = jest.fn();
return Util;
})

DOM操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// dom.js
import $ from 'jquery'

const addDivToBody = () =>{
$('body').append('<div/>')
}

export default addDivToBody

// dom.test.js
// node 本身不具备 dom
// jest 在 node 环境下模拟了一套 dom 的 api,jsDom
import $ from 'jquery'
import addDivToBody from addDivToBody;

test('测试 addDivToBody',()=>{
addDivToBody();
expect($('body').find('div').length).toBe(1)
})

TDD(测试驱动开发)

全称:Test Driven Development

开发流程(Red-Green Development)

  1. 编写测试用例
  2. 运行测试,测试用例无法通过测试
  3. 编写代码,使测试用例通过测试
  4. 优化代码,完成开发
  5. 重复上述步骤

优势

  1. 长期减少回归 bug
  2. 代码质量更好(组织、可维护性)
  3. 测试覆盖率高
  4. 错误测试代码不容易出现

Vue TDD

开始

1
2
3
4
# 安装脚手架
npm i @vue/cli@3.8.4 -g
# 安装vue,可以选择默认配置,也可以自定义配置
vue create vue-jest

@vue/test-utils

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
// HelloWorld.test.js
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'

describe('HelloWorld.vue', () => {
/** 如果不使用 @vue/test-utils
* import Vue from 'vue'
* it('renders props.msg when passed', () => {
const root = document.createElement('div')
root.className = 'root'
document.body.appendChild(root)
new Vue({
render: h => h(HelloWorld, {
props: {
msg: 'laibh'
}
})
}).$mount('.root')
expect(document.getElementsByClassName('hello').length).toBe(1)
})
*/
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})
expect(wrapper.text()).toMatch(msg)
})

shallowMount浅层渲染,只渲染第一层,不渲染子组件,适合单元测试

mount则会渲染子组件,适合集成测试

开发 Header 组件

测试用例先行

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
// Header.test.js
import { shallowMount } from '@vue/test-utils'
import Header from '@/components/Header/Header.vue'

describe('测试 Header 组件', () => {
it('Header 包含 Input 框', () => {
const wrapper = shallowMount(Header)
const input = wrapper.find('[data-test="input"]')
expect(input.exists()).toBe(true)
})
it('Header 中 Input 初始内容为空', () => {
const wrapper = shallowMount(Header)
const inputValue = wrapper.vm.$data.inputValue
expect(inputValue).toBe('')
})

it('Header 中 Input 框值发生变化,值应该也跟着改变', () => {
const wrapper = shallowMount(Header)
const input = wrapper.find('[data-test="input"]')
input.setValue('laibh')
const inputValue = wrapper.vm.$data.inputValue
expect(inputValue).toBe('laibh')
})

it('Header 中 Input 框输入回车,无内容时无反应', () => {
const wrapper = shallowMount(Header)
const input = wrapper.find('[data-test="input"]')
input.setValue('')
input.trigger('keyup.enter')
expect(wrapper.emitted().add).toBeFalsy()
})

it('Header 中 Input 框输入回车,有内容时向外触发事件,同时清空 inputValue', () => {
const wrapper = shallowMount(Header)
const input = wrapper.find('[data-test="input"]')
input.setValue('laibh')
input.trigger('keyup.enter')
expect(wrapper.emitted().add).toBeTruthy()
expect(wrapper.vm.$data.inputValue).toBe('')
})
})

根据测试用例写代码

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
// Header.vue
<template>
<div>
<input data-test="input" v-model="inputValue" @keyup.enter="addTodoItem" />
</div>
</template>

<script>
export default {
name: "Header",
props: {},
data() {
return {
inputValue: ""
};
},
methods: {
addTodoItem() {
if (this.inputValue) {
this.$emit("add", this.inputValue);
this.inputValue = "";
}
}
}
};
</script>

<style scoped lang="less">
</style>

测试覆盖率

1
2
3
4
5
6
7
8
9
10
// jest.config.js

module.exports = {
collectCoverageFrom: ['**/*.{js,vue}', '!**/node_modules/**'],
}

// package.json
"scripts": {
"test:cov": "vue-cli-service test:unit --coverage"
},

React TDD

Enzyme

github

1
2
# 安装 
npm i --save-dev enzyme enzyme-adapter-react-16

同样,shallow适合单元测试,mount则是集成测试

例子:

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
// App.je
import React from 'react';

function App() {
return (
// 使用的data-test=xxx 等属性可以做到解耦,不会因为改变样式名而发生改变,另外也不会被hash掉
<div className="app-container" title="laibh" data-test="container">
hello world
</div>
);
}

export default App;
// App.test.js
import React from 'react';
import App from './App';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });


it('renders without crashing', () => {
const wrapper = shallow(<App />)
// 输出整个内容字符串
console.log(wrapper.debug())
/**
<div className="app-container" title="laibh" data-test="container">
hello world
</div>
*/
expect(wrapper.find('[data-test="container"]').length).toBe(1)
expect(wrapper.find('[data-test="container"]').prop('title')).toBe('laibh')
});

另外它海域一些扩展 API,例如 jest-enzyme,让语法易懂简洁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// App.test.js
import React from 'react';
import App from './App';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });


it('renders without crashing', () => {
const wrapper = shallow(<App />)
console.log(wrapper.debug())
const container = wrapper.find('[data-test="container"]')
expect(container.length).toBe(1)
expect(container.prop('title')).toBe('laibh')
// 等同上面两句
expect(container).toExist()
expect(container).toHaveProp('title', 'laibh')
});

别忘记在 jest.config.js里面进行配置

1
2
3
module.exports = {
setupFilesAfterEnv: ['./node_modules/jest-enzyme/lib/index.js'],
}

开发 Header 组件

同样测试先行

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
49
50
51
52
53
54
55
56
57
58
59
60
import React from 'react';
import Header from '../../index';
import { shallow } from 'enzyme';

let wrapper;
let inputElem;
describe('测试 Header 组件', () => {
beforeEach(() => {
wrapper = shallow(<Header />)
inputElem = wrapper.find('[data-test="input"]')
})

it('正常渲染', () => {
expect(wrapper).toMatchSnapshot()
});

it('包含一个 input', () => {
expect(inputElem).toExist()
});

it('input初始化内容应该为空', () => {
expect(inputElem.prop('value')).toBe('')
});

it('当用户输入时,input内容会跟着变化', () => {
const value = '哈哈哈'
inputElem.simulate('change', {
target: {
value
}
})
expect(wrapper.state('value')).toBe(value)
});

it('当用户输入后,键入回车,如果 input 没有内容,则不操作', () => {
const fn = jest.fn();
const wrapper = shallow(<Header addUndoItem={fn} />)
wrapper.setState({ value: '' })
const inputElem = wrapper.find('[data-test="input"]')
inputElem.simulate('keyUp', {
keyCode: 13
})
expect(fn).not.toBeCalled()
});

it('当用户输入后,键入回车,如果 input 有内容,addUndoItem应该被调用,然后input被清空', () => {
const fn = jest.fn();
const wrapper = shallow(<Header addUndoItem={fn} />)
const value = 'haha';
wrapper.setState({ value })
const inputElem = wrapper.find('[data-test="input"]')
inputElem.simulate('keyUp', {
keyCode: 13
})
expect(fn).toBeCalled()
expect(fn).toBeCalledWith(value)
const newInputElem = wrapper.find('[data-test="input"]')
expect(newInputElem.prop('value')).toBe('')
});
});

根据测试写代码

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
import React, { Component } from 'react';
import styles from './index.less'

class Header extends Component {
constructor(props) {
super(props)
this.state = {
value: ''
}
}

handleInputKeyUp = (e) => {
const { value } = this.state;
const { addUndoItem } = this.props;
if (e.keyCode === 13 && value) {
addUndoItem(value)
this.setState({ value: '' })
}
}

handleInputChange = (e) => {
this.setState({ value: e.target.value })
}

render() {
const { value } = this.state;
return (
<div className={styles.header}>
<div className={styles.headerContent}>
TodoList
<input
className={styles.headerInput}
data-test='input'
value={value}
onChange={this.handleInputChange}
onKeyUp={this.handleInputKeyUp}
placeholder='Add Todo'
/>
</div>
</div>
)
}
}

export default Header;

TDD 小结

优势:代码质量提高

单元测试

测试覆盖率高,业务耦合度高,代码量大,过于独立

BDD(行为驱动开发)

全称:Behavior Driven Development

集成测试

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
49
50
51
52
53
// vue integration/todoList 
import { mount } from '@vue/test-utils'
import TodoList from '../../TodoList'

it(`
1.用户会在 header输入框输入内容
2.用户会点击回车按钮
3.列表项应该增加用户输入内容的列表项
`, () => {
const wrapper = mount(<TodoList />);
const inputElem = wrapper.findAll('[data-test="header-input"]').at(0)
const content = 'haha'
inputElem.setValue(content)
inputElem.trigger('change')
inputElem.trigger('keyup.enter')
const listItems = wrapper.findAll('[data-test="list-item"]').at(0)
expect(listItems.length).toBe(1)
expect(listItems.at(0).text()).toContain(content)
})

// react integration/todoList
import React from 'react';
import { mount } from 'enzyme';
import TodoList from './../../index';

describe('集成测试:TodoList', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(<TodoList />)
})

it(`
1.用户会在 header输入框输入内容
2.用户会点击回车按钮
3.列表项应该增加用户输入内容的列表项
`, () => {
const value = 'haha'
const headerInput = wrapper.find('[data-test="header-input"]')
headerInput.simulate('change', {
target: {
value
}
})
// 按下回车键,keyCode为13
headerInput.simulate('keyUp', {
keyCode: 13
})
// undoListItem
const listItems = wrapper.find('[data-test="list-item"]')
expect(listItems.length).toBe(1)
expect(listItems.at(0).text()).toContain(value)
})
})

TDD 与 BDD 比较

TDD

  1. 先写测试再写代码
  2. 一般结合单元测试使用,是白盒测试
  3. 测试重点在代码
  4. 安全感低
  5. 速度快

BDD

  1. 先写代码再写测试
  2. 一般结合集成测试使用,是黑盒测试
  3. 测试重点在 UI (DOM)
  4. 安全感高
  5. 速度慢

Redux相关测试

增加 redux 在项目

createStore

1
2
3
4
5
6
7
8
9
10
11
// src/store/createStore
import { createStore, combineReducers } from 'redux'
import { reducer as todoReducer } from '../containers/TodoList/store'

const reducer = combineReducers({
todo: todoReducer
})

const store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());

export default store;

store/actions.js

1
2
3
4
5
6
import { CHANGE_INPUT_VALUE } from './constants';

export const changeInputValue = value => ({
type: CHANGE_INPUT_VALUE,
value
})

store/constants.js

1
export const CHANGE_INPUT_VALUE = 'CHANGE_INPUT_VALUE'

store/reducer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { CHANGE_INPUT_VALUE } from './constants';

const initialState = {
inputValue: ''
}

export default (state = initialState, action) => {
switch (action.type) {
case CHANGE_INPUT_VALUE:
return {
inputValue: action.value
};
default:
return state;
}
}

store/index.js

1
2
3
4
5
import reducer from './reducer'
import * as actions from './actions'


export { reducer, actions }

TodoList/index.js

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import React, { Component } from 'react';
import Header from './../../components/Header/index';
import UndoList from './../../components/UndoList/index';
import styles from './index.less'

class TodoList extends Component {

constructor(props) {
super(props)
this.state = {
undoList: []
}
}

handledeleteItem = (index) => {
const { undoList } = this.state;
const newList = undoList.filter((item, itemIndex) => itemIndex !== index)
this.setState({ undoList: newList })
}

handleStatusChange = (index) => {
const { undoList } = this.state;
// undoList.forEach((item, itemIndex) => {
// if (itemIndex === index) {
// Object.assign(item, { status: 'input' })
// return;
// } Object.assign(item, { status: 'div' })
// })
const newList = undoList.map((item, itemIndex) => {
if (itemIndex === index) {
return {
...item,
status: 'input'
}
} return {
...item,
status: 'div'
}
})
this.setState({ undoList: newList })
}

handleBlur = (index) => {
const { undoList } = this.state;
const newList = undoList.map(item => {
return {
...item,
status: 'div'
}
})
this.setState({ undoList: newList })
}

handleInputValue = (index, value) => {
const { undoList } = this.state;
const newList = undoList.map((item, itemIndex) => {
if (itemIndex === index) {
return {
...item,
value
}
} return {
...item,
}
})
this.setState({ undoList: newList })
}



addUndoItem = (value) => {
const { undoList } = this.state;
this.setState({
undoList: [...undoList, {
status: 'div',
value
}]
})
}

render() {
const { undoList } = this.state;
return (
<div className={styles.todoList}>
<Header addUndoItem={this.addUndoItem} />
<UndoList
list={undoList}
deleteItem={this.handledeleteItem}
changeStatus={this.handleStatusChange}
changeBlur={this.handleBlur}
valueChange={this.handleInputValue}
/>
</div>
)
}
}

export default TodoList;

src/index.js

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './store/createStore'

ReactDOM.render((
<Provider store={store}>
<App />
</Provider>
), document.getElementById('root'));

然后修改集成测试的测试用例

src/containers/TodoList/__test__/integration/TodoList.test.js

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
import React from 'react';
import { mount } from 'enzyme';
import TodoList from './../../index';
// 增加的内容
import { Provider } from 'react-redux';
import store from './../../../../store/createStore';



describe('集成测试:TodoList', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(
<Provider store={store}>
<TodoList />
</Provider>
)
})

it(`
1.用户会在 header输入框输入内容
2.用户会点击回车按钮
3.列表项应该增加用户输入内容的列表项
`, () => {
const value = 'haha'
const headerInput = wrapper.find('[data-test="header-input"]')
headerInput.simulate('change', {
target: {
value
}
})
// 按下回车键,keyCode为13
headerInput.simulate('keyUp', {
keyCode: 13
})
// undoListItem
const listItems = wrapper.find('[data-test="list-item"]')
expect(listItems.length).toBe(1)
expect(listItems.at(0).text()).toContain(value)
})
})

异步代码

compontDidMount

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
componentDidMount(){
/*
{
data:[
{status:'div',value:'haha'},
],
success:true
}
*/
axios.get('/undoList.json').then(res=>{
this.setState({
undoList:res.data
})
}).catch(e=>{
console.log(e)
})
}

模拟函数

src/__mock__/axios.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const mockUndoList = {
data:[{
status:'div',value:'haha'
}],
success:true
}
export default {
get(url){
if(url === '/undoList.json'){
return new Promise((resolve,reject)=>{
resolve(mockUndoList)
})
}
}
}

测试用例

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
it(`
1.用户打开页面
2.应该展示接口返回的数据
`,(done)=>{
const wrapper = mount(
<Provider store={store}>
<TodoList />
</Provider>
// 解决数据异步问题
setTimeout(()=>{
wrapper.update();
const listItems = wrapper.find('[data-test="list-item"]')
expect(listItems.length).toBe(1)
done()
},0)

// node的语法
process.nextTick(()=>{
wrapper.update();
const listItems = wrapper.find('[data-test="list-item"]')
expect(listItems.length).toBe(1)
done()
})
)
})

setTimeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
componentDidMount(){
/*
{
data:[
{status:'div',value:'haha'},
],
success:true
}
*/
setTimeout(()=>{
axios.get('/undoList.json').then(res=>{
this.setState({
undoList:res.data
})
}).catch(e=>{
console.log(e)
})
},5000)
}

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
jest.useFakeTimers();

it(`
1.用户打开页面
2.应该展示接口返回的数据
`,(done)=>{
const wrapper = mount(
<Provider store={store}>
<TodoList />
</Provider>

expect(setTimeout).toHaveBeenCalledTimes(1);
// 跑完所有 setTimeout 的时间
jest.runAllTimers();
// 解决数据异步问题
setTimeout(()=>{
wrapper.update();
const listItems = wrapper.find('[data-test="list-item"]')
expect(listItems.length).toBe(1)
done()
},0)
)
})

前端自动化测试的优势

  1. 更好的代码组织,项目的可维护性增强
  2. 更小的bug 出现概率,尤其是回归测试中的 Bug
  3. 修改工程质量差的项目,更加安全
  4. 项目具备潜在的文档特性
  5. 扩广前端的知识面

Enzyme

Enzyme 是 React 的 JavaScript 测试应用程序,可以轻松测试 React Components 的输出。还可以在给定输出的情况下,遍历以某种方式模拟运行。

主要通过模仿 Jq 用于 DOM 操作和遍历

API

at(index)

.at(index) => shallowWrapper

返回当前 wrapper 中指定索引的节点

1
2
const wrapper = shallow(<MyComponent />)
expect(wrapper.find(foo).at(0).props().foo).toEqual('bar')

first()

.first() => ShallowWrapper

将匹配节点集合减少到集合中的第一个,就像.at(0)

1
expect(wrapper.find(Foo).first().props().foo).to.equal('bar');

last()

.last() => ShallowWrapper

将匹配节点集减少到集合中的最后一个,就像.at(length - 1)

1
2
const wrapper = shallow(<MyComponent />);
expect(wrapper.find(Foo).last().props().foo).to.equal('bar');

childAt(index)

.childAt(index) => ShallowWrapper

返回指定索引的子节点

1
2
const wrapper = shallow(<TodoList items={items}/>);
expect(wrapper.find('ul').childAt(0).type).toEqual('li')

children([selector])

.children([selector]) => ShallowWrapper

返回父节点某个元素的所有子节点

1
2
const wrapper = shallow(<TodoList items={items}/>);
expect(wrapper.find('ul').children.length).toEqual(items.length)

closest(selector)

.closest(selector) => shallowWrapper

通过遍历节点祖先,返回第一个相匹配的节点

1
2
const wrapper = shallow(<MyComponent />);
expect(wrapper.find(Foo).closest('.bar')).to.have.lengthOf(1);

contains(nodeOrNodes)

.contains(nodeOrNodes) => Boolean

返回所有给定的react元素是否与渲染树中的元素匹配。它将通过检查期望元素是否与包装器元素具有相同的 props 并共享相同的值来确定包装器中的元素是否与预期元素匹配。

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
let wrapper;
wrapper = shallow((
<div>
<div data-foo="foo" data-bar="bar">Hello</div>
</div>
));

expect(wrapper.contains(<div data-foo="foo" data-bar="bar">Hello</div>)).to.equal(true);

expect(wrapper.contains(<div data-foo="foo">Hello</div>)).to.equal(false);
expect(wrapper.contains(<div data-foo="foo" data-bar="bar" data-baz="baz">Hello</div>)).to.equal(false);
expect(wrapper.contains(<div data-foo="foo" data-bar="Hello">Hello</div>)).to.equal(false);
expect(wrapper.contains(<div data-foo="foo" data-bar="bar" />)).to.equal(false);

wrapper = shallow((
<div>
<span>Hello</span>
<div>Goodbye</div>
<span>Again</span>
</div>
));

expect(wrapper.contains([
<span>Hello</span>,
<div>Goodbye</div>,
])).to.equal(true);

expect(wrapper.contains([
<span>Hello</span>,
<div>World</div>,
])).to.equal(false);

containsAllMatchingElements(patternNodes)

.containsAllMatchingElements(patternNodes) => Boolean

返回所有给定的react元素是否patternNodes与包装器的渲染树中的元素匹配。每个元素patternNodes必须匹配一次或多次。匹配遵循规则containsMatchingElement

1
2
3
4
5
6
7
8
9
10
11
12
13
const style = { fontSize: 13 };
const wrapper = shallow((
<div>
<span className="foo">Hello</span>
<div style={style}>Goodbye</div>
<span>Again</span>
</div>
));

expect(wrapper.containsAllMatchingElements([
<span>Hello</span>,
<div>Goodbye</div>,
])).to.equal(true);

containsAnyMatchingElements(patternNodes)

.containsAnyMatchingElements(patternNodes) => Boolean

返回至少一个给定的react元素是否patternNodes与包装器的渲染树中的元素匹配。一个或多个元素patternNodes必须匹配一次或多次。匹配遵循规则containsMatchingElement

1
2
3
4
5
6
7
8
9
10
11
12
13
const style = { fontSize: 13 };
const wrapper = shallow((
<div>
<span className="foo">Hello</span>
<div style={style}>Goodbye</div>
<span>Again</span>
</div>
));

expect(wrapper.containsAnyMatchingElements([
<span>Bonjour</span>,
<div>Goodbye</div>,
])).to.equal(true);

containsMatchingElement(patternNode)

.containsMatchingElement(patternNode) => Boolean

返回patternNodereact元素是否与渲染树中的任何元素匹配。

1
2
3
4
5
6
7
8
9
10
11
12
const wrapper = shallow((
<div>
<div data-foo="foo" data-bar="bar">Hello</div>
</div>
));

expect(wrapper.containsMatchingElement(<div data-foo="foo" data-bar="bar">Hello</div>)).to.equal(true);
expect(wrapper.containsMatchingElement(<div data-foo="foo">Hello</div>)).to.equal(true);

expect(wrapper.containsMatchingElement(<div data-foo="foo" data-bar="bar" data-baz="baz">Hello</div>)).to.equal(false);
expect(wrapper.containsMatchingElement(<div data-foo="foo" data-bar="Hello">Hello</div>)).to.equal(false);
expect(wrapper.containsMatchingElement(<div data-foo="foo" data-bar="bar" />)).to.equal(false);

context([key])

返回包装器根节点的上下文哈希。可选地传入一个props,它将只返回该值。

1
2
3
4
5
6
const wrapper = shallow(
<MyComponent />,
{ context: { foo: 10 } },
);
expect(wrapper.context().foo).to.equal(10);
expect(wrapper.context('foo')).to.equal(10);

debug([options])

.debug([options]) => String

返回包装器的类似HTML的字符串,以便进行调试。当测试没有通过时,打印到控制台很有用。

optionsObject[可选]):

  • options.ignoreProps:(Boolean[可选]):是否应在结果字符串中省略props。默认情况下包含道具。
  • options.verbose:(Boolean[可选]):是否应该详细打印作为道具传递的数组和对象。

dive([options])

.dive([options]) => ShallowWrapper

浅呈现当前包装器的一个非DOM子项,并返回结果周围的包装器。它必须是单节点包装器,并且该节点必须是React组件。

注意:只能在单个非DOM组件元素节点的包装上调用,否则会引发错误。如果必须使用多个子节点对包装器进行浅包装,请使用.shallow()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Bar() {
return (
<div>
<div className="in-bar" />
</div>
);
}
function Foo() {
return (
<div>
<Bar />
</div>
);
}
const wrapper = shallow(<Foo />);
expect(wrapper.find('.in-bar')).to.have.lengthOf(0);
expect(wrapper.find(Bar)).to.have.lengthOf(1);
expect(wrapper.find(Bar).dive().find('.in-bar')).to.have.lengthOf(1);

equals(node)

.equals(node) => Boolean

返回当前包装器根节点呈现树是否与传入的树相似

1
2
const wrapper = shallow(<MyComponent />);
expect(wrapper.equals(<div className="foo bar" />)).to.equal(true);

every(selector)

.every(selector) => Boolean

返回包装器中的所有节点是否与提供的选择器匹配。

1
2
3
4
5
6
7
8
9
10
const wrapper = shallow((
<div>
<div className="foo qoo" />
<div className="foo boo" />
<div className="foo hoo" />
</div>
));
expect(wrapper.find('.foo').every('.foo')).to.equal(true);
expect(wrapper.find('.foo').every('.qoo')).to.equal(false);
expect(wrapper.find('.foo').every('.bar')).to.equal(false);

everyWhere(fn)

.everyWhere(fn) => Boolean

1
2
3
4
5
6
7
8
9
10
const wrapper = shallow((
<div>
<div className="foo qoo" />
<div className="foo boo" />
<div className="foo hoo" />
</div>
));
expect(wrapper.find('.foo').everyWhere(n => n.hasClass('foo'))).to.equal(true);
expect(wrapper.find('.foo').everyWhere(n => n.hasClass('qoo'))).to.equal(false);
expect(wrapper.find('.foo').everyWhere(n => n.hasClass('bar'))).to.equal(false);

exists([selector])

.exists([selector]) => Boolean

返回包装器中是否存在任何节点。或者,如果传入选择器,则该选择器是否在包装器中具有任何匹配项。

1
2
3
const wrapper = mount(<div className="some-class" />);
expect(wrapper.exists('.some-class')).to.equal(true);
expect(wrapper.find('.other-class').exists()).to.equal(false);

filter(selector)

.filter(selector) => ShallowWrapper

返回一个新的包装器,其中只包含与提供的选择器匹配的当前包装器的节点。

1
2
const wrapper = shallow(<MyComponent />);
expect(wrapper.find('.foo').filter('.bar')).to.have.lengthOf(1);

filterWhere(fn)

.filterWhere(fn) => ShallowWrapper

返回一个新的包装器,它只包含当前包装器的节点,当传递给提供的谓词函数时,返回true

1
2
3
const wrapper = shallow(<MyComponent />);
const complexFoo = wrapper.find('.foo').filterWhere(n => typeof n.type() !== 'string');
expect(complexFoo).to.have.lengthOf(4);

find(selector)

.find(selector) => ShallowWrapper

查找当前包装器的呈现树中与提供的选择器匹配的每个节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Foo from '../components/Foo';

const wrapper = shallow(<MyComponent />);
expect(wrapper.find('.foo')).to.have.lengthOf(1);
expect(wrapper.find('.bar')).to.have.lengthOf(3);

// compound selector
expect(wrapper.find('div.some-class')).to.have.lengthOf(3);

// CSS id selector
expect(wrapper.find('#foo')).to.have.lengthOf(1);


// 组件
expect(wrapper.find(Foo)).to.have.lengthOf(1);

// 组件显示名称
expect(wrapper.find('Foo')).to.have.lengthOf(1);

// 对象属性选择器
expect(wrapper.find({ prop: 'value' })).to.have.lengthOf(1);

findWhere(fn)

.findWhere(fn) => ShallowWrapper

查找渲染树中为提供的谓词函数返回true的每个节点。

1
2
3
const wrapper = shallow(<MyComponent />);
const complexComponents = wrapper.findWhere(n => n.type() !== 'string');
expect(complexComponents).to.have.lengthOf(8);

forEach(fn)

.forEach(fn) => Self

迭代当前包装器的每个节点,并使用围绕作为第一个参数传入的相应节点的包装器执行提供的函数。

1
2
3
4
5
6
7
8
9
10
11
const wrapper = shallow((
<div>
<div className="foo bax" />
<div className="foo bar" />
<div className="foo baz" />
</div>
));

wrapper.find('.foo').forEach((node) => {
expect(node.hasClass('foo')).to.equal(true);
});

get(index)

.get(index) => ReactElement

1
2
const wrapper = shallow(<MyComponent />);
expect(wrapper.find(Foo).get(0).props.foo).to.equal('bar');

getWrappingComponent()

.getWrappingComponent() => ShallowWrapper

如果wrappingComponent传入了a options,则此方法返回ShallowWrapper渲染的周围wrappingComponent。这ShallowWrapper可以用来更新wrappingComponent props,state等。

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
import { Provider } from 'react-redux';
import { Router } from 'react-router';
import store from './my/app/store';
import mockStore from './my/app/mockStore';

function MyProvider(props) {
const { children, customStore } = props;

return (
<Provider store={customStore || store}>
<Router>
{children}
</Router>
</Provider>
);
}
MyProvider.propTypes = {
children: PropTypes.node,
customStore: PropTypes.shape({}),
};
MyProvider.defaultProps = {
children: null,
customStore: null,
};

const wrapper = shallow(<MyComponent />, {
wrappingComponent: MyProvider,
});
const provider = wrapper.getWrappingComponent();
provider.setProps({ customStore: mockStore });

getElement()

.getElement() => ReactElement

返回包装的ReactElement。如果当前包装器正在包装根组件,则返回根组件的最新呈现输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
const element = (
<div>
<span />
<span />
</div>
);

function MyComponent() {
return element;
}

const wrapper = shallow(<MyComponent />);
expect(wrapper.getElement()).to.equal(element);

getElements()

.getElements() => Array<ReactElement>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const one = <span />;
const two = <span />;

function Test() {
return (
<div>
{one}
{two}
</div>
);
}

const wrapper = shallow(<Test />);
expect(wrapper.find('span').getElements()).to.deep.equal([one, two]);

hasClass(className)

.hasClass(className) => Boolean

返回包装节点是否具有className包含传入的类名称的prop。它必须是单节点包装器。

1
2
3
4
5
const wrapper = shallow(<MyComponent />);
expect(wrapper.find('.my-button').hasClass('disabled')).to.equal(true);

// 正则
expect(wrapper.find('.my-button').hasClass(/(ComponentName)-(other)-(\d+)/)).to.equal(true);

hostNodes()

.hostNodes() => ShallowWrapper

返回仅包含主机节点的新包装器。当使用react-dom,主机节点是HTML元素,而不是定制反应的组分

1
2
3
4
5
6
7
8
const wrapper = shallow((
<div>
<MyComponent className="foo" />
<span className="foo" />
</div>
));
const twoNodes = wrapper.find('.foo');
expect(twoNodes.hostNodes()).to.have.lengthOf(1);

html()

.html() => String

返回整个当前渲染树(不仅仅是浅渲染部分)的渲染HTML标记的字符串。只能在单个节点的包装器上调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Foo() {
return (<div className="in-foo" />);
}

function Bar() {
return (
<div className="in-bar">
<Foo />
</div>
);
}

const wrapper = shallow(<Bar />);
expect(wrapper.html()).to.equal('<div class="in-bar"><div class="in-foo"></div></div>');
expect(wrapper.find(Foo).html()).to.equal('<div class="in-foo"></div>');

const wrapper = shallow(<div><b>important</b></div>);
expect(wrapper.html()).to.equal('<div><b>important</b></div>');

instance()

.instance() => ReactComponent

返回单节点包装器节点的底层类实例; this在它的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Stateless() {
return <div>Stateless</div>;
}

class Stateful extends React.Component {
render() {
return <div>Stateful</div>;
}
}

test('shallow wrapper instance should be null', () => {
const wrapper = shallow(<Stateless />);
const instance = wrapper.instance();

expect(instance).to.equal(null);
});

test('shallow wrapper instance should not be null', () => {
const wrapper = shallow(<Stateful />);
const instance = wrapper.instance();

expect(instance).to.be.instanceOf(Stateful);
});

is(selector)

.is(selector) => Boolean

返回单个包装节点是否与提供的选择器匹配。它必须是单节点包装器。

1
2
const wrapper = shallow(<div className="some-class other-class" />);
expect(wrapper.is('.some-class')).to.equal(true);

isEmptyRender()

.isEmptyRender() => Boolean

返回包装器是否最终只呈现允许的假值:falsenull

1
2
3
4
5
6
function Foo() {
return null;
}

const wrapper = shallow(<Foo />);
expect(wrapper.isEmptyRender()).to.equal(true);

key()

.key() => String

返回当前包装器节点的键值。它必须是单节点包装器。

1
2
3
4
5
6
7
const wrapper = shallow((
<ul>
{['foo', 'bar'].map(s => <li key={s}>{s}</li>)}
</ul>
)).find('li');
expect(wrapper.at(0).key()).to.equal('foo');
expect(wrapper.at(1).key()).to.equal('bar');

map(fn)

.map(fn) => Array<Any>

将当前节点数组映射到另一个数组。每个节点作为a传递ShallowWrapper 给map函数。

1
2
3
4
5
6
7
8
9
10
const wrapper = shallow((
<div>
<div className="foo">bax</div>
<div className="foo">bar</div>
<div className="foo">baz</div>
</div>
));

const texts = wrapper.find('.foo').map(node => node.text());
expect(texts).to.eql(['bax', 'bar', 'baz']);

matchesElement(patternNode)

.matchesElement(patternNode) => Boolean

回给定的react元素是否patternNode与包装器的渲染树匹配。它必须是单节点包装器,并且仅检查根节点。

这些patternNode行为就像一张通配符。为了匹配包装器中的节点:

  • 标签名称必须匹配
  • 内容必须匹配:在文本节点中,前导和尾随空格被忽略,但中间空间不被忽略。子元素必须根据这些规则以递归方式匹配。
  • patternNodeprops(attributes)必须出现在包装器的节点中,而不是相反。如果它们出现,它们的值必须匹配。
  • patternNode样式CSS属性必须出现在包装器节点的样式中,而不是相反。如果它们出现,它们的值必须匹配。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
// ...
}

render() {
return (
<button type="button" onClick={this.handleClick} className="foo bar">Hello</button>
);
}
}

const wrapper = shallow(<MyComponent />);
expect(wrapper.matchesElement(<button>Hello</button>)).to.equal(true);
expect(wrapper.matchesElement(<button className="foo bar">Hello</button>)).to.equal(true);

name()

.name() => String|null

返回此包装器的当前节点的名称。如果它是复合组件,则这将是最顶层渲染组件的名称。如果它是本机DOM节点,则它将是标记名称的字符串。如果是的话null,那就是null

返回名称的优先顺序是:type.displayName- > type.name- > type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const wrapper = shallow(<div />);
expect(wrapper.name()).to.equal('div');

function SomeWrappingComponent() {
return <Foo />;
}
const wrapper = shallow(<SomeWrappingComponent />);
expect(wrapper.name()).to.equal('Foo');

Foo.displayName = 'A cool custom name';
function SomeWrappingComponent() {
return <Foo />;
}
const wrapper = shallow(<SomeWrappingComponent />);
expect(wrapper.name()).to.equal('A cool custom name');

参考链接

  1. 前端要学的测试课 从Jest入门到TDD/BDD双实战
  2. jest 官方文档
  3. enzyme官方文档
0%