jest-vue前端自動化測試實踐03—非同步處理&mock

  • 2019 年 11 月 1 日
  • 筆記

jest-vue前端自動化測試實踐03—非同步處理&mock

[toc]

Write By CS逍遙劍仙 我的主頁: www.csxiaoyao.com GitHub: github.com/csxiaoyaojianxian Email: [email protected]

本節程式碼地址 https://github.com/csxiaoyaojianxian/JavaScriptStudy 下的自動化測試目錄

1. async 非同步請求處理

一般項目程式碼中會有不少非同步 ajax 請求,例如測試下面 async.js 中的程式碼

import axios from 'axios';  // 傳入 callback 函數進行處理  export const fetchData1 = (fn) => {      axios.get('http://www.csxiaoyao.com/api/data.json').then((response) => {          fn(response.data);      })  }  // 返回 promise 交給後續程式處理  export const fetchData2 = () => {      return axios.get('http://www.csxiaoyao.com/api/data.json')  }

新建測試用例文件 async.test.js 進行測試

import {fetchData1, fetchData2} from './async';  ...

【1】callback 中處理,需要手動結束 done,否則可能走不到 callback

test('fetchData1 返回結果為 { success: true }', (done) => {      fetchData1((data) => {          expect(data).toEqual({ success: true });          // 如果不寫 done(),當介面404會導致用例不執行          done();      })  })

【2】返回 promise

處理成功,需要指定返回 expect 數量,否則可能直接走失敗分支跳過

test('fetchData2 返回結果為 { success: true }', () => {      // 指定執行返回的 expect 數量      expect.assertions(1);      return fetchData2().then((response) => {          expect(response.data).toEqual({ success: true });      })  })

處理失敗,需要指定返回 expect 數量,否則可能直接走成功分支跳過

test('fetchData2 返回結果為 404', () => {      // 當介面不為404,則不會走catch      expect.assertions(1);      return fetchData2().catch((e) => {      	expect(e.toString().indexOf('404') > 1).toBe(true);      })  })

【3】promise – resolves / rejects 處理方式

promise – resolves

test('fetchData2 返回結果為 { success: true }', () => {      return expect(fetchData2()).resolves.toMatchObject({          data: { success: true }      });  })

promise-rejects

test('fetchData2 返回結果為 404', () => {      return expect(fetchData2()).rejects.toThrow();  })

【4】promise-async|await 處理方式

成功處理方式1

test('fetchData2 返回結果為 { success: true }', async () => {      await expect(fetchData2()).resolves.toMatchObject({          data: { success: true }      });  })

成功處理方式2

test('fetchData2 返回結果為 { success: true }', async () => {      const response = await fetchData2();      expect(response.data).toEqual({ success: true });  })

失敗處理方式1

test('fetchData2 返回結果為 404', async () => {      await expect(fetchData2()).rejects.toThrow();  })

失敗處理方式2

test('fetchData2 返回結果為 404', async () => {      expect.assertions(1);      try {          await fetchData2();      } catch (e) {          expect(e.toString().indexOf('404') > -1).toBe(true);      }  })

2. mock – ajax 模擬 ajax 請求

介面的正確性一般由後端自動化測試保證,前端自動化測試,一般需要 mock 觸發的 ajax 請求,例如測試 mock.js 中介面調用

export const getData = () => {      return axios.get('/api').then(res => res.data)  }

測試用例,jest.mock('axios') 模擬 axios 請求

import { getData } from './mock'  import axios from 'axios'  // jest 模擬 axios 請求  jest.mock('axios')  test('測試 axios getData', async () => {      // 模擬函數的返回,getData 不會真正發起 axios 請求      axios.get.mockResolvedValueOnce({ data: 'hello' })      axios.get.mockResolvedValueOnce({ data: 'world' })      // axios.get.mockResolvedValue({ data: 'hello' })      await getData().then((data) => {          expect(data).toBe('hello')      })      await getData().then((data) => {          expect(data).toBe('world')      })  })

3. __mocks__ 文件替換 ajax

如果需要測試 mock.js 中 ajax 請求

export const fetchData = () => {      return axios.get('/api').then(res => res.data) // '(function(){return 123})()'  }

除了上述方法指定 mock 函數和返回結果,還可以使用 mock 文件替換對應方法,讓非同步變同步,需要在 __mocks__ 文件夾下建立同名文件,如 __mocks__/mock.js

export const fetchData = () => {      return new Promise ((resolved, reject) => {          resolved('(function(){return 123})()')      })  }

測試用例,對於在 mock.js 但不在 __mocks__/mock.js 中的方法則不會被覆蓋

import { fetchData } from './mock'  jest.mock('./mock');  // jest.unmock('./08-mock2'); // 取消模擬  test('測試 fetchData', () => {      return fetchData().then(data => {          expect(eval(data)).toEqual(123);      })  })

還可以設置自動 mock,jest.config.js 中打開 automock: true,程式會自動在 mocks 文件夾下找同名文件,省去了手動調用 jest.mock('./mock');

4. mock – function 模擬函數調用

對於單元測試,無需關心外部傳入的函數的實現,使用 jest.fn 生成一個 mock 函數,可以捕獲函數的調用和返回結果,以及this和調用順序,例如測試 mock.js

export const runCallback = (callback) => {      callback(123)  }

測試用例

import { runCallback } from './mock'  test('測試 callback', () => {      // 【1】使用 jest 生成一個 mock 函數 func1,用來捕獲函數調用      const func1 = jest.fn()    	// 【2】模擬返回數據      // 1. mockReturnValue / mockReturnValueOnce      // func1.mockReturnValue(10)      func1.mockReturnValueOnce(456).mockReturnValueOnce(789)        // 2. 回調函數      const func2 = jest.fn(() => { return 456 })      // 等價於      func2.mockImplementation(() => { return 456 })      // func2.mockImplementationOnce(() => { return this })      // func2.mockReturnThis        // 【3】執行3次func1,1次func2      runCallback(func1)      runCallback(func1)      runCallback(func1)      runCallback(func2)        // 【4】斷言      // 被執行      expect(func1).toBeCalled()      // 調用次數      expect(func1.mock.calls.length).toBe(3)      // 傳入參數      expect(func1.mock.calls[0]).toEqual([123])      expect(func1).toBeCalledWith(123)      // 返回結果      expect(func2.mock.results[0].value).toBe(456)        // 【5】輸出mock,進行觀察      console.log(func1.mock)  })

輸出的 mock 為

{      calls: [ [ 123 ], [ 123 ], [ 123 ] ],      instances: [ undefined, undefined, undefined ],      invocationCallOrder: [ 1, 2, 3 ],      results: [          { type: 'return', value: 456 },          { type: 'return', value: 789 },          { type: 'return', value: undefined }      ]  }

5. mock – function 模擬 class 函數

對於單元測試,外部 class 的實現無需關心,使用 jest.fn 生成一個 mock 類,例如測試 mock.js

export const createObject = (classItem) => {      new classItem()  }

測試用例

import { createObject } from './mock'  test('測試 createObject', () => {      const func = jest.fn()      createObject(func)      console.log(func.mock)  })

輸出結果為

{      calls: [ [] ],      instances: [ mockConstructor {} ],      invocationCallOrder: [ 1 ],      results: [      	{ type: 'return', value: undefined }      ]  }

6. mock – class 模擬實例化 class

例如測試 func.js,從外部引入了 Util 類,但單元測試不關心 Util 的實現

import Util from './es6-class'  const demoFunction = (a, b) => {      const util = new Util()      util.a(a)      util.b(b)  }  export default demoFunction

有三種方案進行模擬

【1】jest.mock 真實 class 文件

jest.mock('./es6-class')

jest.mock 如果發現是一個類,會自動把構造函數和方法變成 jest.fn() 以提升性能,相當於執行了

const Util = jest.fn()  Util.a = jest.fn()  Util.b = jest.fn()

【2】自定義 jest.mock 傳參

jest.mock('./es6-class', () => {const Util = jest.fn() ... })

【3】在 __mocks__ 中編寫同名文件覆蓋

__mocks__ 文件除了可以替換 ajax 請求,還能替換 class 等,編寫 __mocks__/es6-class.js

const Util = jest.fn(() => { console.log('constructor --') })  Util.prototype.a = jest.fn(() => { console.log('a --') })  Util.prototype.b = jest.fn()  export default Util

編寫測試用例

import demoFunction from './func'  import Util from './es6-class'  test('測試 demo function', () => {      demoFunction()      expect(Util).toHaveBeenCalled()      expect(Util.mock.instances[0].a).toHaveBeenCalled()      expect(Util.mock.instances[0].b).toHaveBeenCalled()      console.log(Util.mock)  })

輸出 mock

{      calls: [ [] ],      instances: [ Util { a: [Function], b: [Function] } ],      invocationCallOrder: [ 1 ],      results: [ { type: 'return', value: undefined } ]  }

7. mock – timer 模擬定時器

例如測試 timer.js

export default (callback) => {      setTimeout(() => {          callback();          setTimeout(() => {              callback();          }, 3000);      }, 3000);  }

如果直接使用 done,需要等定時器執行,要等待較長時間,影響測試效率

test('測試 timer', (done) => {  	timer(() => {  		expect(1).toBe(1)  		done()  	})  })

因此需要使用 useFakeTimers / runAllTimers / runOnlyPendingTimers / advanceTimersByTime 來縮短 timers 時間,對於本案例

【1】定時器立即執行

jest.runAllTimers() // 執行2次

【2】只運行隊列中的timer

jest.runOnlyPendingTimers() // 執行1次

【3】快進x

jest.advanceTimersByTime(3000) // 快進3s

import timer from './timer'  // 各個用例之間定時器不影響  beforeEach(() => {      jest.useFakeTimers()  })  test('測試 timer', () => {      const fn = jest.fn()      timer(fn)      jest.advanceTimersByTime(3000) // 快進3s      expect(fn).toHaveBeenCalledTimes(1)      jest.advanceTimersByTime(3000) // 再快進3s      expect(fn).toHaveBeenCalledTimes(2)  })
sign.jpg