小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
上次讲了Jest的基础入门(一),今天再接着聊,大概是如何测试异步代码、钩子函数、Snapshot快照等。
测试异步代码
在JavaScript中,代码异步运行是很常见的。当您有异步运行的代码时,Jest需要知道它正在测试的代码何时完成,然后才能转到另一个测试。Jest有以下几种处理方法,我们一一来看。
Callbacks
第一种就是通过回调函数来接受异步代码执行的执行结果,例如,我们通常写一个请求函数fetchData(callback)
来向服务端请求我们所需要的数据,数据请求成功后传入callback
中,我们就可以在数据请求成功后执行某些操作了。
// 例如 1秒后,请求回数据 peanut butter ,然后传入callback
export const fetchData = (callback) => {
setTimeout(() => {
callback('peanut butter')
}, 1000)
}
test('the data is peanut butter', () => {
function callback(data) {
expect(data).toBe('peanut butter');
}
fetchData(callback);
});
// 测试会成功通过
写上两段代码,测试用例会通过,大家会说这不对了嘛,和之前一样呀。但大家可以试试,可以吧第一个片段中改成callback('peanut butter111')
,在运行还是通过测试,这是咋回事呢?原来是测试在执行完fetchData(callback);
后会理解结束,不会等待请求回结果调用callback
后结束。那怎么样解决这个问题呢,test接受两个参数,第一个可以说是描述,第二个是测试函数,这个函数可以接受一个done
函数,这样干了之后在Jest执行测试代码后不会立即结束,而是调用done
后立即结束。将测试代码改成如下,我们来看看效果:
// fetchData 不变,测试用例通过
// fetchData 中 callback 传入的参数要不是 'peanut butter' 那么测试不会通过
test('the data is peanut butter', done => {
function callback(data) {
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
fetchData(callback);
});
Promises
除了使用callback
回调函数外,我们还经常使用Promise
来返回请求的结果,那我们如何测试呢,修改代码如下:
export const fetchData = (callback) => {
return new Promise((resolve) => {
resolve('peanut butter')
})
}
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
// 测试通过
那如果我们想要测试reject
一个promise呢?.then
就不会执行了呀,我们就可以添加.catch
来完善测试用例,
test('the fetch fails with an error', () => {
// expect.assertions(1);
return fetchData().catch(e => expect(e).toMatch('error'));
});
// 测试不通过,要添加 expect.assertions(1); 才会
结合注释,当我们在fetchData
中还是resolve
一个值后我们的测试用例还会通过,为什么?因为只要fetchData执行了然后状态变了(resolve、reject
),都会结束测试用例,不会执行catch
方法,那么自然通过了,需要我们加上expect.assertions(1);
来指定执行了几次expect
,数量不对就会不通过。
.resolves / .rejects
使用Promise
不仅可以通过.then
和.catch
两个方法,还可以使用.resolves / .rejects
来测试。Jest会等待这个Promise
结束,然后继续执行
test('the data is peanut butter', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', () => {
return expect(fetchData()).rejects.toMatch('error');
});
Async/Await
测试异步代码当然也少不了async
和 await
两个方法了,相同的fetchData
场景可以变成如下:
test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
await fetchData();
} catch (e) {
expect(e).toMatch('error');
}
});
钩子函数
示例
在进行测试之前或之后,我们可以要进行一些准备工作或整理工作,Jest就提供了全局钩子函数来进行这件事。常见的有beforeEach、afterEach、beforeAll、afterAll
等。举个例子如下:
class MathFunc {
constructor() {
this.num = 0
}
add() {
this.num = this.num + 1
}
sub() {
this.num = this.num - 1
}
}
const m = new MathFunc()
test('test function MathFunc add methods', () => {
m.add()
expect(m.num).toBe(1)
});
test('test function MathFunc add methods', () => {
m.sub()
expect(m.num).toBe(0)
});
定义了一个类MathFunc然后实例化他,进行测试,大家可以想想为什么是0不是-1?因为共用了一个实例 m,共用了属性num,导致测试不太对,两个用例进行了相互干扰,怎么解决了?在每个用例里面都new实例化一边?当然可以,有没有更好的方法呢?有,改成如下:
let m = null
beforeEach(() => {
m = new MathFunc()
});
test('test function MathFunc add methods', () => {
m.add()
expect(m.num).toBe(1)
});
test('test function MathFunc add methods', () => {
m.sub()
expect(m.num).toBe(-1)
});
作用域
作用域这个概念就不陌生了,在Jest中也有这个概念,可能测试多个模块多个用例,需要进行不同的准备,而直接在上面写就执行每条用例都需要执行代码,影响效率或者可能造成影响,能不能针对不同的用例进行不同的准备只对局部生效呢?有,我们可以这么使用describe
进行分组显示,不同组使用不同的钩子函数,在全局作用域写的,就是全局钩子函数
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
Snapshot快照测试
在我们测试过程中,要对UI组件进行测试,这该怎么测呢?或者我们有些配置文件想进行测试怎么办,当然可以把复制文件的内容复制成一个变量然后用之前介绍的方法进行比较,那我们更改配置得改两个地方,是不是不太方便呢?Snapshot快照就来了,第一次生成__
snapshots
__
文件夹,里面存放自动生成的快照文件,在之后测试时通过对比快照文件,来判断是否通过。
例子如下:
export const getConfig = () => {
return {
server: 'http://localhost',
port: 8080,
time: '2021.10.25'
}
}
test('toMatchSnapshot', () => {
expect(getConfig()).toMatchSnapshot();
});
会生成一个快照文件index.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`toMatchSnapshot 1`] = `
Object {
"port": 8080,
"server": "http://localhost",
"time": "2021.10.25",
}
`;
当我们修改getConfig
函数中某一项后进行测试,测试结果如下:
我们可以清晰地看到差异,测试提醒报错,此时测试终端还没结束,如果确实错了不是你想要的修改后重新执行;如果是你想要的,就按u
对快照进行更新,然后测试用例通过了。
如果我们有个字段是动态变化的该怎么,比如把getConfig
函数改成如下:
export const getConfig = () => {
return {
server: 'http://localhost',
port: 8080,
time: new Date()
}
}
每次执行测试后,返回对象的time属性值和快照中永远不一样,该怎么办?更新?更新了下次运行还是不一样?不太好对不对,怎么解决呢?对toMatchSnapshot
传入第二个参数,对返回值进行处理,只要求time属性值是时间类型的就好了,当然还有其他类型。保存后会报错,按u更新后,再次运行会通过用例
test('toMatchSnapshot', () => {
expect(getConfig()).toMatchSnapshot({
time: expect.any(Date)
});
});
问题来了,这只是最基本的情况,要是我们有多个快照呢,怎么对比?方法是一样的,只是有些小情况,举个例子如下:
export const getConfig = () => {
return {
server: 'http://localhost',
port: 8080,
time: new Date()
}
}
export const getAntherConfig = () => {
return {
server: '127.0.0.1',
port: 8888,
time: new Date()
}
}
test('toMatchSnapshot-getConfig', () => {
expect(getConfig()).toMatchSnapshot({
time: expect.any(Date)
});
});
test('toMatchSnapshot-getAntherConfig', () => {
expect(getAntherConfig()).toMatchSnapshot({
time: expect.any(Date)
});
});
这个时候同上也会生成快照文件,那么我们修改了两个配置的某些参数,然后运行,结果可想而知,测试不通过,显示对比结果,此时按u
两个快照都会被更新,而如果更多的话我们可能看不过来,打印信息太多了,或者有的是我们想更新的,有的不是(就是错的),这样操作就会全部更新,显然不合理;那该怎么办呢?还有个操作键i
,按i
后进去挨个对比显示,就和之前一样,按u
更新。大家可以试一下,还可以试试其他命令。
今天就到这里呢,下次我们聊聊mock数据和jsdom。