單元測試規範

原則

單元測試文件必須擁有良好的結構和格式;
測試用例的分組名稱和用例名稱必須清晰易懂;
測試用例必須能描述測試目標的行爲;
優先測試代碼邏輯(過程)而非執行結果;
單元測試的各項覆蓋率指標必須在95%以上;vue

技術

Jest:https://facebook.github.io/jestgit

結構

編寫單元測試所涉及的文件應存放於如下兩個目錄:github

  • __mocks__/:模擬文件目錄
  • [name].mock.json:【例】單個模擬文件
  • __tests__/:單元測試目錄
  • [target].test.js:【例】單個單元測試文件,[target]與目標文件名保持一致,當目標文件名爲index時,採用其上層目錄或模塊名。

[target].test.js 文件

應按照以下結構編寫測試文件,注意其中的空行:json

/* eslint global-require: 0 */
const thirdPartyModule = require('thrid-party-module')


describe('@zpfe/module-name' () => {
  const mocks = {}
  
  beforeAll(() => {})
  
  beforeEach(() => {})
  
  test('描述行爲', () => {
    mocks.fake.mockReturnValue('控制模擬行爲的代碼置於最上方')
    
    const target = require('../target.js')
    const result = target.foo('執行目標待測功能')
    
    expcet(result).toBe('斷言置於最下方')
  })
})

保證每一個describe內部只有mock對象、生命週期鉤子函數和test函數,將模擬對象都添加到mocks對象的適當位置,將初始化操做都添加到適當的生命週期函數中。koa

mocks 對象

常量mocks的結構以下:函數

const mocks = {
  zpfe: {
    // @zpfe模塊,如有,將包名轉換爲駝峯式以便訪問,好比:koaMiddleware
    log: {
      info: jest.fn()
    }
  },
  dependencies: {
    thirdPartyModule1: {
    // 第三方依賴模塊,如有
  },
  files: {
    // 本地依賴文件
    router: jest.fn()
  },
  others: {
    // 公共假對象
    ctx: jest.fn()
  }
}

請注意,mocks對象的價值在於保存模擬依賴項及部分複用對象,請勿添加不涉及模擬也沒有被複用的內容。單元測試

生命週期函數

在beforeAll中設置依賴模擬,好比:測試

beforeAll(() => {
  jest.mock('@zpfe/log', () => mocks.zpfe.log)
  jest.mock('../router.js', () => mocks.files.router)
  jest.spyOn(console, 'log')
})

在beforeEach中進行每一個單元測試運行前須要的重置工做,好比:ui

beforeEach(() => {
  process.env.NODE_ENV = 'production'
})

describe 函數

若模塊包含多個文件,則每一個文件對應專門的測試文件,其describe應這樣寫:eslint

describe('@zpfe/module-name: file-name' () => {})

目標對象

提倡在每一個test函數中require目標文件,若綜合評估以後,能肯定將require目標文件的代碼提取到生命週期鉤子函數中也不會產生干擾或混亂,則能夠考慮提取,好比:

describe('@zpfe/module-name' () => {
  let moduleName
  
  beforeEach(() => {
    moduleName = require('../target.js')
  })
})

test 函數

請用空行分隔test函數內不一樣目的的代碼塊(好比模擬、執行目標、和斷言)。

請勿在測試中編寫try...catch...,應明確斷言是否拋出異常,並根據須要斷言拋出的錯誤信息及日誌記錄狀況。

命名

describe表示分組,其名稱應屬於下列幾種狀況之一:

  • 模塊名,好比:@zpfe/module-name
  • 組件名,好比:a-input
  • 隸屬於模塊或組件的文件名,好比:a-input/nativa-control
  • 功能名,好比:props
  • 條件名,好比:當 NODE_ENV = production 時

test表示測試用例,其名稱應當明確表示其行爲,好比:當 disabled 屬性被設置爲非 Boolean 類型時,拋出異常。不容許將describe的命名規則應用到test。

良好的命名有助於組織測試用例,使其更能充當文檔之用。當某個測試用例失敗時,良好的結構和命名能讓讀者快速瞭解其影響範圍,好比:

[FAILED] a-input > props -> disabled -> 當傳入非 Boolean 類型的值時,拋出異常

模擬

請查閱Jest文檔,以詳細瞭解Jest所提供的各種模擬API。

臨時替換默認實現

若在mocks對象中初始化了實現,又須要在測試用例當中臨時修改其實現,能夠這樣作:

const mocks = {
  others: {
    foo: jest.fn(() => 'foo')
  }
}
test('demo', () => { 
  mocks.others.foo.mockImplementationOnce(() => 'bar') 
})

mockImementOnce會臨時修改默認實現,且只生效一次,故不會影響其餘測試用例。

若須要在測試用例當中臨時修改模擬函數的實現,且模擬函數會被屢次調用,就應該使用另一種方式實現,好比:

const mocks = {
  others: {
    foo: jest.fn()
  }
}
beforeEach(() => {
  mocks.others.foo.mockImplementation(() => 'foo')
})
test('demo', () => {
  mocks.others.foo.mockImplementation(() => 'bar')
})

即在mocks對象中只定義模擬函數,不定義具體實現,在beforeEach鉤子函數中定義具體實現,使得每一個測試用例都會從新初始化該實現,接着在具體測試用力中使用mockImementation完全替換掉默認實現。

斷言

請查閱Jest文檔,以詳細瞭解Jest所提供的各種斷言API。

斷言參數

若須要斷言調用函數時的參數傳遞,可以使用:

expect(mocks.zpfe.log.info).toHaveBeenCalledWith('觀察C ZooKeeper客戶端')

若須要部分匹配參數,可以使用:

expect(mocks.zpfe.log.info).toHaveBeenCalledWith(expect.stringContaining('觀察'), expect.objectContaining({ key: 'value' }))

調試

在VS Code中,打開測試文件,選中調試配置【調試 Jest 測試】,按【F5】便可。

Vue 組件測試

技術

@vue/test-utils:https://vue-test-utils.vuejs.org

結構

單元測試文件在__tests__目錄內的組織形式應與目標文件在src目錄保持一致,並按照以下順序結構組織組件的單元測試文件:

describe('組件:a-component-name', () => {
  const mocks = {}
 
  beforeAll()


  // 僅針對 props 定義進行基礎測試,不測試 props 如何使用
  describe('props', () => {
    describe('prop-name', () => {
      test('類型應爲 xxx')
      test('默認值應爲 xxx')
      test('有效性校驗')      
    })
  })
  
  // 僅針對可被用戶使用的嵌套組件族進行嵌套校驗測試
  describe('受限嵌套', () => {
    test('當父組件不爲 xxx 時,拋出異常')
    test('當子組件不爲 xxx 時,拋出異常')
  }) 
  
  // 僅針對 slots 渲染位置進行基礎測試
  describe('slots', () => {
    test('default')
    test('named-slot')
  })  
  
  // 根據實際狀況,結合 props 和 slots 進行各類場景下的渲染測試
  describe('render', () => {
    test('使用 prop-name 來渲染 xxx')
  })
  
  // 測試全部公開方法,不測試私有方法
  describe('methods', () => {
    describe('method-name', () => {
      test('行爲')
    })
  })
  
  // 觸發並測試全部事件是否正常觸發
  // 若 props 中包含 value,則 events 中必須包含 input
  describe('events', () => {
    describe('event-name', () => {
      test('當 xxx 時,觸發此事件')
    })
  }) 
  
  // 測試UI交互是否能正常響應(忽略與 events 測試雷同,則可忽略)
  describe('交互', () => {
    test('當點擊 xxx 時, 如此這般')
  })
}

掛載組件

按照以下規則掛載組件:

在掛載時傳遞 props;
掛載產生的對象應命名爲 e;
若組件須要使用原生DOM方法,請啓用 attachToDocument;

好比:

const target = mount(ComponentName, {
  propsData: {
    foo: 'bar'
  },
  attachToDocument: true
})

組件依賴關係

除非互相依賴的組件之間定義了嵌套校驗,不然優先考慮模擬子組件來進行父組件的測試。好比:

const target = mount(APaginationWithJumper, {
  stubs: {
    'dependent-component': true
  }
}
// 經過 target.find('dependent-component-stub').vm來模擬或控制其行爲
相關文章
相關標籤/搜索