淺談前端測試

前端測試或許被好多人誤解,也許你們更加傾向於編寫面向後端的測試,邏輯性強,測試方便等html

聊到這致使了好多前端歷來不寫測試(測試全靠手點~~~)前端

其實不必達到測試驅動開發的程度,只要寫完代碼能夠補測試,而且補出高效的測試,前端或許真的不須要手點vue

大前端時代不談環境不成方圓,本文從下面幾個環境一一分析下如何敏捷測試node

  • node 環境
  • vue 環境
  • nuxt 服務端渲染環境
  • react 環境
  • next 服務端渲染環境
  • angular 環境

理解測試前須要補充下單元測試(unit)和端到端測試(e2e)的概念,這裏不贅述react

node 環境

推薦測試框架 jestgit

jest 是 FB 的傑做之一,方便各類場景的 js 代碼測試,這裏選擇 jest 是由於確實方便es6

使用方法及配置信息能夠去官方文檔github

配置的注意事項express

{
  testEnvironment: 'node' // 如不聲明默認瀏覽器環境
}
複製代碼

針對 node 只聊一下單元測試,e2e 測試比較少見npm

當決定寫一個 npm 模塊時,代碼完成後必不可少的就是單元測試,單元測試須要注意的問題比較瑣碎

mock

當引入三方庫時,不得不 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 方法執行了

解釋了這麼多測試新手們應該也都看的明白了,下面聊一下錯在哪,怎麼改進

  1. mockImplementation 最好替換爲 mockReturnValueOnce,注意這裏出現了 Once 結尾,也就是僅模擬一次返回值,mockImplementation 最好使用在複雜場景,所謂的複雜就是咱們手動實現一個 readFileSync 方法使得測試達到咱們預期的目的,在這個簡單的場景裏面咱們只須要模擬返回值就好
  2. 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)
  3. 斷言的目的是測試 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%,的確不會有人蠢到把代碼改爲這樣,只是一個例子,實際上邏輯會比這個複雜的多

那就聊一聊解決方案

  • mock 數據的隨機化,每次測試生成隨機的 list 進行測試,現有庫 mockjs
  • 強關聯測試,證實 map 方法的確執行了,而且參數正確,先 spy spyOn(Array.prototype, 'map') 而後斷言

聊了一圈從覆蓋率聊到了測試健壯性的問題,能夠思考下寫過的測試是否真的知足註釋或修改任何一行代碼都能引發測試的 pass 報錯

關於 node 就聊這麼多,其實下文主要思想都同樣,更多的是介紹些簡單可行的方案,以及可能會踩坑的地方

vue 環境

在 vue 使用場景下,無非就是組件庫和業務邏輯,組件庫偏向於 unit 測試,業務邏輯偏向於 e2e 測試,固然二者並不衝突

unit 測試

推薦神器: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 都能提供對應的方案,整體上節約不少測試成本

e2e 測試

也是推薦尤大基於最新腳手架的 @vue/cli-plugin-e2e-nightwatch

e2e 測試的重點在於判斷真實 DOM 是否知足預期要求,甚至不多出現 mock 場景,不可或缺的是一個瀏覽器運行環境,具體細節不贅述,能夠看官方文檔。

nuxt 服務端渲染環境

nuxt 官方推薦 ava,順勢帶出 ava 的方案

unit 測試

麻煩在配置上面,先給出須要安裝的依賴

"@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 單元測試相關就聊這麼多

e2e 測試

這裏有個歧義點,nuxt 官網只給出了 e2e 的測試案例 end-to-end-testing

當使用默認腳手架構建的項目,也就是沒有 server 端入口文件的項目,這個方案確實可行

可是涉及到其它框架(express|koa)的時候就顯得不夠用了,頗有可能在自定義 server 入口是加入了大量中間件,這對於官網給出的例子是個巨大考驗,不可能在每一個測試文件裏實現一遍 new Nuxt,因此須要更高層的封裝,也就是忽略 server 啓動流程的差別性,直接在瀏覽器中抓取頁面

推薦:nuxt-jest-puppeteer

react 環境

unit 測試

這一波沒得可選,jest 完勝,人家官網就有 React,RN 的支持文檔

文檔的案例也是十分全面,沒得講,不贅述

e2e 測試

其實上面講了兩個 e2e 的方案選擇,大同小異,須要一個能在 node 跑的無頭瀏覽器,官方沒有推薦,這裏站 vue 一票選擇 nightwatchjs

next 服務端渲染環境

unit 測試

主要講一下如何配置,先是依賴包

"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()
})
複製代碼

接下來就能夠愉快的寫測試了

e2e 測試

跳過了~~~

angular 環境

之因此加了這一節,仍是由於多少寫過一些 angular,angular 做爲框架自己就是全面的,cli 新建的項目自身就帶有 unit 測試和 e2e 測試

unit 測試默認是 karma + jasmine e2e 測試默認是 protractor

也沒什麼可爭辯的,這就是官方解決方案,用起來也方便順手

總結

聊了好多個環境,其實行文目的主要有兩方面

  • 測試思想,如何寫好單元測試,主要集中在前半文
  • 測試工具推薦和相應配置

測試自己並不複雜,可是想寫出高效測試並不容易,千萬不要造成爲了測試而測試的想法

用謊話去驗證謊話獲得的仍是謊話。。。

大多數狀況下都是項目在趕進度沒空寫測試,抽空把測試補上真的是一件值得去作的事情

相關文章
相關標籤/搜索