网上有很多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, 因而要留神其只在某一个测试用例中起作用,否则会导致其余测试用例失败。