使用Jest對Vue進(jìn)行自動化測試

上一篇文章中牍戚,講解了 Jest 的一些基本用法伦仍,理所當(dāng)然的娄蔼,我們需要應(yīng)用到實(shí)際項(xiàng)目中怖喻,這里以 Vue 舉例底哗,介紹一些 Jest 在 Vue 開發(fā)中的基本用法。

TDD vs BDD

在開始之前锚沸,我們需要先了解兩種編寫測試用例的方式跋选,以便在實(shí)際開發(fā)中選取合適的方式。

Test Driven Development (TDD) 測試驅(qū)動開發(fā)

TDD 的原理就是在編寫代碼之前先編寫測試用例哗蜈,由測試來決定我們的代碼前标,而且 TDD 更多的需要編寫?yīng)毩⒌臏y試用例,比如只測試一個組件的某個功能點(diǎn)距潘,某個工具函數(shù)等炼列。它是白盒測試。

開發(fā)流程大致是:編寫測試用例音比、運(yùn)行測試俭尖、編寫代碼使測試通過、優(yōu)化代碼洞翩。

TDD 的優(yōu)勢:從長期來看稽犁,可以有效減少回歸測試的 Bug;因?yàn)橄染帉憸y試骚亿,所以可能出現(xiàn)的問題都被提前發(fā)現(xiàn)了已亥;測試覆蓋率高,因?yàn)楹缶帉懘a循未,因此測試用例基本都能照顧到陷猫;保證代碼質(zhì)量。

TDD 的劣勢:因?yàn)閭?cè)重點(diǎn)在于代碼的妖,更多是保證某個測試單元沒問題绣檬,因此無法保證業(yè)務(wù)流程沒有問題;而且需求經(jīng)常變更嫂粟,在修改某個功能點(diǎn)之前要先修改測試用例娇未,因此在復(fù)雜的項(xiàng)目中工作量很大;測試代碼和實(shí)際代碼可能會出現(xiàn)耦合星虹,經(jīng)常需要修改零抬。

BDD (Behavior Driven Development) 行為驅(qū)動開發(fā)

BDD 是從產(chǎn)品角度出發(fā),它鼓勵開發(fā)人員和非開發(fā)人員之間的協(xié)作宽涌,是一種黑盒測試平夜。

開發(fā)流程大致是:獲悉需求并編寫代碼,然后再從用戶角度編寫集成測試卸亮。

BDD 的優(yōu)勢:它的測試重點(diǎn)更多是站在項(xiàng)目角度忽妒,在 UI 和 DOM 的角度進(jìn)行測試,直接地測試業(yè)務(wù)流程是否沒問題,測試代碼和實(shí)際代碼解耦段直。

BDD 的劣勢:因?yàn)槭羌蓽y試吃溅,因此不是那么關(guān)注每個函數(shù)功能,測試覆蓋率比較低鸯檬,沒有 TDD 那么嚴(yán)格的保證代碼質(zhì)量决侈。

Vue 中配置 Jest

在這里,直接借助了 Vue CLI 工具來初始化項(xiàng)目喧务,在初始化時會詢問是否使用單元測試赖歌,我們只需要按照步驟選擇,并選擇 Jest 即可蹂楣。

init.png

通過這種方式 Vue 會內(nèi)置 Vue Test Utils 幫助我們進(jìn)行測試

我們可以打開package.json文件修改"test:unit": "vue-cli-service test:unit"在后面加上--watch這樣就只測試發(fā)生變動的文件

安裝后俏站,項(xiàng)目目錄下會有一個jest.config.js文件,里面放的是 jest 相關(guān)的配置痊土,我們可以根據(jù)自己的需要修改之

module.exports = {
  // 依次找 js肄扎、jsx、json赁酝、vue 后綴的文件
  moduleFileExtensions: [
    'js',
    'jsx',
    'json',
    'vue'
  ],
  // 使用 vue-jest 幫助測試 .vue 文件
  // 遇到 css 等轉(zhuǎn)為字符串 不作測試
  // 遇到 js jsx 等轉(zhuǎn)成 es5
  transform: {
    '^.+\\.vue$': 'vue-jest',
    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
    '^.+\\.jsx?$': 'babel-jest'
  },
  // 哪些文件下的內(nèi)容不需要被轉(zhuǎn)換
  transformIgnorePatterns: [
    '/node_modules/'
  ],
  // 模塊的映射 @ 開頭到根目錄下尋找
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  // snapshot 怎么去存儲
  snapshotSerializers: [
    'jest-serializer-vue'
  ],
  // npm run test:unit 時到哪些目錄下去找 測試 文件
  testMatch: [
    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
  ],
  // 模擬的瀏覽器的地址是什么
  testURL: 'http://localhost/',
  // 兩個幫助使用 jest 的插件 過濾 測試名/文件名 來過濾測試用例
  watchPlugins: [
    'jest-watch-typeahead/filename',
    'jest-watch-typeahead/testname'
  ]
}

為了方便犯祠,修改了testMatch的配置:

  // 這條是自己新增的,測試 .vue 文件的測試覆蓋率
  // vue-cli-service test:unit --coverage 可以生成測試覆蓋率文件
  // 可以在 package.json 作如下配置
  // "test:coverage": "vue-cli-service test:unit --coverage"
  collectCoverageFrom: ['**/*.{vue}', '!**/node_modules/**'],
  testMatch: [
    // 進(jìn)行測試時匹配 __tests__ 目錄下的 js/jsx/ts/tsx 文件
    '**/__tests__/**/*.(js|jsx|ts|tsx)'
  ],
  // .eslintrc.js 不需要進(jìn)行測試
  testPathIgnorePatterns: [
    '.eslintrc.js'
  ],

同時我們要明確開發(fā)的目錄結(jié)構(gòu)酌呆,好的目錄組織可以幫助開發(fā)者更好的理解代碼衡载,以下是一種參考,當(dāng)然可以有別的方式隙袁,只要組織得容易理解就好

├── App.vue
├── assets
├── components # 基礎(chǔ)組件
├── views  # 項(xiàng)目主頁面
│   └── Article # Article 頁面
│       ├── ArticleHome.vue # Article 頁面組件
│       ├── __mocks__
│       │   └── axios.js # 需要 mock 的文件
│       ├── __tests__ # 測試目錄
│       │   ├── integration # 集成測試目錄
│       │   │   └── Article.js
│       │   └── unit # 單元測試目錄
│       │       └── store.js
│       └── components  # 頁面子組件
│           ├── MyHeader.vue
│           └── MyFooter.vue
├── main.js
├── store.js # Vuex store
└── utils # 工具函數(shù)

下面介紹幾種基礎(chǔ)的用法痰娱,更詳細(xì)的可以看官方文檔

使用 Jest 測試 Vue 項(xiàng)目

判斷 DOM 結(jié)構(gòu)是否發(fā)生改變

Vue-test-utils提供的shallowMount可以幫助我們掛載組件,但它不掛載子組件菩收,在組件單元測試中特別有用

另外mount方法則是把子組件也進(jìn)行掛載

返回的wrapper包含了所測試組件的屬性以及 vnode梨睁,我們可以借助它來測試 Vue

import { shallowMount } from '@vue/test-utils'
import MyHeader from '../../components/MyHeader.vue'

// __test__/unit/MyHeader.js
it('提示 MyHeader 樣式是否有發(fā)生變更', () => {
  const wrapper = shallowMount(MyHeader)
  expect(wrapper).toMatchSnapshot()
})

input

為了獲取 DOM,通常需要添加data-test=""屬性來獲取

我們可以編寫一個webpack plugin在生成production環(huán)境代碼的時候移除之

<!-- MyHeader.vue -->
<input
  data-test="input"
  v-model="inputValue"
  @keyup.enter="add"
/>
// __test__/unit/MyHeader.js

// 為了讓代碼看起來更清晰娜饵,我們可以使用 describe 包裹起來

describe('MyHeader 組件測試', () => {
  it('input 測試', () => {
    // 掛載 MyHeader 組件
    const wrapper = shallowMount(MyHeader)
    // 判斷是否存在 input
    // wrapper.findAll('[data-test="input"]').at(0) 取第一個元素
    const input = wrapper.find('[data-test="input"]')
    expect(input.exists()).toBe(true)

    // input 一開始內(nèi)容為空
    const inputValue = wrapper.vm.inputValue
    expect(inputValue).toBe('')

    // 模擬輸入了內(nèi)容
    input.setValue('name')

    // 模擬觸發(fā)回車事件
    input.trigger('keyup.enter')

    // 模擬向外發(fā)送了一個 add 事件
    expect(wrapper.emitted().add).toBeTruthy()

    // 模擬回車之后內(nèi)容為空
    expect(wrapper.vm.inputValue).toBe('')
  })
})

集成測試

相比于單元測試坡贺,集成測試從業(yè)務(wù)流程角度出發(fā),同時測試多個組件箱舞,保證整個用戶行為是沒有問題的遍坟。


it(`
  1. 在 input 輸入框輸入內(nèi)容
  2. 點(diǎn)擊回車按鈕
  3. 增加用戶輸入內(nèi)容的列表項(xiàng)
`, () => {
  const wrapper = mount(TodoList)
  const inputElem = wrapper.findAll('[data-test="input"]').at(0)
  const content = '今晚去踢波'
  inputElem.setValue(content)
  inputElem.trigger('change')
  inputElem.trigger('keyup.enter')

  // 這是從另外一個組件中獲取的
  const listItems = wrapper.findAll('[data-test="list-item"]')
  expect(listItems.length).toBe(1)
  expect(listItems.at(0).text(0)).toContain(content)
})

Vuex

測試 Store

import store from '../../../../store'

it('當(dāng) store commit change 時 value 發(fā)生變化', () => {
  const value = 'content'
  store.commit('change', value)
  expect(store.state.value).toBe(value)
})

組件中

// 在 mount 時把 store 傳入,這樣就能使用 store
const wrapper = mount(TodoList, { store })

異步測試

在組件掛載時晴股,我們經(jīng)常會加載遠(yuǎn)程數(shù)據(jù)來渲染頁面愿伴,這樣,渲染出來后的 DOM 就不是可以立即能獲取的电湘,因此應(yīng)該這樣測試:

// 因?yàn)槭褂玫?nextTick公般,因此需要傳入 done 參數(shù)
it(`
  1. 用戶打開頁面時万搔,請求遠(yuǎn)程數(shù)據(jù)
  2. 頁面渲染遠(yuǎn)程數(shù)據(jù)
`, (done) => {
  const wrapper = mount(TodoList, { store })

  // 只需要稍有延遲就能取到數(shù)據(jù)
  wrapper.vm.$nextTick(() => {
    // 此時可以獲取到渲染后的 DOM
    const listItems = wrapper.findAll('[data-test="list-item"]')
    expect(listItems.length).toBe(2)
    // 當(dāng) done 被執(zhí)行才結(jié)束測試
    done()
  })
})

mock 和 timer

假如我們使用了 axios,我們可以使用手動 mock 的方式官帘,不用真正地請求數(shù)據(jù),而是重寫獲取數(shù)據(jù)的實(shí)現(xiàn)昧谊,這樣可以省去每次遠(yuǎn)程獲取數(shù)據(jù)的時間刽虹。

// __mocks__/axios.js
const response = {
  errorCode: 0,
  data: [{ id: 0, name: 'name' }]
}

export default {
  get () {
    if (url === '/getData') {
      return new Promise((resolve, reject) => {
        if (this.errorCode === 0) {
          resolve(response)
        } else {
          reject(new Error())
        }
      })
    }
  }
}

// TodoList.vue
mounted() {
  setTimeout(() => {
    axios.get('/getData').then((res) => {
      this.data = res.data
    }).catch(e => {
      
    })
  }, 5000)
}

通過上面的 mock,當(dāng)我們請求/getData接口時就會先找到 mock 中模擬的請求呢诬,并返回模擬請求的中的數(shù)據(jù)

接下來就可以直接在測試中使用了涌哲,在這里再加了一個 5s 的延時,以演示如何測試有 timer 的情況

// 對 setTimeout 的統(tǒng)計(jì)都清零尚镰,避免測試之間相互影響
// 進(jìn)入導(dǎo)致 toHaveBeenCalledTimes 得不到預(yù)期值
beforeEach(() => {
  jest.useFakeTimers()
})

it(`
  1. 用戶打開頁面時阀圾,等待 5s,然后請求遠(yuǎn)程數(shù)據(jù)
  2. 頁面渲染遠(yuǎn)程數(shù)據(jù)
`, (done) => {
  const wrapper = mount(TodoList, { store })

  // 希望 setTimeout 被調(diào)用一次
  // 也可以使用 jest.advanceTimersByTime(5000) 來前進(jìn)多少秒
  expect(setTimeout).toHaveBeenCalledTimes(1)

  // 讓 timer 立即執(zhí)行
  jest.runAllTimers()

  // 獲取遠(yuǎn)程數(shù)據(jù)狗唉,在 nextTick 后把數(shù)據(jù)渲染出來
  wrapper.vm.$nextTick(() => {
    const listItems = wrapper.findAll('[data-test="list-item"]')
    expect(listItems.length).toBe(2)
    done()
  })
})

如果要測試請求出現(xiàn)失敗的情況初烘,我們可以這樣做:

import axios from '../../__mocks__/axios'

beforeEach(() => {
  // 每個測試用例之前,都把請求設(shè)為成功
  axios.errorCode = 0
})

it(`測試失敗`, () => {
  // 在這個測試用例中分俯,把請求設(shè)為失敗
  // 錯誤碼是自己定的
  axios.errorCode = 10000
})

小結(jié)

我們需要明確單元測試肾筐、集成測試、TDD缸剪、BDD幾個概念吗铐,針對不同的情況使用不同的測試方式,比如測試工具函數(shù)可以用 TDD 的測試方式杏节,面對復(fù)雜的項(xiàng)目唬渗,我們需要保證用戶的體驗(yàn),就可以使用 BDD 的測試方式奋渔,他們之間不是對立的镊逝,我們可以在項(xiàng)目中靈活地使用它們,把它拓展到團(tuán)隊(duì)中卒稳,讓自動化測試達(dá)到最佳實(shí)踐蹋半。如果測試寫得好,那么測試本身就已經(jīng)是一份文檔了充坑,能保證項(xiàng)目在迭代中的代碼質(zhì)量减江,在多人協(xié)同開發(fā)中特別有用。

官方文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末捻爷,一起剝皮案震驚了整個濱河市辈灼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌也榄,老刑警劉巖巡莹,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件司志,死亡現(xiàn)場離奇詭異,居然都是意外死亡降宅,警方通過查閱死者的電腦和手機(jī)骂远,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來腰根,“玉大人激才,你說我怎么就攤上這事《詈伲” “怎么了瘸恼?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長册养。 經(jīng)常有香客問我东帅,道長,這世上最難降的妖魔是什么球拦? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任靠闭,我火速辦了婚禮,結(jié)果婚禮上刘莹,老公的妹妹穿的比我還像新娘阎毅。我一直安慰自己,他們只是感情好点弯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布扇调。 她就那樣靜靜地躺著,像睡著了一般抢肛。 火紅的嫁衣襯著肌膚如雪狼钮。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天捡絮,我揣著相機(jī)與錄音熬芜,去河邊找鬼。 笑死福稳,一個胖子當(dāng)著我的面吹牛涎拉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播的圆,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼鼓拧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了越妈?” 一聲冷哼從身側(cè)響起季俩,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎梅掠,沒想到半個月后酌住,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體店归,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年酪我,在試婚紗的時候發(fā)現(xiàn)自己被綠了消痛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡都哭,死狀恐怖肄满,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情质涛,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布掰担,位于F島的核電站汇陆,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏带饱。R本人自食惡果不足惜毡代,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望勺疼。 院中可真熱鬧教寂,春花似錦、人聲如沸执庐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轨淌。三九已至迂烁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間递鹉,已是汗流浹背盟步。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留躏结,地道東北人却盘。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拓诸。 傳聞我的和親對象是個殘疾皇子信不,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評論 2 355