前端自動化測試詳解

1 前言

文章研究了四個問題:什麼是自動化測試、爲何要自動化測試、什麼項目適合自動化測試、自動化測試具體要怎麼作。在尋找這四個問題答案的過程當中,梳理了一套完整的前端自動化測試方案,包括:單元測試接口測試功能測試基準測試javascript

2 什麼是自動化測試

維基百科是這樣定義的html

在軟件測試中,測試自動化(英語:Test automation)是一種測試方法,使用特定的軟件,去控制測試流程,並比較實際的結果與預期結果之間的差別。經過將測試自動化,可讓正式的測試過程當中的必要測試,能夠反覆進行;經過這種方法,也能夠將難以手動進行的測試,交由軟件來作。前端

測試自動化的最大優點就是能夠快速並且反覆的進行測試。java

總結一下:自動化測試指軟件測試的自動化,讓軟件代替人工測試,能夠快速、反覆進行。node

關於自動化測試有一個金字塔理論,把測試從上到下分爲UI(用戶界面測試)/Service(服務測試) /Unit(單元測試 )。如圖所示,越往金字塔底層,測試的效率越高,測試質量保障程度越高,測試的成本越低。怎麼理解這句話呢?前端項目一般UI變化頻繁,一旦發生變化,UI測試用例就沒法執行且難以維護,因此UI自動化測試的成本高,收益小;相比UI測試,Service測試更加簡單直接且變化不會很頻繁;單元測試主要對公共函數、方法進行測試,測試用例複用度高且更能保證代碼質量。react

測試金字塔
在接下來的問題中,咱們所討論的自動化測試,主要指四個方向: 單元測試接口測試功能測試基準測試。所謂單元,能夠理解爲一個函數、一個react組件;接口即API,接口測試主要關注提供的接口是否可靠;功能能夠理解爲應用的UI、功能是否符合預期;基準測試能夠幫咱們測試代碼的性能。

3 實施自動化測試有什麼好處

測試最重要的目的是驗證代碼正確性,確保項目質量。舉個例子,某一天我寫了一個邏輯複雜的函數,這個函數被不少地方調用,過了一個月以後,我可能忘記這裏面的具體邏輯了,出於某種緣由須要爲這個函數增長一些功能,修改這個函數的代碼,那我要怎麼作才能保證修改代碼後不影響其餘的調用者呢,或者說,我要怎麼作,才能快速的知道哪些地方受影響,哪些地方不受影響呢?答案就是實施自動化測試,跑測試用例。git

若是不進行自動化測試,咱們會如何驗證代碼的正確性?一般FE使用的方法是手動測試:console、alert、打斷點、點點點。但手動測試是一次性的,若是下次有人對代碼功能作了修改,咱們不得再也不次重複手動測試的工做,而且很難保證測試的全覆蓋。但若是編寫測試用例進行自動化測試,第一次寫完的測試用例是能夠重複使用的,一次編寫,屢次運行。若是測試用例寫的完善、語義化,開發人員還能夠經過看測試用例快速瞭解項目需求。實施自動化測試能夠驅動開發人員在代碼的設計中作更好的抽象,寫可測試的代碼,以測試公用方法爲例,要確保被測試的方法無反作用,既對外部變量沒有依賴,也不會改變全局本來的狀態。github

總結一下,實施自動化測試有四個好處:web

  • 能夠驗證代碼正確性,保證項目質量正則表達式

  • 測試用例能夠複用,一次編寫,屢次運行

  • 經過看測試用例能夠快速瞭解需求

  • 驅動開發,指導設計,保證寫的代碼可測試

4 什麼樣的項目適合自動化測試

自動化測試如此優秀,那是否是全部項目都適合進行自動化測試?答案是否認的,由於有成本。在實施自動化測試以前須要對軟件開發過程進行分析,基於投入產出來判斷是否適合實施自動化測試。實施自動化測試的項目一般須要同時知足如下條件:

  • 需求變更不頻繁
  • 項目週期足夠長
  • 自動化測試腳本可重複使用
  • 代碼規範可測試

若是需求變更過於頻繁,維護測試腳本的成本過高;若是項目週期比較短,沒有足夠的時間去支持自動化測試的過程;若是測試腳本重複使用率低,耗費的精力大於創造的價值,不值得;若是代碼不規範,可測試性差,那自動化測試實施起來會比較困難。

5 自動化測試怎麼作

5.1 原始的測試方法

舉個例子,如今有一個方法sum

const sum = (a, b) => { return a + b }
複製代碼

如何證實sum方法的正確性?咱們一般會使用以下代碼進行測試

// test/util.test.js
const sum = (a, b) => { return a + b }
if(sum(1,1)===2){
    console.log('sum(1,1)===2,測試結果符合預期,方法正確')
}else{
    console.log('sum(1,1)===2,測試結果不符合預期,方法出錯')
}
複製代碼

執行測試代碼後控制檯輸出結果以下

符合預期的輸出

測試結果正確。假設如今把sum方法改成+1

const sum = (a, b) => { return a + b + 1 }
複製代碼

執行測試代碼後控制檯輸出結果以下

不符合預期的輸出

這個輸出雖然顯示了方法出錯的提示,可是對結果正確與錯誤沒有作明顯的區分,測試結論不夠直觀,咱們把測試代碼修改一下

// test/util.test.js
const sum = (a, b) => { return a + b + 1 }
if (sum(1, 1) === 2) {
  console.log(' sum(1,1)===2,測試結果符合預期,方法正確')
} else {
  throw new Error('sum(1,1)===2,測試結果不符合預期,方法出錯')
}
複製代碼

這段代碼執行後,一旦方法執行的結果不符合預期就主動拋出錯誤

不符合預期時拋錯

這樣就能更直觀的看出測試結論。咱們進一步優化,使用nodejs提供的斷言模塊來書寫測試用例

const sum = (a, b) => { return a + b + 1 }
const assert = require('assert')
assert.equal(sum(1, 1), 2)
複製代碼

執行測試代碼後控制檯結果以下

assert測試

輸出信息與剛纔的效果相似:執行結果不符合預期就主動拋出錯誤。使用assert達到了相同的效果,但代碼量減少了,而且更加語義化。

5.2 使用測試框架

上面的方法能夠幫助咱們完成代碼測試,那有沒有更好的方式呢?咱們開發項目時一般會選擇使用框架和庫,使用框架的好處是約束咱們代碼的風格,保證代碼的可維護性和擴展性,使用工具庫能夠提升開發效率。同理,在實施自動化測試時咱們也會選擇使用測試框架和庫。目前市面上比較流行的前端測試框架有Mocha、QUnit、Jasmine、Jest等,以下作個簡單介紹

經常使用的測試框架

框架能夠爲咱們輸出更加直觀的測試報告,好比像下面這樣,正確和錯誤的測試結果都給咱們展現

結構化的測試報告輸出

還能夠輸出文檔結構的測試報告,好比下面這樣

html格式的文檔輸出

5.3 測試方案技術選型

本文討論的自動化測試方案技術選型以下:

  • 測試框架:mocha
  • 斷言庫:chai
  • 測試報告:mochawesome
  • 測試覆蓋率:Istanbul
  • 測試瀏覽器:chrome
  • 瀏覽器驅動:selenium-webdriver/chrome
  • 接口測試http請求斷言:supertest
  • react組件測試:enzyme
  • 基準測試:benchmark

選擇Mocha是由於它:

  • 精簡而靈活,擴展性強
  • 社區成熟用的人多
  • 各類測試用例在社區都能找到

下面咱們經過一段測試用例來看一下Mocha有什麼能力:

Mocha的能力

能夠看到Mocha最核心的四項能力

  • 測試用例分組
  • 生命週期鉤子
  • 兼容不一樣風格斷言
  • 同步異步測試架構

代碼中describe塊稱爲「測試套件」,表示一組相關的測試,它是一個函數,第一個參數是測試套件的名稱("測試 sum 方法"),第二個參數是實際執行的函數,分組讓測試用例代碼結構化,易於維護。it塊稱爲"測試用例",表示一個單獨的測試,是測試的最小單位。它也是一個函數,第一個參數是測試用例的名稱("1 加 1 應該等於 2"),第二個參數是實際執行的函數。

選擇chai做爲斷言庫是由於它提供了兩種風格的斷言:BDD風格(行爲驅動開發)和TDD風格(測試驅動開發),其中BDD風格更接近天然語言。使用它能夠自由、靈活的與Mocha搭配,下圖是chai官網展現的兩種斷言風格。

chai斷言示例

5.4 測試方案代碼

下面開始梳理完整的自動化測試方案,總體目錄結構以下:

自動化測試方案項目結構

5.4.1 單元測試

(1)對以下方法進行單元測試

// /src/client/common/js/testUtil.js
export const sum = (a, b) => {
  return a + b
}
複製代碼

編寫好測試用例

import { sum } from '../../src/client/common/js/testUtil.js'
const { expect } = require('chai')

describe('單元測試: sum (a, b)', function () {
  it('1+1 應該等於 2', function () {
    expect(sum(1, 1)).to.be.equal(2)
  })
})
// skip能夠指定跳過某個分組
describe.skip('單元測試:金額按千分位逗號分隔的方法 formatMoney (s, type)', function () {...})

複製代碼

而後使用mocha執行測試用例,輸出結果以下

單元測試-公共方法

能夠看到兩個測試分組有一個測試經過,一個被咱們主動跳過。使用mocha執行測試用例時,由於咱們指定了測試報告格式--reporter參數爲mochawesome,測試報告會被輸出爲以下的html格式

單元測試報告

爲了分析當前測試用例對源代碼的覆蓋狀況,咱們使用Istanbul生成測試覆蓋率報告

單元測試代碼覆蓋率報告
代碼覆蓋率有四個測量維度:

  • 語句覆蓋率(statement coverage):是否每一個語句都執行了
  • 分支覆蓋率(branch coverage):是否每一個if代碼塊都執行了
  • 函數覆蓋率(function coverage):是否每一個函數都調用了
  • 行覆蓋率(line coverage):是否每一行都執行了

分別對應上圖的Statements、Branches、Functions、Lines,點擊左側連接能夠查看源碼測試詳情,綠色部分表示已被測試覆蓋

代碼覆蓋詳情

關於測試覆蓋率,須要強調的是,咱們不該該把測試覆蓋率的高低做爲檢驗項目質量的標準,只能做爲參考。代碼覆蓋率真正的意義在於幫助開發者找到代碼設計的問題,幫助咱們發現爲何有的代碼沒有被測試覆蓋到,是代碼設計有問題,仍是加入了無用代碼,它能夠指導咱們在代碼設計中作更好的抽象,寫可測試的代碼。

(2)React組件測試

如今有以下的React組件

// /src/client/components/Empty/index.jsx'
import React, { Component } from 'react'
import { Icon } from 'antd'

const Empty = (props) => {
  const placeholder = props.placeholder

  return (
    <div> <Icon type='meh-o' /> <span>{placeholder || '數據爲空'}</span> </div> ) } module.exports = Empty 複製代碼

編寫測試用例對它進行測試

import React from 'react'
import { expect } from 'chai'
import Enzyme, { mount, render, shallow } from 'enzyme'
import Adapter from 'enzyme-adapter-react-15.4' // 根據React的版本安裝適配器
import Empty from '../../src/client/components/Empty/index.jsx'
import { spy } from 'sinon' // 對原有的函數進行封裝並進行監聽

Enzyme.configure({ adapter: new Adapter() }) // 使用Enzyme 先適配React對應的版本

describe('測試React組件: <Empty />', () => {
  it('不傳入屬性時,組件中span的文本爲"數據爲空"', () => {
    const wrapper = render(<Empty />)
    expect(wrapper.find('span').text()).to.equal('數據爲空')
  })

  it('傳入屬性"我是佔位文本"時,組件中span的文本爲"我是佔位文本"', () => {
    const wrapper = render(<Empty placeholder='我是佔位文本' />)
    expect(wrapper.find('span').text()).to.equal('我是佔位文本')
  })
})

複製代碼

使用mocha執行測試用例會生成以下測試報告,測試經過

React組件測試報告

測試覆蓋率報告以下

React組件測試覆蓋率

React組件代碼覆蓋詳情

5.4.2 接口測試

編寫測試用例,使用supertest實施接口測試

const request = require('supertest')
const { expect } = require('chai')
const BASE_URL = 'http://127.0.0.1:1990'

describe('接口測試:商戶登陸測試用例', function () {
  it('登陸接口 /api/user/login', function (done) {
    request(BASE_URL)
      .post('/api/user/login')
      .set('Content-Type', 'application/json') // set header內容
      .send({ // send body內容
        user_code: 666666,
        password: 666666
      })
      .expect(200) // 斷言但願獲得返回http狀態碼
      .end(function (err, res) {
        // console.info(res.body) // 返回結果
        expect(res.body).to.be.an('object')
        expect(res.body.data.user_name).to.equal('商戶AAAAA')
        done()
      })
  })
})

複製代碼

執行接口測試用例生成以下測試報告

接口測試報告

接口測試報告

5.4.3 e2e測試

編寫e2e測試用例,使用selenium-webdriver驅動瀏覽器進行功能測試

const { expect } = require('chai')
const { Builder, By, Key, until } = require('selenium-webdriver')
const chromeDriver = require('selenium-webdriver/chrome')
const assert = require('assert')

describe('e2e測試:商戶系統端到端測試用例', () => {
  let driver
  before(function () {
    // 在本區塊的全部測試用例以前執行
    driver = new Builder()
      .forBrowser('chrome')
      // 設置無界面測試
      // .setChromeOptions(new chromeDriver.Options().addArguments(['headless']))
      .build()
  })

  describe.skip('登陸相關傳統用例-跳過', function () {...})

  describe('登陸商戶系統', function () {
    this.timeout(50000)
    it('登陸跳轉', async () => {
      await driver.get('http://dev.company.home.ke.com:1990/login') // 打開商戶登陸頁面
      await driver.findElement(By.xpath('//*[@id="root"]/div/div[2]/div/ul/li[1]/input')).sendKeys(666666) // 輸入用戶名
      await driver.findElement(By.xpath('//*[@id="root"]/div/div[2]/div/ul/li[2]/input')).sendKeys(666666) // 輸入密碼
      await driver.findElement(By.xpath('//*[@id="root"]/div/div[2]/div/div/button')).click() // 點擊登陸按鈕
      const currentTitle = await driver.getTitle()
      await driver.sleep(2000)
      expect(currentTitle).to.equal('商戶管理系統')
    })
  })

  after(() => {
    // 在本區塊的全部測試用例以後執行
    driver.quit()
  })
})
複製代碼

使用mocha執行e2e測試用例生成以下測試報告

e2e測試報告

下圖是selenium-webdriver驅動chrome瀏覽器自動運行,進行功能測試

e2e測試效果

驅動瀏覽器執行測試任務

5.4.4 基準測試

假設當前須要測試正則表達式的test方法和字符串的indexOf方法的性能,咱們一般會採用以下方法進行測試:讓兩個方法分別執行1000次,比較哪一個耗時長。

// 判斷某個字符串中是否存在特定字符,比較reg.test和str.indexOf性能
const testPerf = (count) => {
  var now = new Date() - 1
  var i = count
  while (i--) {
    /o/.test('Hello World!')
  }
  console.log(`test方法執行${count}次用時`, new Date() - 1 - now)
}

const indexOfPerf = (count) => {
  var now = new Date() - 1
  var i = count
  while (i--) {
    'Hello World!'.indexOf('o') > -1
  }
  console.log(`indexOf方法執行${count}次用時`, new Date() - 1 - now)
}

testPerf(1000)
indexOfPerf(1000)
複製代碼

測試結果以下,由於代碼執行較快,兩個方法執行1000次的時間都爲零,沒法準確判斷代碼執行效率

方法性能對比

科學的統計方法是須要屢次執行,對大量的執行結果進行採樣,咱們可使用工具幫咱們完成這件事,以下使用benchmark進行測試

// 判斷某個字符串中是否存在特定字符,比較reg.test和str.indexOf性能
const Benchmark = require('benchmark')
const suite = new Benchmark.Suite()

// add test
suite.add('正則表達式test方法', function () {
  /o/.test('Hello World!')
})
  .add('字符串indexOf方法', function () {
    'Hello World!'.indexOf('o') > -1
  })
  // add listeners
  .on('cycle', function (event) {
    console.log(String(event.target))
  })
  .on('complete', function () {
    console.log('Fastest is ' + this.filter('fastest').map('name'))
  })
  // run async
  .run({ 'async': true })
複製代碼

執行測試代碼,結果以下,indexOf每秒執行的次數比test每秒執行的次數超出了一個數量級,因此indexOf性能更好

基準測試結果

6 總結

梳理完單元測試、接口測試、功能測試、基準測試的具體實施方案後,結合自動化測試的特色咱們能夠得出如下結論:

前端要不要進行自動化測試,須要根據具體的項目特色進行判斷,對於知足如下條件的代碼能夠進行自動化測試:

  • 核心功能模塊、函數
  • 短時間不會發生變化的UI組件
  • 提供外部調用的接口
  • 對方法性能進行基準測試

最後,要強調一點,咱們的目標是保證代碼健壯、可維護,提升開發效率,自動化測試只是一種手段。

7 參考資料

相關文章
相關標籤/搜索