网上有很多 vue-test-utils + jest 的相干文章,大体上能够分为两个流派:
- 配置搭建派
- 非凡问题派
通观起来总感觉仿佛对于真正想要施行单元测试的入门开发人员来说,缺了一环:面对具体场景时,应该如何进行单元测试编码。
这篇文章就依据不同场景,列出一些实在的代码示例。心愿可能帮忙到想要真正施行单元测试的团队。
mock 实例办法
场景举例
submit 办法内,个别会调用校验办法,依赖校验办法的返回 true || false
来判断是否进行理论提交逻辑的执行。
但有时候须要测试的办法内调用的其余办法很难结构,比方校验办法须要传递若干参数,而且所有参数必须合规能力继续执行提交的代码逻辑。那么此时让校验办法间接通过比拟好。
当然这引出另外一个话题,咱们在进行单元测试的时候是否应该连带援用办法一起测试?
我的认识是,既然是单元测试,那么援用办法不肯定必须全副笼罩到,起因如下:
- 在 TDD 过程中需测试的办法和其中的援用办法不肯定是同一个人负责的
- 单元测试应该仅为以后测试方法的逻辑是否正确负责,如提交办法须要思考的是提交的各种胜利或者失败场景,而校验办法应该自行通过本人的单元测试代码来确保校验自身是否正确
因而在这种场景下,咱们对于特地简单的援用办法能够思考进行 mock。
代码如下
jest.spyOn(wrapper2.vm, 'validate').mockImplementation(() => {return true})
wrapper2.vm.handleSubmit() // 再间接调用提交办法的话,校验办法就会间接返回胜利,以便咱们间接对提交外部的办法进行测试
对于特定字段的断言
场景举例
有时候在逻辑代码中一个办法的调用,其参数可能十分宏大,然而对于某种测试场景,咱们只须要判断参数中是否含有某几个特定字段或者值。
比方咱们调用一个办法,其代码如下
function fn () {
...
let params = {
key1: value1,
key2: value2,
key3: value3,
...
keyn: valuen
}
...
request.get(params)
}
在进行单元测试编写中,咱们须要校验的是某种场景下,params
必须含有 keyJ = J 的参数值。此时,咱们不须要对整个 params 对象进行断言
let params = {
key1: value1,
key2: value2,
key3: value3,
...
keyn: valuen
}
// 下面齐全手动构建了残缺的参数对象,不要这样做
expect(request.get).toBeCalledWith(params)
如果只关怀一个大对象其中的某个字段的值,不要像下面这样编写测试代码,请参考上面的测试代码示例
expect(request.get).toBeCalledWith(expect.objectContaining({key3: 42 // 假如咱们构建的测试场景只须要特地关注该场景下的 key3 值必须为 42}))
对于 refs 的解决
在业务代码中含有调用以后组件 refs 援用自组件,并调用他们办法的时候要怎么解决呢?
思考如下场景
methods: {save: () => {this.$refs[name].validate((valid) => {if (valid) {......}
})
}
}
咱们应该如何为 save
办法编写测试代码?
如果应用 vue test utils 的 shallowMount
办法,实际上不会在测试中实例化嵌套组件。比方下面这段代码,这里援用到的 refs 指向的是 Form 组件。如果间接在测试中执行 wrapper.vm.save(),会报错显示调用了 undefined 的 validate 办法。
这时候须要在 shallowMount 的时候通过 stubs 属性来模仿加载一个嵌入的组件
const Form = {render: jest.fn(),
methods: {validate: (cb) => {cb(true)}
}
}
wrapper = shallowMount(indexTip, {
localVue,
store,
propsData: {},
stubs: {Form}
})
先申明这个须要 stub 的 Form 组件。这个新申明的组件只用来模仿测试中可能须要的行为,以防止引起 undefined 的异样,所以只须要申明必要的办法即可。
- render 办法间接申明为 jest.fn(),因为不须要理论渲染出任何 html
- methods 里边申明了 validate 办法,在测试代码中会理论调用这里的申明。留神 stub 的办法申明无论如何都会返回 true。这里仍然还是依照下面提及的准则,单元测试仅测试与以后函数相干的逻辑
此外。在 shallowMount
的参数中,应用 stubs 属性内申明自定义的 Form。
这样,再次调用 wrapper.vm.save 办法的时候,就不会报 validate 办法未定义了。
留神:业务码中调用的 refs.xxxname,其中 xxxnames 为模板中组件元素的 ref 名。但 stub 的时候须要申明的是组件的名称。比方下面的例子,业务代码中 refname 为 xxxname,组件名为 Form。这时候须要 stub 模仿的是 Form,而不是 xxxname。
Parent 组件模仿
与下面的场景相同,这次咱们探讨如果业务代码中有对于 parent 组件的调用应该如何编写测试代码。
this.$parent.getList()
尽管我认为间接调用 parent 的办法不是一种良好的实际,然而谁让 vue 提供了这样的能力呢,你就不能保障没有人会这么用。对于这种场景的测试代码,原理跟下面一个场景是相似的,只是在 shallowMount
的调用中有一点不同,咱们看具体代码
const Parent = {data: () => ({val: true}),
methods: {getList: () => {} // 当然如果业务代码中须要依赖 getList 办法的返回,也能够在这里 return result},
template: '<div />'
}
// 接下来在 shallowMount 的时候申明父组件即可
wrapper = shallowMount(indexTip, {
parentComponent: Parent, // 申明父组件为下面边定义的 Parent
localVue,
store,
propsData: {}})
mock 异步申请
在测试中,常常会碰到须要针对不同异步申请的后果编写测试代码的状况。
比方接口返回 code: 0 的时候做什么解决,code: 1 的时候做什么解决,甚至是申请在网络失败的状况下做什么解决
function fn (xxx) {return axios.get(url, {id: xxx})
.then(res => {if (res.code === 10000) {this.list = res.list} else {this.fail(res.errmsg)
}
})
.catch(err => {this.toast(err.message)
})
}
如上代码,咱们要测试的函数叫做 fn,其中调用了一个异步申请,那么咱们在测试代码中须要获取到这个异步 Promise 对象能力对其外部逻辑编写测试代码,因而 fn 函数须要返回这个 Promise 对象。
测试代码如下
it('getMarkerList test case', async () => {await wrapper.vm.fn(xxx)
expect(wrapper.vm.list).toBe([...somelist]) // 断言此时 vm.list 已被赋值胜利
})
进一步思考,既然是单元测试,是否不应该依赖接口的返回,比方咱们须要测试没有网络连接的状况下前端代码的逻辑解决,总不能在继续集成过程中忽然拔网线吧。
所以咱们还需应用 jest 提供的 mock promise 能力
对 Promise 对象返回值的 mock
下面介绍了 mock Promise 对象的办法,能够让咱们在没有理论进行网络调用的状况下就触发相应的解决逻辑。
但大多数状况下,还要更加细分地对不同代码分支做断言。
比方
if (res.code === 10000)
...
else if (res.code === 10002) {if (res.hasSelected === true)
...
else {...}
}
因而,jest 提供了快捷的办法来进行不同返回值的模仿
axios.get.mockResolvedValue({code: 10000, list: [1, 2, 3]}) // 模仿胜利的状态
await wrapper.vm.fn()
expect(wrapper.vm.list.length).toBe(3) // 断言 fn 外部 promise 调用胜利时,list 属性应该是一个三个元素的数组
axios.get.mockResolvedValue({code: 10001, msg: 'id not fount'}) // 模仿没有找到该 id 的对应记录
await wrapper.vm.fn()
expect(wrapper.vm.fail).toBeCalledWith('id not found') // 断言 fn 此场景下,fail 办法被调用且参数为‘id not found’axios.get.mockRejectedValue('networkError') // reject 的状况
await wrapper.vm.getLouDong()
expect(wrapper.vm.toast).toBeCalledWith('networkError')
模仿 setTimeout
业务代码中,可能会在办法外部执行 setTimeout,过肯定工夫之后,再进行某些操作,比方
submit () {this.toast('清地面')
setTimeout(() => {this.loaded = []
}, 300)
}
这个场景可能是出于用户体验的思考。
那么如果依照惯例的办法,咱们在执行了 getNewList 办法之后就马上去进行断言,则测试必定是无奈通过的。
针对这种状况,jest 提供了一个有用的性能,useFakeTimers。
在测试代码中只须要申明 jest.useFakeTimers 而后再调用相应办法,让假的计时器向前或者向后若干毫秒即可,看代码
it('submit test case', () => {jest.useFakeTimers()
wrapper.vm.submit()
jest.advanceTimersByTime(350)
expect(wrapper.vm.loaded.length).toBe(0)
})
模仿 window.location
在业务代码中常常会碰到须要应用以后 URL 来进行判断的场景,但在 jest 中咱们不能间接对 window.location 进行赋值来模仿这个场景,如何做到?
比方咱们须要依据 location.search
中蕴含特定的参数值来执行不同的代码分支。
能够依照两个步骤来进行模仿:
- 自定义 window 对象,笼罩 jsdom 中的 window 对象
- 通过 defineProperty 对 window.location 对象进行赋值
global.window = Object.create(window);
const url = "http://dummy.com?foo=bar";
Object.defineProperty(window, 'location', {
value: {
href: url,
search: '?foo=bar'
}
});
expect(global.location.search.indexOf('foo') > -1).toBeTruthy()
留神:因为笼罩了全局变量 location, 因而要留神其只在某一个测试用例中起作用,否则会导致其余测试用例失败。