foreword
Although many front-end teams are unlikely to use unit testing now or even in the future, including my own team, the reason is nothing more than delaying time, the development task itself is relatively heavy and so on.
But I think blindly trying to be fast is always drinking poison to quench thirst, falling into a vicious circle, 项目快 \--> 代码烂 \--> 修改和加功能花费更多的时间和精力 \--> 来不及做优化必须更快 \--> 项目快 \--> 代码烂 \-->
an infinite loop.
This is why I think the most important reason for doing unit testing is that when refactoring the code, confirm that there is no problem with the function, not afraid of personnel turnover, function migration, and most importantly 产品撕b
, the test case is the best proof 😁.
If you can’t use it for business projects, if you write a library and don’t write a single test, the students who may use it will have scruples, so writing a single test is a necessary skill for the front-end above the advanced level.
Unit Testing Framework Fundamentals
For example, the following test case, feel the basic appearance, we will implement a simple version of the method used in it later
// 意思是字符串hello是否包含ll
test('测试字符串中是否包含 ll'), () => {
expect(findStr('hello')).toMatch('ll')
})
function findStr(str){
return `${str} world`
}
复制代码
We can simply implement the methods used in the above test cases test、expect、toMatch
, so that even if we have mastered the basic principles of the test framework
test
function test(desc, fn){
try{
fn();
console.log(`✅ 通过测试用例`)
}catch{
console.log(`❌ 没有通过测试用例`)
}
}
复制代码
expect, toMatch
function expect(ret){
return {
toMatch(expRet){
if(typeof ret === 'string'){ throw Error('') }
if(!ret.includes(expRet)){ throw Error('') }
}
}
}
复制代码
jest basic configuration
Necessary tools:
$ npm i -D jest babel-jest ts-jest @types/jest
复制代码
Refer to the configuration jest.config.js, the test files are placed in the tests directory: the following testRegex represents the jsx or tsx files ending with test or spec in the matching tests folder
module.exports = {
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
moduleFileExtensions: ['tsx', 'ts', 'js', 'jsx', 'json', 'node'],
};
复制代码
Finally, add it to the scripts of package.json
{
test: "jest"
// 如果要测试覆盖率,后面加上--coverage
// 如果要监听所有测试文件 --watchAll
}
复制代码
matcher
Matchers are a very important concept in Jest, which can provide many ways for you to verify the return value of your test. Let's take an example to understand what a matcher is.
The matchers here can be glanced at, and you probably know that there is such a thing. Just look up the matchers you want when you use them, and you don't need to memorize them deliberately.
Equal matching, this is our most commonly used matching rule
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
复制代码
In this code, the result we expect expact(2 + 2)
will be returned. Usually, we only need to call expect
it. The parentheses can be a function with a return value or an expression. The latter toBe
is a matcher.
Here are some commonly used matchers:
normal matcher
-
toBe: object.is is equivalent to ===
test('测试加法 3 + 7', () => {
// toBe 匹配器 matchers object.is 相当于 ===
expect(10).toBe(10)
})
复制代码
-
toEqual: content is equal, matches content, does not match reference
test('toEqual 匹配器', () => {
// toEqual 匹配器 只会匹配内容,不会匹配引用
const a = { one: 1 }
expect(a).toEqual({ one: 1 })
})
复制代码
Matchers related to true and false
-
true and false -
toBeNull: only matches Null
test('toBeNull 匹配器', () => {
// toBeNull
const a = null
expect(a).toBeNull()
})
复制代码
toBeUndefined: only matches undefined
test('toBeUndefined 匹配器', () => {
const a = undefined
expect(a).toBeUndefined()
})
复制代码
toBeDefined: Contrary to toBeUndefined, matching null is passed here
test('toBeDefined 匹配器', () => {
const a = null
expect(a).toBeDefined()
})
复制代码
toBeTruthy: matches any if statement to true
test('toBeTruthy 匹配器', () => {
const a = 1
expect(a).toBeTruthy()
})
复制代码
toBeFalsy: matches any if statement to false
test('toBeFalsy 匹配器', () => {
const a = 0
expect(a).toBeFalsy()
})
复制代码
not: negate
test('not 匹配器', () => {
const a = 1
// 以下两个匹配器是一样的
expect(a).not.toBeFalsy()
expect(a).toBeTruthy()
})
复制代码
number
toBeGreaterThan: Greater than
test('toBeGreaterThan', () => {
const count = 10
expect(count).toBeGreaterThan(9)
})
复制代码
toBeLessThan: less than
test('toBeLessThan', () => {
const count = 10
expect(count).toBeLessThan(12)
})
复制代码
toBeGreaterThanOrEqual: greater than or equal to
test('toBeGreaterThanOrEqual', () => {
const count = 10
expect(count).toBeGreaterThanOrEqual(10) // 大于等于 10
})
复制代码
toBeLessThanOrEqual: less than or equal to
test('toBeLessThanOrEqual', () => {
const count = 10
expect(count).toBeLessThanOrEqual(10) // 小于等于 10
})
复制代码
toBeCloseTo: Calculate floating point numbers
test('toBeCloseTo', () => {
const firstNumber = 0.1
const secondNumber = 0.2
expect(firstNumber + secondNumber).toBeCloseTo(0.3) // 计算浮点数
})
复制代码
string
toMatch: match a specific item string, support regular
test('toMatch', () => {
const str = 'http://www.zsh.com'
expect(str).toMatch('zsh')
expect(str).toMatch(/zsh/)
})
复制代码
array
toContain: whether the match contains a specific item
test('toContain', () => {
const arr = ['z', 's', 'h']
const data = new Set(arr)
expect(data).toContain('z')
})
复制代码
abnormal
toThrow
const throwNewErrorFunc = () => {
throw new Error('this is a new error')
}
test('toThrow', () => {
// 抛出的异常也要一样才可以通过,也可以写正则表达式
expect(throwNewErrorFunc).toThrow('this is a new error')
})
复制代码
Test asynchronous code
Suppose the request function is as follows
const fethUserInfo = fetch('http://xxxx')
复制代码
There are several ways to test asynchronous code, I recommend the one that I think is more common
// fetchData.test.js
// 测试promise成功需要加.resolves方法
test('the data is peanut butter', async () => {
await expect(fethUserInfo()).resolves.toBe('peanut butter');
});
// 测试promise成功需要加.rejects方法
test('the fetch fails with an error', async () => {
await expect(fethUserInfo()).rejects.toMatch('error');
});
复制代码
scope
Jest provides a descible function to separate each test case, that is, put the related code into a group, so simple, just look at an example to understand.
// 分组一
describe('Test xxFunction', () => {
test('Test default return zero', () => {
expect(xxFunction()).toBe(0)
})
// ...其它test
})
// 分组二
describe('Test xxFunction2', () => {
test('Pass 3 can return 9', () => {
expect(xxFunction2(3)).toBe(9)
})
// ...其它test
})
复制代码
hook function
There are 4 hook functions in jest
-
beforeAll: execute before all tests -
afterAll: After all tests are executed -
beforeEach: executed before each test instance -
afterEach: executed after each test instance completes
We illustrate why they are needed with an example.
index.js
Write some methods to be tested in
export default class compute {
constructor() {
this.number = 0
}
addOne() {
this.number += 1
}
addTwo() {
this.number += 2
}
minusOne() {
this.number -= 1
}
minusTwo() {
this.number -= 2
}
}
复制代码
Suppose we want index.test.js
to write test instances in
import compute from './index'
const Compute = new compute()
test('测试 addOne', () => {
Compute.addOne()
expect(Compute.number).toBe(1)
})
test('测试 minusOne', () => {
Compute.minusOne()
expect(Compute.number).toBe(0)
})
复制代码
-
Here, the two test instances affect each other and share a computet instance. We can
const Compute = new compute()
solve it by placing it in beforeEach. Renew compute before each test instance. -
Similarly, what you want to run separately after each test can be put
afterEach
into
Let's take a look at what circumstances to use beforeAll
, if we test whether the database data is saved correctly
-
At the very beginning of the test, that is, in the beforeAll
life cycle, we add 1 piece of data to the database -
After the test, that is, in the afterAll
cycle, delete the previously added data -
Finally, use the global scope afterAll
to confirm whether the database is restored to its original state
here it says
// 模拟数据库
const userDB = [
{ id: 1, name: '小明' },
{ id: 2, name: '小花' },
]
// 新增数据
const insertTestData = data => {
// userDB,push数据
}
// 删除数据
const deleteTestData = id => {
// userDB,delete数据
}
// 全部测试完
afterAll(() => {
console.log(userDB)
})
describe('Test about user data', () => {
beforeAll(() => {
insertTestData({ id: 99, name: 'CS' })
})
afterAll(() => {
deleteTestData(99)
})
})
复制代码
Mock in jest
Why use Mock function?
In the project, it is often encountered that the A module drops the B module. Moreover, in unit testing, we may not need to care about the execution process and result of the method called internally, just want to know whether it is called correctly, and even specify the return value of the function. At this point, a mock function is needed.
The following three features provided by the Mock function are very useful when we write test code:
-
Capture function calls -
set function return value -
Change the internal implementation of the function
jest.fn()
jest.fn()
It is the most common way to create mock functions.
test('测试jest.fn()', () => {
let mockFn = jest.fn();
let result = mockFn(1);
// 断言mockFn被调用
expect(mockFn).toBeCalled();
// 断言mockFn被调用了一次
expect(mockFn).toBeCalledTimes(1);
// 断言mockFn传入的参数为1
expect(mockFn).toHaveBeenCalledWith(1);
})
复制代码
jest.fn()
The created Mock function can also set the return value , define the internal implementation or return the Promise
object .
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]");
})
复制代码
2. jest.mock()
fetch.js
The request method encapsulated in the folder may not need to make an actual request when other modules are called (the request method has passed the single test or the method needs to return non-real data). jest.mock()
At this point, it is very necessary to use to mock the entire module.
Next we src/fetch.js
create one in the same level directory src/events.js
.
import fetch from './fetch';
export default {
async getPostList() {
return fetch.fetchPostsList(data => {
console.log('fetchPostsList be called!');
// do something
});
}
}
复制代码
import events from '../src/events';
import fetch from '../src/fetch';
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);
});
复制代码
In the test code we use jest.mock('../src/fetch.js')
to mock the entire fetch.js
module. If you comment out this line of code, the following error message will appear when executing the test script
From this error, we can draw an important conclusion:
In jest, if you want to capture the calling of a function, the function must be mocked or spy!
3. jest.spyOn()
jest.spyOn()
The method also creates a mock function, but the mock function can not only capture the calling of the function, but also execute the spy function normally. Actually, jest.spyOn()
yes jest.fn()
syntactic sugar, it creates a mock function with the same internal code as the function being spyed.
The above picture is jest.mock()
a screenshot of the correct execution result in the previous sample code. From the shell script, you can see console.log('fetchPostsList be called!');
that this line of code is not printed in the shell, because jest.mock()
after passing, the method in the module will not be actual by jest implemented. Then we need to use it jest.spyOn()
.
// functions.test.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);
})
复制代码
After execution npm run test
, you can see the print information in the shell, indicating that it passed jest.spyOn()
and fetchPostsList
was executed normally.
snapshot
A snapshot is to save a copy of the data you are comparing. What does it mean? Let's take an example:
This isindex.js
export const data2 = () => {
return {
name: 'zhangsan',
age: 26,
time: new Date()
}
}
复制代码
index.test.js
Write some test instances in
import { data2 } from "./index"
it('测试快照 data2', () => {
expect(data2()).toMatchSnapshot({
name: 'zhangsan',
age: 26,
time: expect.any(Date) //用于声明是个时间类型,否则时间会一直改变,快照不通过
})
})
复制代码
-
toMatchSnapshot
will match the parameters to the snapshot -
expect.any(Date)
for matching a time type
The execution npm run test
will generate a __snapshots__
folder with the generated snapshots. When you modify the test code, you will be prompted that the snapshots do not match. If you are sure you need to make changes, press the u key to update the snapshot. This is useful for testing UI components.
React's BDD unit test
Next, let's take a look at how the react code is tested, using a small example to illustrate.
Enzyme was introduced in the case. _Enzyme_ from airbnb is a JavaScript testing tool for React that allows you to judge, manipulate and traverse React Components output.
我们达成的目的是检测:
-
用户进入首页,看到两个按钮,分别是counter1和counter2 -
点击counter1,就能看到两个按钮的文字部分分别是"counter1"和"counter2"
react代码如下
import React from 'react';
function Counter(){
return (
<ul>
<li>
<button id='counter1' className='button1'>counter1</button>
</li>
<li>
<button id='counter2' className='button2'>counter2</button>
</li>
</ul>
)
}
复制代码
单测的文件:
import Counter from xx;
import { mount } from 'enzyme';
describle('测试APP',() => {
test('用户进入首页,看到两个按钮,分别是counter1和counter2,并且按钮文字也是counter1和counter2',()=>{
const wrapper = mount(<Counter />);
const button = wrapper.find('button');
except(button).toHaveLength(2);
except(button.at(0).text()).toBe('counter1');
except(button.at(1).text()).toBe('counter2');
})
})
复制代码
Jest | 測試設置分類(describe)及作用域[1]
jest入门单元测试[2]
作者:孟祥_成都
https://juejin.cn/post/7092188990471667749
前端技术交流群 随着读者越来越多,我也建了几个技术交流群,九分聊技术,一分聊风雪,欢迎有兴趣的同学加入我们。 可以长按识别下方二维码,一定要注意:城市+昵称+前端,根据格式,可以更快捷地通过选择内容进入群。 ▲长按扫描 — 完 —
关注公众号后,回复下面关键词获取 回复 面试,获取最新大厂面试资料。 回复 简历,获取 3200 套 简历模板。 回复 TypeScript,获取 TypeScript 精讲课程。 回复 uniapp,获取 uniapp 精讲课程。 回复 Node,获取 Nodejs+koa2 实战教程。 回复 架构师,获取 架构师学习资源教程。 更多教程资源应用尽有,欢迎 关注获取