vue-test-utils + jest 實(shí)戰(zhàn)代碼舉例

網(wǎng)上有很多vue-test-utils + jest 的相關(guān)文章,大體上可以分為兩個流派:

  • 配置搭建派
  • 特殊問題派

通觀起來總覺得似乎對于真正想要實(shí)施單元測試的入門開發(fā)人員來說,缺了一環(huán):面對具體場景時,應(yīng)該如何進(jìn)行單元測試編碼。

這篇文章就根據(jù)不同場景,列出一些真實(shí)的代碼示例。希望能夠幫助到想要真正實(shí)施單元測試的團(tuán)隊。

mock 實(shí)例方法

場景舉例

submit方法內(nèi),一般會調(diào)用校驗(yàn)方法,依賴校驗(yàn)方法的返回 true || false 來判斷是否進(jìn)行實(shí)際提交邏輯的執(zhí)行。

但有時候需要測試的方法內(nèi)調(diào)用的其他方法很難構(gòu)造,比如校驗(yàn)方法需要傳遞若干參數(shù),而且所有參數(shù)必須合規(guī)才能繼續(xù)執(zhí)行提交的代碼邏輯。那么此時讓校驗(yàn)方法直接通過比較好。

當(dāng)然這引出另外一個話題,我們在進(jìn)行單元測試的時候是否應(yīng)該連帶引用方法一起測試?

我的看法是,既然是單元測試,那么引用方法不一定必須全部覆蓋到,原因如下:

  • 在TDD過程中需測試的方法和其中的引用方法不一定是同一個人負(fù)責(zé)的

  • 單元測試應(yīng)該僅為當(dāng)前測試方法的邏輯是否正確負(fù)責(zé),如提交方法需要考慮的是提交的各種成功或者失敗場景,而校驗(yàn)方法應(yīng)該自行通過自己的單元測試代碼來確保校驗(yàn)本身是否正確

因此在這種場景下,我們對于特別復(fù)雜的引用方法可以考慮進(jìn)行mock。

代碼如下

jest.spyOn(wrapper2.vm, 'validate').mockImplementation(() => {return true})
wrapper2.vm.handleSubmit()  // 再直接調(diào)用提交方法的話,校驗(yàn)方法就會直接返回成功,以便我們直接對提交內(nèi)部的方法進(jìn)行測試

對于特定字段的斷言

場景舉例

有時候在邏輯代碼中一個方法的調(diào)用,其參數(shù)可能非常龐大,但是對于某種測試場景,我們只需要判斷參數(shù)中是否含有某幾個特定字段或者值。

比如我們調(diào)用一個方法,其代碼如下

function fn () {
    ...
    let params = {
        key1: value1,
        key2: value2,
        key3: value3,
        ...
        keyn: valuen
    }
    ...
    request.get(params)
}

在進(jìn)行單元測試編寫中,我們需要校驗(yàn)的是某種場景下,params必須含有 keyJ = J 的參數(shù)值。此時,我們不需要對整個params對象進(jìn)行斷言

let params = {
  key1: value1,
  key2: value2,
  key3: value3,
  ...
  keyn: valuen
}
// 上面完全手動構(gòu)建了完整的參數(shù)對象,不要這樣做
expect(request.get).toBeCalledWith(params)

如果只關(guān)心一個大對象其中的某個字段的值,不要像上面這樣編寫測試代碼,請參考下面的測試代碼示例

expect(request.get).toBeCalledWith(expect.objectContaining({
    key3: 42 // 假設(shè)我們構(gòu)建的測試場景只需要特別關(guān)注該場景下的key3 值必須為 42
}))

關(guān)于refs的處理

在業(yè)務(wù)代碼中含有調(diào)用當(dāng)前組件refs引用自組件,并調(diào)用他們方法的時候要怎么處理呢?

考慮如下場景

methods: {
    save: () => {
    this.$refs[name].validate((valid) => {
        if (valid) {
            ......
      }
    })
  }
}

我們應(yīng)該如何為save方法編寫測試代碼?

如果使用vue test utils的shallowMount方法,實(shí)際上不會在測試中實(shí)例化嵌套組件。比如上面這段代碼,這里引用到的refs指向的是Form組件。如果直接在測試中執(zhí)行wrapper.vm.save(),會報錯顯示調(diào)用了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(),因?yàn)椴恍枰獙?shí)際渲染出任何html
  • methods里邊聲明了validate方法,在測試代碼中會實(shí)際調(diào)用這里的聲明。注意stub的方法聲明無論如何都會返回true。這里依然還是按照上面提及的原則,單元測試僅測試與當(dāng)前函數(shù)相關(guān)的邏輯

此外。在shallowMount的參數(shù)中,使用stubs屬性內(nèi)聲明自定義的Form。

這樣,再次調(diào)用wrapper.vm.save 方法的時候,就不會報validate方法未定義了。

注意:業(yè)務(wù)碼中調(diào)用的 refs.xxxname,其中 xxxnames 為模板中組件元素的ref名。但stub的時候需要聲明的是組件的名稱。比如上面的例子,業(yè)務(wù)代碼中refname為xxxname,組件名為Form。這時候需要stub模擬的是Form,而不是xxxname。

Parent組件模擬

與上面的場景相反,這次我們討論如果業(yè)務(wù)代碼中有關(guān)于parent組件的調(diào)用應(yīng)該如何編寫測試代碼。

this.$parent.getList()

雖然我認(rèn)為直接調(diào)用parent的方法不是一種良好的實(shí)踐,但是誰讓vue提供了這樣的能力呢,你就不能保證沒有人會這么用。對于這種場景的測試代碼,原理跟上面一個場景是類似的,只是在shallowMount的調(diào)用中有一點(diǎn)不同,我們看具體代碼

const Parent = {
  data: () => ({
    val: true
  }),
  methods: {
    getList: () => {} // 當(dāng)然如果業(yè)務(wù)代碼中需要依賴getList方法的返回,也可以在這里return result
  },
  template: '<div />'
}

// 接下來在shallowMount的時候聲明父組件即可
wrapper = shallowMount(indexTip, {
    parentComponent: Parent, // 聲明父組件為上面邊定義的Parent
    localVue,
    store,
    propsData: {}
})

mock異步請求

在測試中,經(jīng)常會碰到需要針對不同異步請求的結(jié)果編寫測試代碼的情況。

比如接口返回 code: 0 的時候做什么處理,code: 1的時候做什么處理,甚至是請求在網(wǎng)絡(luò)失敗的情況下做什么處理

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)
        })
}

如上代碼,我們要測試的函數(shù)叫做fn,其中調(diào)用了一個異步請求,那么我們在測試代碼中需要獲取到這個異步Promise對象才能對其內(nèi)部邏輯編寫測試代碼,因此fn函數(shù)需要返回這個Promise對象。

測試代碼如下

it('getMarkerList test case', async () => {
  await wrapper.vm.fn(xxx)
  expect(wrapper.vm.list).toBe([...somelist]) // 斷言此時vm.list 已被賦值成功
})

進(jìn)一步思考,既然是單元測試,是否不應(yīng)該依賴接口的返回,比如我們需要測試沒有網(wǎng)絡(luò)連接的情況下前端代碼的邏輯處理,總不能在持續(xù)集成過程中突然拔網(wǎng)線吧。

所以我們還需使用jest提供的mock promise能力

對Promise對象返回值的mock

上面介紹了mock Promise對象的方法,可以讓我們在沒有實(shí)際進(jìn)行網(wǎng)絡(luò)調(diào)用的情況下就觸發(fā)相應(yīng)的處理邏輯。

但大多數(shù)情況下,還要更加細(xì)分地對不同代碼分支做斷言。

比如

if (res.code === 10000) 
    ...
else if (res.code === 10002) {
    if (res.hasSelected === true) 
        ...
    else {
        ... 
    }
}

因此,jest提供了快捷的方法來進(jìn)行不同返回值的模擬

axios.get.mockResolvedValue({code: 10000, list: [1, 2, 3]}) // 模擬成功的狀態(tài)
await wrapper.vm.fn()
expect(wrapper.vm.list.length).toBe(3) // 斷言fn內(nèi)部promise調(diào)用成功時,list屬性應(yīng)該是一個三個元素的數(shù)組

axios.get.mockResolvedValue({code: 10001, msg: 'id not fount'}) // 模擬沒有找到該id的對應(yīng)記錄
await wrapper.vm.fn()
expect(wrapper.vm.fail).toBeCalledWith('id not found') // 斷言fn此場景下,fail方法被調(diào)用且參數(shù)為 ‘id not found’

axios.get.mockRejectedValue('networkError') // reject 的情況
await wrapper.vm.getLouDong()
expect(wrapper.vm.toast).toBeCalledWith('networkError')

模擬setTimeout

業(yè)務(wù)代碼中,可能會在方法內(nèi)部執(zhí)行setTimeout,過一定時間之后,再進(jìn)行某些操作,比如

submit () {
    this.toast('清空中')
  setTimeout(() => {
        this.loaded = []
  }, 300)
}

這個場景可能是出于用戶體驗(yàn)的考慮。

那么如果按照常規(guī)的方法,我們在執(zhí)行了getNewList方法之后就馬上去進(jìn)行斷言,則測試肯定是無法通過的。

針對這種情況,jest提供了一個有用的功能,useFakeTimers 。

在測試代碼中只需要聲明jest.useFakeTimers 然后再調(diào)用相應(yīng)方法,讓假的計時器向前或者向后若干毫秒即可,看代碼

it('submit test case', () => {
  jest.useFakeTimers()
  wrapper.vm.submit()
  jest.advanceTimersByTime(350)
  expect(wrapper.vm.loaded.length).toBe(0)
})

模擬window.location

在業(yè)務(wù)代碼中經(jīng)常會碰到需要使用當(dāng)前URL來進(jìn)行判斷的場景,但在jest中我們不能直接對window.location進(jìn)行賦值來模擬這個場景,如何做到?

比如我們需要根據(jù) location.search 中包含特定的參數(shù)值來執(zhí)行不同的代碼分支。

可以按照兩個步驟來進(jìn)行模擬:

  • 自定義window對象,覆蓋jsdom中的window對象
  • 通過defineProperty對window.location對象進(jìn)行賦值
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, 因此要注意其只在某一個測試用例中起作用,否則會導(dǎo)致其他測試用例失敗。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容