### 前言
来到公司之后,开始负责起组内的组件库一事,这一路走来,趟了不少坑,走了不少弯路,但也真是这些磨练,让我成长了不少。年已将至,对于这一年所做的事儿,该有所总结,留下些什么,但又不知道从哪里下手,干脆从 Jest 下手,做一个小小的基础扫盲。
### 简介
Jest 是 facebook 推出的一款测试框架,集成了 Mocha,chai,jsdom,sinon 等功能。用 Jest 官网的一句话就是“Jest 是一个令人愉快的 JavaScript 测试框架,专注于简洁明快”。
### 环境搭建
1. 创建项目
来到一个空文件,初始化文件
npm init -y
2. 安装 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 中的应用。拜拜~
本文暂时没有评论,来添加一个吧(●'◡'●)