1. 为什么选择Jest?
Jest 是 Facebook 出品的一个测试框架,相对其他测试框架,其一大特点就是就是内置了常用的测试工具,比如自带断言、Mock 功能、测试覆盖率工具,实现了开箱即用
。
2. 使用
1. 如何安装
Jest 可以通过 npm 或 yarn 进行安装。以 npm 为例,既可用npm install -g jest进行全局安装;也可以只局部安装、并在 package.json 中指定 test 脚本:
{
"scripts": {
"test": "jest"
}
}
Jest 的测试脚本名形如*.test.js,不论 Jest 是全局运行还是通过npm run test运行,它都会**执行当前目录下所有的*.test.js 或 .spec.js 文件*、完成测试。
2. 简单例子
先让我们在一个react工程目录中src下新建一个__tests__文件夹,在里面写我们的各种测试案例。
首先,创建一个sum.js
文件:
function sum(a, b){
return a + b;
}
function foo(){
return 'hello world'
}
export { sum,foo };
然后在__tests__
中,新建一个sum.test.js
的文件:
import { sum,foo } from './sum.js';
describe('test testObject', () => {
test('测试求和函数', () => {
expect(sum(2, 2)).toBe(4);
});
test('测试foo函数', () => { // 也可以用it
expect(foo()).toBe('hell world');
});
})
test()
函数表示一个测试用例
describe()
函数表示一组用例
expect()
函数返回一个期望值对象,该对象提供了大量的工具方法来做结果判定
toBe()
函数表示断言方法
我们把 sum
foo
置于 describe() 函数的处理回调函数中,就实现了对一组用例的测试。
最后运行 yarn test
或 npm run test
,将打印出下面的消息,表示测试通过了。
PASS __test__/sum.test.js
PASS __test__/foo.test.js
√ 测试求和函数 (7ms)
√ 测试foo函数 (1ms)
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 4.8s
3. 匹配器
1. 判断相等:
test('2加2等于4', () => {
expect(2+2).toBe(4);
});
// 测试对象相等
test('测试对象的值', () => {
const data = {a: 1};
expect(data).toEqual({a: 1});
});
// 测试不等,相反的值
test('2加2不等于1', () => {
expect(2 + 2).not.toBe(1);
});
2. 判断真假、空值:
- toBeNull 只匹配 null;
- toBeUndefined 只匹配 undefined;
- toBeDefined 与 toBeUndefined 相反;
- toBeTruthy 匹配任何 if 语句为真;
- toBeFalsy 匹配任何 if 语句为假;
test('null', () => {
const n = null;
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
3. 判断数字:
test('2加2', () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3); // 大于3
expect(value).toBeGreaterThanOrEqual(4); // 大于或等于4
expect(value).toBeLessThan(5); // 小于5
expect(value).toBeLessThanOrEqual(4.5); // 小于或等于4.5
});
4. 判断符点数:
test('两个浮点数字相加', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); 这句会报错,因为浮点数有舍入误差
expect(value).toBeCloseTo(0.3); // 这句可以运行
});
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/);
});
6. 判断数组或可迭代的对象:toContain()
const shoppingList = [
'diapers',
'kleenex',
'beer',
];
test('the shopping list has beer on it', () => {
expect(shoppingList).toContain('beer');
});
7. 判断异常:toThrow()
function compileAndroidCode() {
throw new Error('这是一个错误消息');
}
test('compiling android goes as expected', () => {
expect(compileAndroidCode).toThrow();
// 可以匹配异常消息的内容,也可以用正则来匹配内容
expect(compileAndroidCode).toThrow('这是一个错误消息');
expect(compileAndroidCode).toThrow(/消息/);
});
Tips: 代码提示?
expect
的工具方法太多记不住怎么办,安装 jest
和 @types/jest
包含的声明文件,声明文件是定义了一些类型,写代码的时候就会有提示。
npm i jest @types/jest
4. 异步测试
异步代码的测试,关键点在于告知测试框架测试何时完成,让其在恰当的时机进行断言。
1. Callbacks回调函数:done
当我们的test函数中出现了异步回调函数时,可以给test函数传入一个 done
参数,它是一个函数类型的参数。如果test函数传入了done,jest就会等到 done
被调用才会结束当前的test case,如果 done
没有被调用,则该test自动不通过测试。
it('Test async code with done', (done) => {
setTimeout(() => {
// expect something
done();
}, 500)
});
2. 返回Promise:expect.assertions(number)
- assertions 断言
如果使用的是 promise
,需要在测试中 返回
一个 promise,Jest 会自动等待 promise 被解析处理,如果 promise 被拒绝,那么测试失败。
例1:
test("Test async code with promise", () => {
// 一个rejected状态的 Promise 不会让测试失败
expect.assertions(1);
return doAsync().then((data) => {
expect(data).toBe('example');
});
});
test("Test promise with an error", () => {
// 一个fulfilled状态的 Promise 不会让测试失败
expect.assertions(1);
return doAsync().catch(e => {
expect(e).toMatch('error')
});
});
函数 doAsync
,该函数接收两个回调 callback1
和 callback2
,它将以未知顺序异步调用这两个回调。
例2:
test("doAsync calls both callbacks", () => {
expect.assertions(2);
function callback1(data) {
expect(data).toBeTruthy();
}
function callback2(data) {
expect(data).toBeTruthy();
}
doAsync(callback1, callback2);
});
使用 expect.assertions(2)
确保两个回调都实际被调用。
- .resolves / .rejects Jest语法糖
例1中的代码用匹配符 resolves/rejects
(这里有s,非Promise)可以改写为:
// 假设 doAsync() 返回一个promise,resolve的结果为字符串'example'
it('Test async code with promise', () => {
expect.assertions(1);
return expect(doAsync()).resolves.toBe('example');
});
});
it('Test promise with an error', () => {
expect.assertions(1);
return expect(doAsync()).rejects.toMatch('error'));
});
- async/await Promise语法糖
实际开发中,我们更常用的是用 async/await
来开发业务代码,上面例子的也可以 async/await 实现
// 假设 doAsync() 返回一个promise,resolve的结果为字符串'example'
it('Test async code with promise', async () => {
expect.assertions(1);
const data = await doAsync();
expect(data).toBe('example');
});
});
async/await
也可以和 resolves/rejects
一起使用:
// 假设 doAsync() 返回一个promise,resolve的结果为字符串'example'
it('Test async code with promise', async () => {
expect.assertions(1);
await expect(doAsync()).resolves.toBe('example');
});
});
3. done 和 assertions 区别
done
:异步回调确保测试
assertions
:Promise确保测试
一般测试的时候,异步都是模拟 mock
出来的,要自己控制结束,而不是真正的异步。所以 expect.assertions 某些情况下无法替代 done
5. Mock函数
在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。
1. jest.fn()
jest.fn()
是创建 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);
})
jest.fn()
所创建的Mock函数还可以设置返回值,定义内部实现或返回Promise
对象。
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]");
})
? 实际使用:
在 ts
开发中,有些类型定义为必填项
onChange = (
pagination: PaginationConfig,
filters: Partial<Record<keyof T, string[]>>,
sorter: SorterResult<T>,
extra: TableCurrentDataSource<T>,
) => {}
假如 filters
是函数类型,为必填项,实际测试不想传入这个值,传参可以填 jest.fn()
mock
掉这个函数。
所有的 mock
函数都有一个特殊的 .mock
属性,它保存了关于此函数如何被调用、调用时的返回值的信息。
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
};
};
test('测试forEach函数', () => {
const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);
// 此 mock 函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2);
// 第一次调用函数时的第一个参数是 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// 第一次函数调用的返回值是 42
expect(mockCallback.mock.results[0].value).toBe(42);
})
2. jest.mock()
通常情况下,我们需要调用api,发送ajax请求,从后台获取数据。但是我们在做前端测试的时候,并不需要去调用真实的接口,所以此时我们需要模拟 axios/fetch
模块,让它不必调用api也能测试我们的接口调用是否正确。
下面我们创建一个 events.js
// 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);
});
在测试代码中我们使用了 jest.mock('axios')
去mock整个 fetch.js
模块。如果注释掉这行代码,执行测试脚本时会出现以下报错信息:
从这个报错中,我们可以总结出一个重要的结论:
3. jest.spyOn()
jest.spyOn()
方法创建一个mock函数,并且可以正常执行被spy的函数。
jest.spyOn()
是 jest.fn()
的语法糖,它创建了一个和被spy的函数具有相同内部代码
的mock函数。
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);
})
运行之后,可以看到shell
中的打印信息,说明通过jest.spyOn()
,fetchPostsList
被正常的执行了。
? 在实际项目的单元测试中:
jest.fn()
常被用来进行某些有回调函数的测试;
jest.mock()
可以mock整个模块中的方法,当某个模块已经被单元测试100%覆盖时,使用jest.mock()去mock该模块,节约测试时间和测试的冗余度是十分必要;
jest.spyOn()
当需要测试某些必须被完整执行的方法时,常常需要使用;
这些都需要开发者根据实际的业务代码灵活选择。
6. Jest钩子函数
1. 执行顺序和作用域
钩子函数是指在某一时刻,jest会自动调用的函数。如下:
- beforeAll:在所有测试用例执行之前执行
- afterAll:等所有测试用例都执行之后执行 ,可以在测试用例执行结束时,做一些处理
- beforeEach:每个测试用例执行前执行,可让每个测试用例中使用的变量互不影响,因为分别为每个测试用例实例化了一个对象
- afterEach:每个测试用例执行结束后,做一些处理
注意:钩子函数的作用域为: 所在的 describe
分组;
import Counter from './Counter'
// 使用类中的方法,首先要实例化
let counter = null
beforeAll(() => {
console.log('外部的 beforeAll 执行')
})
beforeEach(() => {
counter = new Counter()
console.log('外部的 beforeEach 执行')
})
describe('Counter 的测试代码', () => {
describe('Counter 中的加法测试代码', () => {
beforeAll(() => {
console.log('内部的 beforeAll 执行')
})
test('测试 Counter 中的 addOne 方法', () => {
counter.addOne()
expect(counter.number).toBe(1)
})
test('测试 Counter 中的 addTwo 方法', () => {
counter.addTwo()
expect(counter.number).toBe(2)
})
})
describe('Counter 中的减法测试代码', () => {
test('测试 Counter 中的 minusOne 方法', () => {
counter.minusOne()
expect(counter.number).toBe(-1)
})
test('测试 Counter 中的 minusTwo 方法', () => {
counter.minusTwo()
expect(counter.number).toBe(-2)
})
})
})
打印结果:
? 实际使用:
import { shallow, mount } from '@vue/test-utils'
import Test2 from './Component'
describe('Test for Test2 Component', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(Test2);
});
afterEach(() => {
wrapper.destroy()
});
it('returns the string in normal order if reversed property is not true', () => {
wrapper.setProps({needReverse: false});
wrapper.vm.inputValue = 'ok';
expect(wrapper.vm.outputValue).toBe('ok')
});
});
shallow
/ mount
,创建一个包含被挂载和渲染的Vue组件的 Wrapper
,每次执行测试语句都要 destroy()
一下, 这样每个test语句都是重新创建的独立 wrapper
,避免引起副作用。
比如在测试 FormModal
时, 如果modal没有 destroy
,在修改情况下,下次代入的还是旧的 form
值。
ps: 在测试modal时,需要先 show
一下,否则是测不到的?,和实际代码逻辑是一样的。
2. describe中的基础代码执行顺序
import Counter from './Counter'
// 使用类中的方法,首先要实例化
let counter = null
beforeEach(() => {
counter = new Counter()
console.log('外部的 beforeEach 执行')
})
describe('Counter 的测试代码', () => {
console.log('describe 11111')
describe('Counter 中的加法测试代码', () => {
console.log('describe 22222')
beforeAll(() => {
console.log('内部的 beforeAll 执行')
})
test.only('测试 Counter 中的 addOne 方法', () => {
counter.addOne()
expect(counter.number).toBe(1)
})
test('测试 Counter 中的 addTwo 方法', () => {
counter.addTwo()
expect(counter.number).toBe(2)
})
})
describe('Counter 中的减法测试代码', () => {
console.log('describe 33333')
test('测试 Counter 中的 minusOne 方法', () => {
counter.minusOne()
expect(counter.number).toBe(-1)
})
test('测试 Counter 中的 minusTwo 方法', () => {
counter.minusTwo()
expect(counter.number).toBe(-2)
})
})
})
打印结果:
由打印结果可以看出,describe
中的基础代码并没有按照我们的意愿去执行,而是最先执行了,所以当我们在写测试代码的基础代码时,一定要在钩子函数内完成。
3. only和skip
test.only(name, fn)
describe.only(name, fn)
only
只对单个测试用例进行测试
test.skip(name, fn)
describe.skip(name, fn)
skip
跳过某个测试用例进行测试
7. 其他知识点
- 写单元测试的时候,同时运行
--watch
命令,每次保存都会自动运行,查看当前test语句是否通过。
npm run test page/Component/BatchUpload.test.ts --watch
- 每写完一个测试文件, 都可以运行
--coverage
命令, 查看分支或者语句的覆盖率, 也可以定位到某个文件夹, 查看模块的覆盖率。
npm run test page/Component/BatchUpload.test.ts --coverage
- 单元测试指标
% stmts 是语句覆盖率(statement coverage):是不是每个语句都执行了?
% Branch 分支覆盖率(branch coverage):是不是每个 if 代码块都执行了?
% Funcs 函数覆盖率(function coverage):是不是每个函数都调用了?
% Lines 行覆盖率(line coverage):是不是每一行都执行了?
8. Vue单元测试实例
? 传送门
Jest结合Vue-test-utils使用的初步实践
github: Vue-unit-test-with-jest
9. 小结
疑问:明知道sum
函数返回a、b之和,为什么还要写测试语句呢 ?
这个和form表单的校验有点类似,有单元测试可以对代码逻辑进行回归验证。
而且在写测试的过程中, 可以发现自己程序的bug,会反向思考自己的代码逻辑,组件划分是否合理、逻辑是不是可以单独拿出来,这是一个相互促进的过程。
很多开源的组件库,项目都有相对应的单元测试,对于开发质量也是一个保证,别人在用的时候也会增强使用信心。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!