Learn jest.js note
2022/1/16 jest
# Web常见测试
来源于VUE: https://cn.vuejs.org/v2/guide/testing.html (opens new window)
- 单元测试(unit): Mocha、Jest ...
- 端到端 (E2E,end-to-end) 测试:cypress.io 、Nightwatch.js、Puppeteer ...
# 单元测试分类
- TDD - 测试驱动开发:先写测试用例再写代码
- BDD - 行为驱动开发:先写代码再写测试用例
# Jest介绍
- https://jestjs.io/ (opens new window)
- Jest 是一款优雅、简洁的 JavaScript 测试框架。
- Jest 支持 Babel、TypeScript、Node、React、Angular、Vue 等诸多框架!
# 快速开始
- 安装 jest
npm install jest -D
or
yarn add jest -D
1
2
3
2
3
- 初始化配置文件
npx jest init // -> jest.config.js
1
- demo
// sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
// sum.test.js
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
// package.json
{
"scripts": {
"test": "jest" // <- add
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 常用配置参数
collectCoverage
: 覆盖testMatch
: 匹配test文件transform
: 匹配到文件后使用的转换loadertestEnvironment
: 测试环境,默认是node环境testEnvironmentOptions
: 设置变量传递给testEnvironment
中moduleNameMapper
: 类似webpack中的别名setupFiles
&setupFilesAfterEnv
: 用于设置 testing environment- demo
const path = require('path');
module.exports = {
testMatch: [
'<rootDir>/src/test/unit/specs/*.spec.js',
],
transform: {
'^.+\\.js?$': 'babel-jest',
'^.+\\.ts?$': 'ts-jest',
'.*\\.(vue)$': 'vue-jest',
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
transformIgnorePatterns: ['/node_modules/'],
// ...
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 常用接口
- 常用的三个:
describe
、test
( 别名it
)、expect
const sum = require('../src/sum');
describe('tests: sum commonjs', () => {
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
})
1
2
3
4
5
6
7
2
3
4
5
6
7
- 钩子函数:多次测试重复设置
beforeEach & afterEach
- 钩子函数:一次性设置
beforeAll & afterAll
- 钩子函数执行顺序
beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));
describe('Scoped / Nested block', () => {
beforeAll(() => console.log('2 - beforeAll'));
afterAll(() => console.log('2 - afterAll'));
beforeEach(() => console.log('2 - beforeEach'));
afterEach(() => console.log('2 - afterEach'));
test('', () => console.log('2 - test'));
});
// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- describe 和 test 块的执行顺序
describe('outer', () => {
console.log('describe outer-a');
describe('describe inner 1', () => {
console.log('describe inner 1');
test('test 1', () => {
console.log('test for describe inner 1');
expect(true).toEqual(true);
});
});
console.log('describe outer-b');
test('test 1', () => {
console.log('test for describe outer');
expect(true).toEqual(true);
});
describe('describe inner 2', () => {
console.log('describe inner 2');
test('test for describe inner 2', () => {
console.log('test for describe inner 2');
expect(false).toEqual(false);
});
});
console.log('describe outer-c');
});
// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test for describe inner 1
// test for describe outer
// test for describe inner 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
28
29
30
31
32
33
34
35
36
37
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
# 匹配器的使用
toBe
test('2 加 2 等于 4', () => {
expect(2 + 2).toBe(4);
});
1
2
3
2
3
toEqual
: 递归检查对象或数组的每个字段
test('对象赋值', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
});
1
2
3
4
5
2
3
4
5
not
test('adding positive numbers is not zero', () => {
for (let a = 1; a < 10; a++) {
for (let b = 1; b < 10; b++) {
expect(a + b).not.toBe(0);
}
}
});
1
2
3
4
5
6
7
2
3
4
5
6
7
toBeNull
只匹配null
toBeUndefined
只匹配undefined
toBeDefined
与toBeUndefined
相反toBeTruthy
匹配任何if
语句为真toBeFalsy
匹配任何if
语句为假- 数字大多数的比较数字有等价的匹配器
test('two plus two', () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
// toBe and toEqual are equivalent for numbers
expect(value).toBe(4);
expect(value).toEqual(4);
});
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
toBeCloseTo
:对于比较浮点数相等
test('两个浮点数字相加', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); // 这句会报错,因为浮点数有舍入误差
expect(value).toBeCloseTo(0.3); // 这句可以运行
});
1
2
3
4
5
2
3
4
5
toMatch
:字符串
test('there is no I in team', () => {
expect('team').not.toMatch(/I/);
});
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/);
});
1
2
3
4
5
6
7
2
3
4
5
6
7
toContain
:Arrays and iterables
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'milk',
];
test('the shopping list has milk on it', () => {
expect(shoppingList).toContain('milk');
expect(new Set(shoppingList)).toContain('milk');
});
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
toThrow
:抛出错误
function compileAndroidCode() {
throw new Error('you are using the wrong JDK');
}
test('compiling android goes as expected', () => {
expect(() => compileAndroidCode()).toThrow();
expect(() => compileAndroidCode()).toThrow(Error);
// You can also use the exact error message or a regexp
expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
expect(() => compileAndroidCode()).toThrow(/JDK/);
});
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 异步代码
- 回调
const fetchData = callback => setTimeout(() => callback&&callback('peanut butter'), 1000)
test('the data is peanut butter', done => {
expect.assertions(1);
function callback(data) {
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
// 异步方法
fetchData(callback);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- Promises:需要有返回值
// 捕获:then
const fetchData = () => Promise.resolve('peanut butter')
test('the data is peanut butter', () => {
expect.assertions(1);
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
// 捕获:cath
const fetchData = () => Promise.reject('catch error')
test('the fetch fails with an error', () => {
expect.assertions(1); // 验证一定数量的断言被调用
return fetchData().catch(e => expect(e).toMatch('error'));
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 【推荐】
.resolves
/.rejects
// resolves
const fetchData = () => Promise.resolve('peanut butter')
test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
// rejects
const fetchData = () => Promise.reject('catch error')
test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
});
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Async/Await
// resolve
const fetchData = () => Promise.resolve('peanut butter')
test('the data is peanut butter', async () => {
expect.assertions(1);
const data = await fetchData();
expect(data).toBe('peanut butter');
});
// reject error
const fetchData = () => Promise.reject('catch error')
test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
await fetchData();
} catch (e) {
expect(e).toMatch('error');
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 【推荐】
async await
和.resolves or .rejects
一起使用
// resolve
const fetchData = () => Promise.resolve('peanut butter')
test('the data is peanut butter', async () => {
await expect(fetchData()).resolves.toBe('peanut butter');
});
// error
const fetchData = () => Promise.reject('catch error')
test('the fetch fails with an error', async () => {
await expect(fetchData()).rejects.toMatch('error');
});
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# Jest Mock
# jest.fn
- 没有定义函数内部的实现,返回
undefined
作为返回值
test('测试jest.fn()调用', () => {
let mockFn = jest.fn();
let result = mockFn(1, 2, 3);
// 断言mockFn的执行后返回undefined
expect(result).toBeUndefined();
// 断言mockFn被调用
expect(mockFn).toBeCalled();
// 断言mockFn被调用了一次
expect(mockFn).toBeCalledTimes(1);
// 断言mockFn传入的参数为1, 2, 3
expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
mockReturnValue
:定义返回值
test('测试jest.fn()返回固定值', () => {
let mockFn = jest.fn().mockReturnValue('default');
// 断言mockFn执行后返回值为default
expect(mockFn()).toBe('default');
})
test('测试jest.fn()内部实现', () => {
let mockFn = jest.fn((num1, num2) => {
return num1 * num2;
})
// 断言mockFn执行后返回100
expect(mockFn(10, 10)).toBe(100);
})
test('测试jest.fn()返回Promise', async () => {
let mockFn = jest.fn().mockResolvedValue('default');
let result = await mockFn();
// 断言mockFn通过await关键字执行后返回值为default
expect(result).toBe('default');
// 断言mockFn调用后返回的是Promise对象
expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- demo
// fetch.js
import axios from 'axios';
export default {
async fetchPostsList(callback) {
return axios.get('https://jsonplaceholder.typicode.com/posts').then(res => {
return callback(res.data);
})
}
}
// fetch.spec.js
import fetch from '../src/fetch.js'
test('fetchPostsList中的回调函数应该能够被调用', async () => {
expect.assertions(1);
let mockFn = jest.fn();
await fetch.fetchPostsList(mockFn);
// 断言mockFn被调用
expect(mockFn).toBeCalled();
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# jest.mock
- 模块内的方法是不会被jest所实际执行
// events.js
import fetch from './fetch';
export default {
async getPostList() {
return fetch.fetchPostsList(data => {
console.log('fetchPostsList be called!'); // not console called
// do something
});
}
}
// events.spec.js
import events from '../src/events';
import fetch from '../src/fetch';
// mock整个fetch.js模块
jest.mock('../src/fetch.js');
test('mock 整个 fetch.js模块', async () => {
expect.assertions(2);
await events.getPostList();
expect(fetch.fetchPostsList).toHaveBeenCalled();
expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# jest.spyOn
// events.js
import fetch from './fetch';
export default {
async getPostList() {
return fetch.fetchPostsList(data => {
console.log('fetchPostsList be called!'); // console called
// do something
});
}
}
// events.spec.js
import events from '../src/events';
import fetch from '../src/fetch';
test('使用jest.spyOn()监控fetch.fetchPostsList被正常调用', async() => {
expect.assertions(2);
const spyFn = jest.spyOn(fetch, 'fetchPostsList');
await events.getPostList();
expect(spyFn).toHaveBeenCalled();
expect(spyFn).toHaveBeenCalledTimes(1);
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 测试库
# 在 Vue 中使用
来源:https://cn.vuejs.org/v2/guide/testing.html (opens new window)
Vue Test Utils (opens new window):vue官方的偏底层的组件测试库
Vue Testing Library (@testing-library/vue) (opens new window)
el-input: textarea 设置默认高度
<el-input
type="textarea"
:rows="10"
/>
1
2
3
4
2
3
4
- element-ui jest
// HelloWorld.vue
<template>
<div class="hello">
<el-button>{{ msg }}</el-button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
// HelloWorld.spec.js
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Button from '@/components/HelloWorld.vue'
import ElementUI from 'element-ui' // added
const localVue = createLocalVue() // added
localVue.use(ElementUI) // added
describe('shallowMount HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(Button, {
propsData: { msg },
localVue
})
expect(wrapper.text()).toMatch(msg)
})
})
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
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
- jest测试:第三方组件
import { shallowMount, createLocalVue } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
import Vuex from 'vuex'
import Router from 'vue-router'
import ElementUI from 'element-ui'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Router)
localVue.use(ElementUI)
describe('HelloWorld.vue', () => {
it('use localVue', () => {
const wrapper = shallowMount(HelloWorld, {
localVue
})
})
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- tests router view: code sinppets (opens new window)
import { mount, createLocalVue } from "@vue/test-utils"
import App from "@/App.vue"
import VueRouter from "vue-router"
import NestedRoute from "@/components/NestedRoute.vue"
import routes from "@/routes.js"
const localVue = createLocalVue()
localVue.use(VueRouter)
// mock components render
jest.mock("@/components/NestedRoute.vue", () => ({
name: "NestedRoute",
render: h => h("div")
}))
describe("App", () => {
it("renders a child component via routing", async () => {
const router = new VueRouter({ routes })
const wrapper = mount(App, {
localVue,
router
})
router.push("/nested-route")
await wrapper.vm.$nextTick()
expect(wrapper.findComponent(NestedRoute).exists()).toBe(true)
})
})
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
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
- jest.mock & jest.fn: code snippets (opens new window)
// NestedRoute.vue
<template>
<div>
Nested Route
<div class="username">
{{ $route.params.username }}
</div>
</div>
</template>
<script>
import { bustCache } from "@/bust-cache.js"
export default {
name: "NestedRoute",
beforeRouteLeave(to, from, next) {
bustCache()
next()
}
}
</script>
// NestedRoute.spec.js
import { shallowMount, createLocalVue } from "@vue/test-utils"
import VueRouter from "vue-router"
import NestedRoute from "@/components/NestedRoute.vue"
import mockModule from "@/bust-cache.js"
jest.mock("@/bust-cache.js", () => ({ bustCache: jest.fn() }))
const localVue = createLocalVue()
localVue.use(VueRouter)
describe("NestedRoute", () => {
it("renders a username from query string", () => {
const username = "alice"
const wrapper = shallowMount(NestedRoute, {
mocks: {
$route: {
params: { username }
}
}
})
expect(wrapper.find(".username").text()).toBe(username)
})
it("calls bustCache and next when leaving the route", () => {
const next = jest.fn()
NestedRoute.beforeRouteLeave(undefined, undefined, next)
expect(mockModule.bustCache).toHaveBeenCalled()
expect(next).toHaveBeenCalled()
})
})
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
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
# How can I mock window
?
# 方式一
- 使用
Object.defineProperty
代理劫持属性 - 相关包:jest-useragent-mock (opens new window)
# 方式二
yarn add --dev jest-environment-jsdom-global jest-environment-jsdom
1
- 配置
jest.config.js
"jest": {
"testEnvironment": "jest-environment-jsdom-global" // 默认是node
}
1
2
3
2
3
# jest Exceeded timeout of 5000 ms for a test #11607
# tsd
tsd: https://github.com/SamVerschueren/tsd (opens new window)
# package
- jest promise: flush-promises
# 相关链接
- Github@Jest issues: Docs: setupFiles vs setupFilesAfterEnv #9314 (opens new window)
- jsdom (opens new window)
- jest-environment-jsdom (opens new window)
- Jest Mocking localstorage #2098 (opens new window)
- Unable to change window.location using Object.defineProperty #5124 (opens new window)
- Jest Unable to change window.location using Object.defineProperty #5124 (opens new window)
- vue tests: https://github.com/tonylua/vue-testing-handbook (opens new window)