文章研究了四個問題:什麼是自動化測試、爲何要自動化測試、什麼項目適合自動化測試、自動化測試具體要怎麼作。在尋找這四個問題答案的過程當中,梳理了一套完整的前端自動化測試方案,包括:單元測試、接口測試、功能測試、基準測試。javascript
維基百科是這樣定義的html
在軟件測試中,測試自動化(英語:Test automation)是一種測試方法,使用特定的軟件,去控制測試流程,並比較實際的結果與預期結果之間的差別。經過將測試自動化,可讓正式的測試過程當中的必要測試,能夠反覆進行;經過這種方法,也能夠將難以手動進行的測試,交由軟件來作。前端
測試自動化的最大優點就是能夠快速並且反覆的進行測試。java
總結一下:自動化測試指軟件測試的自動化,讓軟件代替人工測試,能夠快速、反覆進行。node
關於自動化測試有一個金字塔理論,把測試從上到下分爲UI(用戶界面測試)/Service(服務測試) /Unit(單元測試 )。如圖所示,越往金字塔底層,測試的效率越高,測試質量保障程度越高,測試的成本越低。怎麼理解這句話呢?前端項目一般UI變化頻繁,一旦發生變化,UI測試用例就沒法執行且難以維護,因此UI自動化測試的成本高,收益小;相比UI測試,Service測試更加簡單直接且變化不會很頻繁;單元測試主要對公共函數、方法進行測試,測試用例複用度高且更能保證代碼質量。react
在接下來的問題中,咱們所討論的自動化測試,主要指四個方向: 單元測試、 接口測試、 功能測試、 基準測試。所謂單元,能夠理解爲一個函數、一個react組件;接口即API,接口測試主要關注提供的接口是否可靠;功能能夠理解爲應用的UI、功能是否符合預期;基準測試能夠幫咱們測試代碼的性能。測試最重要的目的是驗證代碼正確性,確保項目質量。舉個例子,某一天我寫了一個邏輯複雜的函數,這個函數被不少地方調用,過了一個月以後,我可能忘記這裏面的具體邏輯了,出於某種緣由須要爲這個函數增長一些功能,修改這個函數的代碼,那我要怎麼作才能保證修改代碼後不影響其餘的調用者呢,或者說,我要怎麼作,才能快速的知道哪些地方受影響,哪些地方不受影響呢?答案就是實施自動化測試,跑測試用例。git
若是不進行自動化測試,咱們會如何驗證代碼的正確性?一般FE使用的方法是手動測試:console、alert、打斷點、點點點。但手動測試是一次性的,若是下次有人對代碼功能作了修改,咱們不得再也不次重複手動測試的工做,而且很難保證測試的全覆蓋。但若是編寫測試用例進行自動化測試,第一次寫完的測試用例是能夠重複使用的,一次編寫,屢次運行。若是測試用例寫的完善、語義化,開發人員還能夠經過看測試用例快速瞭解項目需求。實施自動化測試能夠驅動開發人員在代碼的設計中作更好的抽象,寫可測試的代碼,以測試公用方法爲例,要確保被測試的方法無反作用,既對外部變量沒有依賴,也不會改變全局本來的狀態。github
總結一下,實施自動化測試有四個好處:web
能夠驗證代碼正確性,保證項目質量正則表達式
測試用例能夠複用,一次編寫,屢次運行
經過看測試用例能夠快速瞭解需求
驅動開發,指導設計,保證寫的代碼可測試
自動化測試如此優秀,那是否是全部項目都適合進行自動化測試?答案是否認的,由於有成本。在實施自動化測試以前須要對軟件開發過程進行分析,基於投入產出來判斷是否適合實施自動化測試。實施自動化測試的項目一般須要同時知足如下條件:
若是需求變更過於頻繁,維護測試腳本的成本過高;若是項目週期比較短,沒有足夠的時間去支持自動化測試的過程;若是測試腳本重複使用率低,耗費的精力大於創造的價值,不值得;若是代碼不規範,可測試性差,那自動化測試實施起來會比較困難。
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
達到了相同的效果,但代碼量減少了,而且更加語義化。
5.2 使用測試框架
上面的方法能夠幫助咱們完成代碼測試,那有沒有更好的方式呢?咱們開發項目時一般會選擇使用框架和庫,使用框架的好處是約束咱們代碼的風格,保證代碼的可維護性和擴展性,使用工具庫能夠提升開發效率。同理,在實施自動化測試時咱們也會選擇使用測試框架和庫。目前市面上比較流行的前端測試框架有Mocha、QUnit、Jasmine、Jest等,以下作個簡單介紹
框架能夠爲咱們輸出更加直觀的測試報告,好比像下面這樣,正確和錯誤的測試結果都給咱們展現
還能夠輸出文檔結構的測試報告,好比下面這樣
5.3 測試方案技術選型
本文討論的自動化測試方案技術選型以下:
選擇Mocha是由於它:
下面咱們經過一段測試用例來看一下Mocha
有什麼能力:
能夠看到Mocha最核心的四項能力
代碼中describe
塊稱爲「測試套件」,表示一組相關的測試,它是一個函數,第一個參數是測試套件的名稱("測試 sum 方法"),第二個參數是實際執行的函數,分組讓測試用例代碼結構化,易於維護。it
塊稱爲"測試用例",表示一個單獨的測試,是測試的最小單位。它也是一個函數,第一個參數是測試用例的名稱("1 加 1 應該等於 2"),第二個參數是實際執行的函數。
選擇chai做爲斷言庫是由於它提供了兩種風格的斷言:BDD風格(行爲驅動開發)和TDD風格(測試驅動開發),其中BDD風格更接近天然語言。使用它能夠自由、靈活的與Mocha搭配,下圖是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
生成測試覆蓋率報告
分別對應上圖的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執行測試用例會生成以下測試報告,測試經過
測試覆蓋率報告以下
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測試用例生成以下測試報告
下圖是selenium-webdriver
驅動chrome瀏覽器自動運行,進行功能測試
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
性能更好
梳理完單元測試、接口測試、功能測試、基準測試的具體實施方案後,結合自動化測試的特色咱們能夠得出如下結論:
前端要不要進行自動化測試,須要根據具體的項目特色進行判斷,對於知足如下條件的代碼能夠進行自動化測試:
最後,要強調一點,咱們的目標是保證代碼健壯、可維護,提升開發效率,自動化測試只是一種手段。