### 前言
来到公司之后,开始负责起组内的组件库一事,这一路走来,趟了不少坑,走了不少弯路,但也真是这些磨练,让我成长了不少。年已将至,对于这一年所做的事儿,该有所总结,留下些什么,但又不知道从哪里下手,干脆从 Jest 下手,做一个小小的基础扫盲。
### 简介
Jest 是 facebook 推出的一款测试框架,集成了 Mocha,chai,jsdom,sinon 等功能。用 Jest 官网的一句话就是“Jest 是一个令人愉快的 JavaScript 测试框架,专注于简洁明快”。
### 环境搭建
1. 创建项目
来到一个空文件,初始化文件
npm init -y2. 安装 Jest 相关的依赖包
npm install -D jest @babel/core @babel/preset-env在我们安装的依赖中,@babel/core、@babel/preset-env 的作用是让我们能够使用 ES6 的语法特性进行单元测试,Jest 本身是不支持的。
3. 新建 .babelrc 文件,配置 babel 相关配置
"plugins": [
["@babel/plugin-transform-runtime"]
],
"presets": [
[
"@babel/preset-env"
]
]4. 新建 .jest.js 文件,配置相关内容
const transformIgnorePatterns = [
'/dist/',
'node_modules\/[^/]+?\/(?!(es|node_modules)\/)',
];
module.exports = {
// 多于一个测试文件运行时展示每个测试用例测试通过情况。
verbose: true,
// 这个属性是定义在每个测试文件运行之前,且在测试环境准备好前就会立即执行的文件或模块。
// setupFiles: [
// './tests/setupTests.js'
// ],
// 在测试环境准备好之后且每个测试文件执行之前运行下述文件。
setupFilesAfterEnv: [
],
// 不用手动去序列化 wrapper,后续的 enzyme 篇会使用这个。
// snapshotSerializers: ['enzyme-to-json/serializer'],
// 测试用例运行在一个类似于浏览器的环境里,可以调用浏览器的 API。
testEnvironment: 'jsdom',
// 例如,require('./index') 语句会先找 `index.ts`,找不到找 `index.tsx`等等。
moduleFileExtensions: [
'ts',
'tsx',
'js',
'jsx',
'json',
'md',
],
testPathIgnorePatterns: [
'/node_modules/',
],
// 这个属性是设置哪些文件中的代码是需要被相应的转译器转换成 Jest 能识别的代码,Jest 默认是能识别 JS 代码的,其他语言,例如 Typescript、CSS 等都需要被转译。这有点像 Webpack 的 loader。
// transform: {},
// 这个属性是设置哪些文件不需要转译的。默认是 node_modules 中的模块不需要转译,
transformIgnorePatterns,
};5. 配置 package.json 的 test 脚本
"scripts": {
"test": "jest --config .jest.js",
"test:single": "jest --config .jest.js tests/base/timer/index.test.js",
"test:update": "jest --config .jest.js --updateSnapshot",
"test:debugger": "node --inspect-brk node_modules/.bin/jest --config .jest.js --watchAll --no-cache --runInBand",
"test:sdebugger": "node --inspect-brk node_modules/.bin/jest --config .jest.js tests/base/timer/index.test.js --no-cache --runInBand"
}到这里,我们基础的配置任务已经基本结束,接下来就是基础实战。
### 基础用法
接下来,我们的基础用法将会从以下几个方面展开:
1. 断言 string 类型
2. 断言 number 类型
3. 断言 truthiness 类型
4. 断言 function 类型
5. 断言 object 类型
6. 断言 timer 类型
7. 断言 async 类型
8. 断言 exception 类型
- 对于 string 类型,我们常用的断言方法有:
1. .toMatch(regexp | string)使用 .toMatch 去检查字符串是否与正则表达式或者字符串匹配。
接下来,我们用 toMatch() 来开始我们的单元测试
创建一个叫 string 的文件夹,文件夹下新建 index.js 文件和 index.test.js 文件。在 index.js 文件中,我们编写代码:
const str = '我是Jest string';
export default str;在 index.test.js 文件中,我们编写测试代码:
import str from './index';
describe('test string method', () => {
it('expect to match "我是Jest string"', () => {
expect(str).toMatch("我是Jest string");
});
});执行测试命令:
npm run test单测结果如下:
就这样,我们的第一个单元测试就跑通了~。
- 对于 number 类型,我们常用的断言方法有:
1. .toEqual(value)使用 .toStritEqual 测试对象是否具有相同的类型和结构。
2. .toBeGreaterThan(number | bigint)使用 .toBeGreaterThan 比较期望的数字大于接收的数字或大整数
3. .toBeGreaterThanOrEqual(number | bigint)使用 .toBeGreaterThanOrEqual 比较期望的数字大于等于接收的数字或大整数
4. .toBeLessThan(number | bigint)使用 .toBeLessThan 比较期望的数字小于接收的数字或大整数
5. .toBeLessThanOrEqual(number | bigint)使用 .toBeLessThan 比较期望的数字小于等于接收的数字或大整数
接下来,我们来编写一个测试用例,来熟悉一下 toEqual() 的用法。
创建一个叫 number 的文件夹,文件夹下新建 index.js 文件和 index.test.js 文件。在 index.js 文件中,我们编写代码:
export default (a, b) => a + b;在 index.test.js 文件中,我们编写测试代码:
import add from './index';
describe('test number method', () => {
it('1+2=3', () => {
expect(add(1,2)).toEqual(3);
});
});执行测试命令:
npm run test:single单测结果如下:
- 对于 truthiness 类型,我们常用的断言方法有:
1. .toBeNull()使用 .toBeNull 检验是否是 null。
2. .toBeDefined()使用 .toBeDefined 检验是否定义。
3. .toBeUndefined()使用 .toBeUndefined 检验是否未定义。
4. .toBeTruthy()使用 .toBeTruthyv检验是否未为真。
5. .toBeFalsy()使用 .toBeFalsy 检验是否未为假。
同样,我们来看一下 truthiness 类型相关方法的用法。
创建一个叫 truthiness 的文件夹,文件夹下新建 index.js 文件和 index.test.js 文件。在 index.js 文件中,我们编写代码:
const n = null;
export default n;在 index.test.js 文件中,我们编写测试代码:
import n from './index';
describe('test number method', () => {
it('test null', () => {
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined(); // 不匹配 undefined
expect(n).not.toBeTruthy(); // 不是 真
expect(n).toBeFalsy(); // 假
});
});执行测试命令:
npm run test:single单测结果如下:
- 对于 function 类型,我们常用的断言方法有:
1. .toBeCalled()使用 .toBeCalled 检验 mock 函数是否被调用。
2. .toBeCalledTimes(number)使用 .toBeCalledTimes 检验 mock 函数被调用次数。
3. .toBeCalledWith(arg1, arg2, ...)使用 .toBeCalledWith 检验 mock 函数被调的时候的参数。
4. .toHaveBeenNthCalledWith(nthCall, arg1, arg2, ....)使用 .toHaveBeenNthCalledWith 检验 mock 函数第 n 次被调的时候的参数。
5. .toHaveReturned()使用 .toHaveReturned 检验 mock 函数成功返回。
6. .toHaveReturnedTimes(number)使用 .toHaveReturnedTimes 检验 mock 函数成功的次数。
7. .toHaveReturnedWith(value)使用 .toHaveReturnedWith 检验 mock 函数成功的值。
8. .toHaveLastReturnedWith(value)使用 .toHaveLastReturnedWith 检验 mock 函数最后一次返回的值。
9. .toHaveNthReturnedWith(nthCall, value)使用 .toHaveNthReturnedWith 检验 mock 函数第 n 次返回的值。
另外,上面几个方法还有别名,我们这里就不一一列出来了,大家去 [Jest](https://www.jestjs.cn/docs/expect#tohavebeencalled)官网查看即可。
创建一个叫 function 的文件夹,文件夹下新建 index.test.js 文件。在 index.test.js 文件中,我们编写代码:
describe('Test function methods', () => {
test('测试 jest.fn() 调用', () => {
let mockFn = jest.fn();
let result = mockFn(1, 2, 3);
// 断言 mockFn 被调用
expect(mockFn).toBeCalled();
// 断言 mockFn 的执行后返回 undefined
expect(result).toBeUndefined();
// 断言 mockFn 被调用了一次
expect(mockFn).toBeCalledTimes(1);
// 断言 mockFn 传入的参数为1, 2, 3
expect(mockFn).toBeCalledWith(1, 2, 3);
})
}单测结果如下:
在上面的单测 demo 中,出现了一个 jest.fn() 这样的一个方法。在 Jest 单测,有一个重要的概念就是 Mock 函数。我们从上面的 demo 中可以看到,进行单元测试时,要测试的内容依赖其他内容,比如 AJAX 异步请求,依赖网络,很可能因为网络造成测试达不到效果。怎么办呢?这就用到 Mock 函数。Mock 函数就是把依赖替换成我们可控的内容,实现测试的内容和它的依赖项隔离。那怎么才能实现 mock 呢?使用 Mock 函数。在 jest 中,Mock 函数就是一个虚拟的或假的函数,对它来说,最重要的就是实现依赖的全部功能,从而起到替换的作用。接下来,我们来学习一下 Mock 函数相关的知识。
在 function 文件夹下新建 index.mock.test.js 文件。在 index.mock.test.js 文件中,我们编写代码:
describe('Test Mock function', () => {
test('测试 jest.fn() 实现函数', () => {
let mockFn = jest.fn((num1, num2) => {
return num1 * num2;
});
// 断言 mockFn 执行后返回100
expect(mockFn(10, 10)).toBe(100);
})
test('测试 jest.fn() 返回固定值', () => {
let mockFn = jest.fn().mockReturnValue('default');
// 断言 mockFn 执行后返回值为default
expect(mockFn()).toBe('default');
})
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]");
})
});单测结果如下:
看了上面的 demo,可能你还是一头雾水,Mock 函数到底有什么用呢?
对于上面的所有的单测 demo,我们都没有依赖依赖网络进行数据的请求,而是直接拿数据进行测试。现在,让我们新建一个文件 fetch.js,在 fetch.js 中写下如下代码。
import axios from 'axios';
export default {
async fetchPostsList(callback) {
return axios.get('https://jsonplaceholder.typicode.com/posts/1').then(res => {
return callback(res.data);
})
}
然后在我们的 index.mock.test.js 中增加新的测试内容:
import fetch from './fetch'
describe('Test axios fetch', () => {
test('fetchPostsList 真实请求,并且回调函数应该能够被调用', async () => {
let mockFn = jest.fn((data) => {
console.log('真实请求的数据:', data);
});
await fetch.fetchPostsList(mockFn);
// 断言mockFn被调用
expect(mockFn).toBeCalled();
})
});在网络环境良好的状况下,测试结果如下:
但是,当我断开网络的时候,测试结果如下:
所以,我们需要 Mock 函数来实现这种模拟请求。
在我们的 index.mock.test.js 中增加新的测试内容:
import axios from 'axios';
import fetch from './fetch';
jest.mock('axios', () => {
return {
get: (url, callback) => {
callback(0, 'ok', {name: 'sam'})
}
};
});
describe('Test mock with "jest.mock("module", () => {})"', () => {
test('should return data when fetchPostsListWithCallback request success', () => {
fetch.fetchPostsListWithCallback(data => {
console.log('模拟 callback 请求了, 结果为:', data);
});
})
});此时,不管我们连不链接网络,我们的单测都是成功的,测试结果如下:
对于这种简单的 mock, jest.mock() 还提供了第二种写法,它的第二个参数是一个函数,返回一个 mock 实现。
在我们的 index.mock.test.js 中增加新的测试内容:
describe('Test jest.spyOn method', () => {
it('spyOn console', () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(msg => msg);
console.warn('警告');
expect(warnSpy).toHaveReturnedWith('警告');
warnSpy.mockRestore();
})
});在 fetch.js 中新增如下代码:
async fetchPostsListWithCallback(callback) {
return axios.get('https://jsonplaceholder.typicode.com/posts/1', (code, message, data) => {
callback(data);
})
}测试结果如下:
还有一种 mock 实现的方式,jest.spyOn(), 它接受两个参数,一个是对象,一个是对象上的某一个方法,返回一个 mock 函数。
在我们的 index.mock.test.js 中增加新的测试内容:
1. .toContain(item)测试结果如下:
- 对于 object 类型,我们常用的断言方法有:
2. .toContainEqual(item)使用 .toContain() 检查项目是否在数组中。
2. .toContainEqual(item)使用 .toContainEqual() 检查项目是否完全匹配,但是可以匹配数组内对象。
创建一个叫 object 的文件夹,文件夹下新建 index.test.js 文件。在 index.test.js 文件中,我们编写代码:
describe('test object', () => {
// 匹配数组/Set/字符串中是否包含 item
it('test toContain', () => {
let name = 'eric';
let arr = ['js', 'ts'];
let set = new Set(arr);
expect(name).toContain('eric');
expect(arr).toContain('js');
expect(set).toContain('ts');
});
});测试结果如下:
新增测试代码:
describe('test object', () => {
// 和.toContain类似,必须完全匹配,但是可以匹配数组内对象
test('test toContainEqual', () => {
let name = 'luna'
let arr = ['js', 'ts', {name: 'luna', age: 24}]
let set = new Set(arr)
// expect(name).toContainEqual('luna_tsai') // 错误示例
expect(arr).toContainEqual('js')
expect(set).toContainEqual('ts')
//expect(set).toContainEqual({name: 'luna'}) // 错误示例
expect(set).toContainEqual({name: 'luna', age: 24})
})
});测试结果如下:
- 对于 timer 类型,我们常用的方法有:
1. jest.clearAllTimers()使用 jest.clearAllTimers() 清除所有定时器。
2. jest.runAllTimers()使用 jest.runAllTimers() 执行所有定时器。
3. jest.runTimersToTime(msToRun)jest 会将 setTimeout 替换为自带的 setTimeout 方法,该方法调用时会将对应的回调函数登记到对应的 _timers 列表当中。当中会通过 expiry 记录定时器的到期时间。当执行 jest.runTimersToTime(time) 时,就会进行判断 _now + time >= _timer.expiry,如果达到过期时间,_timer.callback 就会被立即执行。
4. jest.runOnlyPendingTimers()使用 jest.runOnlyPendingTimers() 来启用等待中的定时器。
5. jest.useFakeTimers()使用 jest.useFakeTimers() 来启用假定时器。
6. jest.useRealTimers()使用 jest.useRealTimers() 来启用定时器。
7. jest.advanceTimersByTime(times)使用 jest.advanceTimersByTime() 来提前多少时间来运行定时器。
创建一个叫 timer 的文件夹,文件夹下新建 index.js 和 index.test.js 文件。在 index.js 文件中,我们编写代码:
export const lazy = (fn)=> {
setTimeout(() => {
fn();
console.log('第一个定时器执行')
setTimeout(()=>{
console.log('第二个定时器执行')
},3000)
}, 3000);
在 index.test.js 文件中,我们编写代码:
import {lazy} from './index';
describe('Jest test timer method', () => {
test('should call fn after 3s', () => {
const callback = jest.fn();
lazy(callback);
// 真实执行
setTimeout(() => {
expect(callback).toBeCalled();
}, 3001);
})
});测试结果如下:
通过使用 setTimeout, 我们可以成功执行我们的单测,但是,原生定时器功能(即 setTimeout,setInterval,clearTimeout,clearInterval) 对于测试环境来说不太理想,因为它们依赖于实时时间。如何使用 Jest 模拟的方式,测试我们的代码呢?我们可以使用 Jest 提供的方法 jest.runAllTimers()。
注释掉 setTimeout(),在我们的 index.test.js 中增加新的测试内容:
// 使用 mock timer,需要开启 jest.useFakeTimers()
beforeAll(() => {
jest.useFakeTimers();
});
describe('Jest test timer method', () => {
test('should call fn after 3s', () => {
const callback = jest.fn();
lazy(callback);
// 真实执行
// setTimeout(() => {
// expect(callback).toBeCalled();
// }, 3001);
// 模拟执行
jest.runAllTimers() // 把所有的定时器执行
expect(callback).toHaveBeenCalledTimes(1);
})
});
执行脚本,测试结果如下:
- 对于 async 类型,来看看我们如何处理异步类型:
创建一个叫 async 的文件夹,文件夹下新建 index.test.js 文件。在 index.test.js 文件中,我们编写代码:
const fetchData = (cb) => {
cb(1);
};
describe('test async', () => {
// 方式一
it('Test async code with done', done => {
// 回调
function callback(data) {
expect(data).toEqual(1);
done();
}
fetchData(callback);
});
// 方式二
it('Test async code with promise', () => {
// Promise resolve
expect.assertions(1);
Promise.resolve(1).then(data => {
expect(data).toBe(1);
});
});
// 方式三
it('Test async code with error', () => {
// Promise reject
expect.assertions(1);
Promise.reject('error')
.catch(err => {
expect(err).toMatch('error');
});
});
// 方式四
it('Test async code with async', async () => {
expect.assertions(1);
const data = await Promise.resolve(1);
expect(data).toBe(1);
});
});执行脚本,测试结果如下:
- 对于 exception 类型,我们常用的方法有:
1. .toThrow(error?)使用 .toThrow 测试函数在被调用时是否抛出异常。
创建一个叫 exception 的文件夹,文件夹下新建 index.test.js 文件。在 index.test.js 文件中,我们编写代码:
const exception = () => {
throw new Error('我错了');
};
describe('test exception', () => {
it('expect exception happen', () => {
expect(exception).toThrow();
});
it('expect exception happen because Error', () => {
expect(exception).toThrow(Error);
});
it('expect exception happen with "我错了"', () => {
expect(exception).toThrow('我错了');
});
});执行脚本,测试结果如下:
### 写在最后
终于,我们的基础部分已经介绍完毕,后续会结合 enzyme,跟大家分享一下 Jest + Enzyme 在 React 中的应用。拜拜~

本文暂时没有评论,来添加一个吧(●'◡'●)