前端測試或許被好多人誤解,也許大家更加傾向于編寫面向后端的測試,邏輯性強,測試方便等
聊到這導致了好多前端從來不寫測試(測試全靠手點~~~)
其實沒必要達到測試驅動開發的程度,只要寫完代碼可以補測試,并且補出高效的測試,前端或許真的不需要手點
大前端時代不談環境不成方圓,本文從下面幾個環境一一分析下如何敏捷測試
理解測試前需要補充下單元測試(unit)和端到端測試(e2e)的概念,這里不贅述
推薦測試框架jest
jest 是 FB 的杰作之一,方便各種場景的 js 代碼測試,這里選擇 jest 是因為確實方便
使用方法及配置信息可以去官方文檔
配置的注意事項
{
testEnvironment: 'node' // 如不聲明默認瀏覽器環境
}
針對 node 只聊一下單元測試,e2e 測試比較少見
當決定寫一個 npm 模塊時,代碼完成后必不可少的就是單元測試,單元測試需要注意的問題比較瑣碎
當引入三方庫時,不得不 mock 數據,因為單元測試更多講求的是局部測試,不要受外界三方引入包的影響
例如:
const { readFileSync } = require('fs')
const getFile = () => {
try {
const text = readFileSync('text.txt', 'utf8')
} catch (err) {
throw new Error(err)
}
console.log(text)
}
module.exports = getFile
這時我們并不需要關心 text.txt
是否真的存在,也不需要關系 text
的內容具體是什么,我們的關注點應該在于讀取文件錯誤時能否及時拋出異常,以及 console.log()
是否如預期執行
對應到測試
const getFile = require('./getFile')
describe('readFile', () => {
const mocks = {
fs: {
readFileSync: jest.fn()
},
other: {
text: 'Test text'
}
}
beforeAll(() => {
jest.mock('fs', () => mocks.fs)
})
test('read file success run console.log', () => {
mocks.fs.readFileSync.mockImplementation(() => this.mocks.other.text)
getFile()
expect(console.log).toBeCalled()
})
})
上面代碼簡單的實現了一個讀取文件是否成功的測試,先別急著糾錯,這段測試本身是錯的,下面慢慢分析
我們在最開始創建了一個 mocks
對象,用來模擬數據,由于 readFileSync
方法可能存在多種返回結果(成功或報錯),所以暫時用 jest.fn()
模擬
other 里面則是放一些固定的測試數據(不會隨著測試過程而改變)
beforeAll
鉤子里面執行我們的 mock,把 require 進來的 fs 模塊攔截調,也是本測試用例中的關鍵步驟
在第一個 test 里面我們改寫 mocks.fs.readFileSync
的返回形式,這里使用的 mockImplementation
是直接模擬了一個執行函數,當然也可以模擬返回值,具體可以到 jest 官網
expect
用來斷言我們的 console.log
方法執行了
解釋了這么多測試新手們應該也都看的明白了,下面聊一下錯在哪,怎么改進
mockImplementation
最好替換為 mockReturnValueOnce
,注意這里出現了 Once 結尾,也就是僅模擬一次返回值, mockImplementation
最好使用在復雜場景,所謂的復雜就是我們手動實現一個 readFileSync
方法使得測試達到我們預期的目的,在這個簡單的場景里面我們只需要模擬返回值就好expect(console.log)
這里會報錯,因為 jest 斷言的內容只能是 mock function 或 spy,這里 console 是全局對象 global 上的方法,我們沒有 require 將其引入,所以 jest.mock 顯然處理上有些吃力,這時候 spy 就派上用場了, beforeAll
鉤子里直接執行 jest.spyOn(global.console, 'log')
,接下來我們就能監聽到 console.log
的執行了 expect(global.console.log)
console.log
的執行,這是不嚴謹的測試,我們需要使用 toBeCalledWith
來代替 toBeCalled
,不僅要測試執行了,而且要測試參數正確,簡單修改為 expect(global.console.log).toBeCalledWith(this.mocks.other.text)
下面補一下 read file 失敗的測試
test('read file fail throw error', () => {
mocks.fs.readFileSync.mockImplementationOnce(() => { throw new Error('readFile error') })
expect(getFile()).toThrow()
expect(global.console.log).not.toBeCalled()
})
讀取文件失敗的測試就好理解的多,注意的就是對一個 jest.fn()
多次進行修改會導致測試用例之間的相互影響,這里盡量使用 Once 結尾方法,復雜場景可以如下
beforeEach(() => { mocks.fs.readFileSync.mockReset() })
每次執行 test 前先清除 mock,避免多個測試用例之間復雜化 mock 導致錯誤
小結:單元測試中的 mock 是個測試思路,我們無需關心外部文件和依賴是什么,只要能模擬出正確的情況程序是否按規則執行,錯誤的情況程序是否有異常處理,邏輯是否正確等。這樣就能排除外界干擾,使得我們測試的當前一小部分是可靠的,穩定的即可。
單拿出一個小結說下 require 的問題,node 9 之前不支持 es6 的 import,這里也不詳細說明了。
require 本身并不復雜,但是如果搞不清楚執行時機,那么測試將無法進行,來一個例子
const env = process.env.NODE_ENV
module.export = () => env
測試如下
const getEnv = require('./getEnv')
describe('env', () => {
test('env will be dev', () => {
process.env.NODE_ENV = 'dev'
expect(getEnv()).toBe('dev')
})
test('env will be pord', () => {
process.env.NODE_ENV = 'pord'
expect(getEnv()).toBe('pord')
})
})
十分簡單的測試,拋開了 mock 的流程,這里會報測試未通過,原因是 require 同時 env 已經被賦值為 undefined
,我們再試著改變 NODE_ENV
環境變量時,程序不會再次執行,當然了,處理起來也十分簡單
let getEnv
test('env will be dev', () => {
process.env.NODE_ENV = 'dev'
getEnv = require('./getEnv')
expect(getEnv()).toBe('dev')
})
test('env will be pord', () => {
process.env.NODE_ENV = 'pord'
getEnv = require('./getEnv')
expect(getEnv()).toBe('pord')
})
順帶說了一下,希望大家不要在這種低級錯誤上浪費時間
其實引用外部文件還有些場景會對測試帶來困惑,比如動態路徑,場景如下
const packageFile = `${process.cwd()}/package.json`
const package = require(packageFile)
讀取當前路徑下的 package.json
,當測試真正跑到這段代碼時會到當前目錄下找 package.json
,這里盡量 mock 掉 package.json
為我們自己的模擬數據,但是 jest 不支持動態路徑的 mock,試著這樣寫 jest.mock(
${process.cwd()}/package.json , () => mockFile)
會報錯,所以盡量使用可以 mock 的方案,保證單元測試可以順利進行,修改如下
const path = require('path')
const filePath = path.join(process.cwd(), 'package.json')
這樣就可以 mock, path
了,和上面 mock 章節,大致思想都差不多
單元測試覆蓋率不達標等于白測,測試過程盡量覆蓋所有判斷條件,而不是全部通過了就不管了,在進一階說,100% 的測試覆蓋率并不證明一定覆蓋到位了,因為順帶執行的代碼也會算進覆蓋率,例如
module.export = (list) => list.map(({ id }) => id)
我們先不考慮這個 list 類型是不是數組,只是簡單的例子,避免過度設計帶來復雜化,我們測試可以這樣
const getId = require('./getId')
const mocks = {
list: [{
id: 1,
name: 'vue'
}, {
id: 2,
name: 'react'
}]
}
test('return id', () => {
expect(getId(mocks.list)).toEqual([1, 2])
})
直到有一天代碼變成了 module.export = (list) => [1, 2]
這時候測試還能通過,并且覆蓋率 100%,的確不會有人蠢到把代碼改成這樣,只是一個例子,實際上邏輯會比這個復雜的多
那就聊一聊解決方案
spyOn(Array.prototype, 'map')
然后斷言聊了一圈從覆蓋率聊到了測試健壯性的問題,可以思考下寫過的測試是否真的滿足注釋或修改任何一行代碼都能引起測試的 pass 報錯
關于 node 就聊這么多,其實下文主要思想都一樣,更多的是介紹些簡單可行的方案,以及可能會踩坑的地方
在 vue 使用場景下,無非就是組件庫和業務邏輯,組件庫偏向于 unit 測試,業務邏輯偏向于 e2e 測試,當然兩者并不沖突
推薦神器: vue-test-utils
README 給了多個測試庫配置的例子,這里還是推薦使用 jest,給個例子
export default {
props: ['value'],
data () {
return {
currentValue: 0
}
},
watch: {
value (val) {
this.currentValue = val
}
}
}
測試如下
import { mount } from '@vue/test-utils'
import Test from './Test.vue'
test('props value', () => {
const options = { propsData: { value: 3 } }
const wrapper = mount(Test)
expect(wrapper.vm.currentValue).toBe(3)
})
十分簡單的例子,亮點在測試文件的 wrapper 上,通過 mount
方法創建了一個組件實例,創建過程中允許加入一些配置信息,甚至是 mock 組件中的 method 方法
vue 單元測試的范圍僅限于數據流動是否正確,邏輯渲染是否正確(v-if v-show v-for),style 和 class 是否正確,我們并不需要關系這個組件在瀏覽器渲染中的位置,也不需要關系對其它組件會造成什么影響,只要保證組件本身正確即可,前面說的斷言,vue-test-utils 都能提供對應的方案,總體上節約很多測試成本
也是推薦尤大基于最新腳手架的 @vue/cli-plugin-e2e-nightwatch
e2e 測試的重點在于判斷真實 DOM 是否滿足預期要求,甚至很少出現 mock 場景,不可或缺的是一個瀏覽器運行環境,具體細節不贅述,可以看官方文檔。
nuxt 官方推薦 ava ,順勢帶出 ava 的方案
麻煩在配置上面,先給出需要安裝的依賴
"@vue/test-utils",
"ava",
"browser-env",
"require-extension-hooks",
"require-extension-hooks-babel",
"require-extension-hooks-vue",
"sinon"
在 package.json 里加幾行 ava 配置
"ava": {
"require": [
"./tests/helpers/setup.js"
]
}
下面來寫 ./tests/helpers/setup.js
const hooks = require('require-extension-hooks')
// Setup browser environment
require('browser-env')()
// Setup vue files to be processed by `require-extension-hooks-vue`
hooks('vue').plugin('vue').push()
// Setup vue and js files to be processed by `require-extension-hooks-babel`
hooks(['vue', 'js']).plugin('babel').push()
上面的代碼唯獨沒看到 sinon
這個庫,說到 ava 是沒有 mock 功能的,這就給單元測試的 mock 帶來巨大困難,不過我們可以通過引入 sinon
來解決 mock 數據的問題,在 mock 方面上 sinon
做的比 jest 還要優秀,支持沙箱模式,不影響外部數據
給個簡單點的例子
<template>
<el-card v-for="item in topicList" :key="item.id">
<div class="card-content">
<span class="link" @click="toMember(item.member.username)">{{ item.member.username }}</span>
</div>
</el-card>
</template>
<script>
export default {
props: {
topicList: {
type: Array,
required: true
}
},
methods: {
toMember (name) {
this.$router.push(`/member/${name}`)
}
}
}
</script>
對應的測試代碼如下
import { shallowMount } from '@vue/test-utils'
import test from 'ava'
import sinon from 'sinon'
test('methods: toMember', t => {
const { topicList } = t.context
const $router = {
push: () => {}
}
const spy = sinon.spy($router, 'push')
const wrapper = shallowMount(TopicListChalk, {
propsData: { topicList },
mocks: {
$router
}
})
topicList.forEach((item, index) => {
const toMemberText = wrapper.findAll('.card-content').at(index).find('.link')
toMemberText.trigger('click')
t.true(spy.withArgs(`/member/${item.member.username}`).calledOnce)
})
})
這里直接將 $router
mock 掉,并且使用 sinon.spy
監聽執行,至于 this.$router.push
后瀏覽器有沒有跳轉并不是單元測試需要關心的,這里的寫法也比較特別,test 方法在回調里默認參數為 t
,對應的方法都掛載在 t
對象上,上下文可通過 t.context
傳遞
nuxt 單元測試相關就聊這么多
這里有個歧義點,nuxt 官網只給出了 e2e 的測試案例end-to-end-testing
當使用默認腳手架構建的項目,也就是沒有 server 端入口文件的項目,這個方案確實可行
但是涉及到其它框架(express|koa)的時候就顯得不夠用了,很有可能在自定義 server 入口是加入了大量中間件,這對于官網給出的例子是個巨大考驗,不可能在每個測試文件里實現一遍 new Nuxt
,所以需要更高層的封裝,也就是忽略 server 啟動流程的差異性,直接在瀏覽器中抓取頁面
這一波沒得可選,jest 完勝,人家官網就有React,RN 的支持文檔
文檔的案例也是十分全面,沒得講,不贅述
其實上面講了兩個 e2e 的方案選擇,大同小異,需要一個能在 node 跑的無頭瀏覽器,官方沒有推薦,這里站 vue 一票選擇nightwatchjs
主要講一下如何配置,先是依賴包
"babel-core",
"babel-jest",
"enzyme",
"enzyme-adapter-react-16",
"jest",
"react-addons-test-utils",
"react-test-renderer"
在 package.json 里面加 script "test": "NODE_ENV=test jest"
在跟路徑下加 jest.config.js
module.exports = {
setupFiles: ['<rootDir>/jest.setup.js'],
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/']
}
在跟路徑下加 jest.setup.js
import { configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
configure({
adapter: new Adapter()
})
接下來就可以愉快的寫測試了
跳過了~~~
之所以加了這一節,還是因為多少寫過一些 angular,angular 作為框架本身就是全面的,cli 新建的項目自身就帶有 unit 測試和 e2e 測試
unit 測試默認是karma +jasmine e2e 測試默認是protractor
也沒什么可爭辯的,這就是官方解決方案,用起來也方便順手
聊了好多個環境,其實行文目的主要有兩方面
測試本身并不復雜,但是想寫出高效測試并不容易,千萬不要形成為了測試而測試的想法
用謊言去驗證謊言得到的還是謊言。。。
大多數情況下都是項目在趕進度沒空寫測試,抽空把測試補上真的是一件值得去做的事情
原文轉自:https://juejin.im/post/5b374d8c6fb9a00e2d480bfe