单元测试我写的好烂
之前的积累:可以说很少
在之前的公司,是最后才开始写测试的,那个时候写起来就感觉有点难受。我记得那个case是我尝试去点击Link,然后判断页面的url是否有改变,我刚开始尝试用侦测window.location去感知url的改变,但是history的改变,不会去触发location的事件,最后的最后,我退而其次去判断了函数有没有被调用,现在来看也不是一个好办法,判断的条件是这样才更稳妥:
const spy = jest.spyOn(router, 'push')
expect(spy).toBeCalledWith('new url')
使用jest暴露的问题
这次刚进去的task也是写单测,相对之前,每个小业务都被封装在包中,react的部分也少,jest写单测的话,真的是捉襟见肘啊,而且我好像没有掌握jest的技巧,感觉写单测都是以写集成测试的思想在写,所以写出来的质量不怎么样。我新增的一个文件的测试,觉得还可以,但一看覆盖率才百分之四十多,于是我开始疯狂的补case想要把每个函数的每个分支都给覆盖了,所以一眼看上去我的测试写的特别多,特别长,但是对照其他同学写的,感觉我写的好像是特别笨重,效果不是那么大。我写了好几天的case,只提升了五个点💔,我总结一下这次遇到问题:
1. 异步函数:callback形式
代码可能长这样:
dynamicInsertScript('scriptUrl', (data) => {
window.data = data
// data.xxx
})
data上的属性和方法特别多,mock起来特别费劲,要是callback里面代码巨多,那针对这一个函数要写很多case。首先刚开始写的时候,我又陷入了误区,一个很朴素的想法,我在写的时候,sleep上很长时间,等待这个函数load完,window.data不就有值了吗,然后我再去判断或者触发data上的属性,于是我写下了:
test('case', () => {
setTimeout(() => {
expect(window.data).no.toBe(undefined)
}, 1000)
})
写完之后,跑过了,完美,以后这样的例子就这么写就行了,开心的提了mr,reviewer给了comment:setTimeout在这里面是无效的,需要done一下。然后我又在setTimeout的最后一行加了done,但是jest一直报错 async callback was not invoked within the 5000ms timeout specified by jest.settimeout.
,这个时候我想到setTimeout用错地方了,我还是老老实实的mock所有函数吧。于是改成了下面这样:
jest.mock('@utils', () => ({
dynamicInsertScript: (_, cb) => {
const data = {
// xxx:
}
cb(data)
}
}));
test('case', () => {
expect(window.data).no.toBe(undefined)
})
这样的话就没有异步了,但是如果data上有很多很多属性,如果当前测试的文件里面有用到,必须把这些属性全部mock,如果有复杂的属性,就写起来比较艰难。
2. 异步函数:promise
有了第一个的经验,可不能再用setTimeout了,这个mock的想法和上面的类似,对于代码
conts getList = () => {
const data = await fetchData('xxx')
// 下面是处理data的部分
}
可以这样写case:
test('case', () => {
const spy = jest.spyOn(api, 'fetchData')
// 每次调用返回不同的值
spy.mockImplementationOnce(() => Promise.resolve({
// xxx
}))
// 一定要记得加await!!!
await getList()
spy.mockImplementationOnce(() => Promise.resolve({
// xxx
}))
await getList()
})
剩下的问题和上一个一行,你要mock的数据要完整。
3. setTimeoue如何快速执行
针对下面的代码:
const change = () => {
setTimeout(() => {
// xxx
}, 0)
}
可以这样来:
jest.useFakeTimers();
test('case', () => {
change()
jest.runAllTimers()
// 然后check结果
})
4. 检查函数调用的参数
针对下面的代码:
const tip = () => {
message.error('error')
}
可以这样来:
test('case', () => {
message.error = jest.fn()
tip()
expect(message.error.mock.calls[0][0]).toBe('error');
})
5. window.addEventListen
test('case', () => {
const map = {}
window.addEventListen = (type, func) => {
map[type] = func
}
window.removeEventListen = (type) => {
delete map[type]
}
map.error({preventDefault: () => {}}) // 参数为event对象
})
6. localStorage
test('case', () => {
const spyGetItem = jest.spyOn(window.localStorage.__proto__, 'getItem')
})
7. new Date()
test('case', () => {
// jest < version 26
const mockDate = new Date(1466424490000)
const spy = jest
.spyOn(global, 'Date')
.mockImplementation(() => mockDate)
spy.mockRestore()
})
8. mock window伤的对象
Object.defineProperty(window.document, 'cookie', {
get: jest.fn(),
});