前端測試(上)
在前端項目中,前端測試並沒有被重視,原因有很多,比如 學習/研發成本高,團隊不夠重視,或者項目不合適等,在這裏我們不去追究是什麼原因導致這種現象,但是有一點我很確定,造成這種原因,還有一個更重要的原因,就是 “意識不到位”,即使有很多同學瞭解過單元測試,但是也不知道如何應用到 “項目” 中,針對這種現象,我們從一個簡單卻很常見的小項目,來打開測試工程化冰山一角
在刷題的過程中,我們經常會使用一個項目用於練習寫筆試題,比如排序,查找之類的算法題目
石器時代
新建一個工程,目錄如下
├── index.js
├── index.html
└── src
└── search.js
- index.html - 基於瀏覽器運行,至少早期的筆試題目,基於瀏覽器即可;此例,引入
search.js和index.js即可 - index.js - 用於寫測試代碼,比如
console.log(binarySearch([1],1)===0) - search.js - 用於寫查找相關的算法代碼,比如順序查找、插值查找等,在這裏我只寫
二分法查找和順序查找函數
/**
* 二分法
* @param {Array} arr
*/
function binarySearch(arr, expected) {
let start = 0
let end = arr.length - 1
while (start <= end) {
let mid = parseInt(start + (end - start) / 2)
let value = arr[mid]
if (value === expected) {
return mid
} else if (value > expected) {
end = mid
} else {
start = mid
}
}
return -1
}
/**
* 順序查找
* @param {*} arr
* @param {*} expected
*/
function sequentialSearch(arr, expected) {
let i = 0
while (i > arr.length) {
let value = arr[i]
if (value === expected) {
return i
}
i++
}
return -1
}
OK,大功告成,把頁面拖到瀏覽器中直接運行,連服務器都省了~~!
當我準備為我這個完美的項目鼓掌的時候,眼角瞟到我的座右銘,成為一個專業的大前端,此時此刻 專業這個詞格外刺眼,作為新世紀好青年,我怎麼可能讓別人質疑我的專業,於是我要繼續裝(入)逼(坑)
青銅時代
- 使用npm創建工程,並且對模塊進行管理
- 使用git對項目倉庫以及迭代進行管理
- 載體nodejs環境代替瀏覽器環境,這樣可以測試文件讀取等IO操作
.
├── node_modules
└── test
└── test.js
└── src
└── search.js
├── package.json
├── .gitignore
├── index.js
在 package.json 配置
{
....
"scripts":{
"test":"node test/test.js"
}
}
對應 js的模塊 要改成commonjs規範
search.js 調整
function binarySearch(){
//todo
}
function sequentialSearch(){
//todo
}
module.exports = {
binarySearch,
sequentialSearch
}
index.js 調整
const { binarySearch,sequentialSearch } = require('./src/search')
module.exports = {
binarySearch,
sequentialSearch
}
test.js 調整,為了讓提示更加明顯點,我們嘗試讓描述更加豐富點
const { binarySearch,sequentialSearch } = require('../index')
console.log(‘二分查找: [1]的1在數組0位置上’,binarySearch([1],1)===0)
console.log(‘二分查找:[1,2,3]的1在數組0位置上’,binarySearch([1,2,3],1)===0)
console.log(‘二分查找:[1,2,3]的2在數組1位置上’,binarySearch([1,2,3],2)===0)
console.log(‘順序查找:[1]的1在數組0位置上’,sequentialSearch([1],1)===0)
console.log(‘順序查找:[1,2,3]的1在數組0位置上’,sequentialSearch([1,2,3],1)===0)
console.log(‘順序查找:[1,2,3]的2在數組1位置上’,sequentialSearch([1,2,3],2)===0)
一頓操作猛如虎之後,感覺完美的一筆~~
我迫不及待,運行 npm run test
二分查找:[1]的1在數組0位置上 true
二分查找:[1,2,3]的1在數組0位置上 true
二分查找:[1,2,3]的2在數組1位置上 false
順序查找:[1]的1在數組0位置上 false
順序查找:[1,2,3]的1在數組0位置上 false
順序查找:[1,2,3]的2在數組1位置上 false
我們發現 有幾點不足:
- 當測試用例增加時,測試代碼變的難以管理,所有測試輸出揉在一起,沒有分組的管理
- 不管成功或者失敗,沒有高亮顯示,也沒有顏色上的區分
- 即使錯誤,也沒有把錯誤詳細打印出來
- 沒有相關輸出報表
黃金時代
為了解決 青銅時代 遺留下不少體驗問題,我們不得不封裝一些方法,強化console的輸出,文檔輸出,可視化等輸出,然而我們所做的一切強化,都是新概念 測試框架的雛形,不過在正式介紹 測試框架前,我們先了解下 斷言
“我×,測試框架?斷言?這尼瑪又是什麼?”
斷言是單元測試中用來保證最小單元是否正常的檢測方法,用於判斷邏輯執行是否達到開發者預期的表達式,斷言在運行的過程中,若斷言不為真,程序會中止運行
“常用的斷言庫有哪些?”
- assert - nodejs的內置核心模塊,node環境可以直接使用
- shouldjs - 基於assert模塊進行封裝擴展
- expectjs - 基本是 shouldjs 的縮水版
- chai - 目前比較流行的斷言庫,支持 TDD(assert),BDD(expect、should)兩種風格
我們先簡單學習 assert, 作為Nodejs內置核心模塊,無需引用,最為 斷言 入門庫最為合適
## assert
var assert=require('assert')
assert.equal(Math.max(1,100),100)
一旦 assert.equal()不滿足期望,將會拋出AssertionError異常,整個程序將會停止運行
常用的檢測方法
- ok(actual) - 判斷結果是否為真
- strictEqual(actual,expected,[,message]) - 判斷實際值和期望值是否嚴格相等
- deepStrictEqual(actual, expected[, message]) -判斷實際值和期望值是否深度嚴格相等
- doesNotReject(asyncFn, error) - 判斷代碼塊是否返回reslove
- rejects(block, error)- 判斷結果返回reject
- throws(block, error)- 判斷結果是否拋出異常
- ifError()- 判斷實際值是否為一個假值(null,undefined,0,'',false);如果為真值,就會拋出異常
- fail([message]) - 直接報錯
“感覺腦殼疼,能不能通俗點?”
先來一個例子,壓壓驚,我們把青銅時代的代碼優化下
console.log(‘順序查找:[1]的1在數組0位置上’,sequentialSearch([1],1)===0)
//為了通用性,我們把sequentialSearch([1],1)===0 提煉出來
function equal(actual,expected,message){
return actual===expected?message:`${actual}!==${expected}`
}
console.log(‘順序查找:[1]的1在數組0位置上’,equal(sequentialSearch([1],1),0,'成功'))
通俗的説 就是 equal這個方法就是斷言
“我迷迷糊糊的貌似明白了一點,那我運行一下嚐嚐鮮吧”
test/index.js
const assert = chai.assert
const { binarySearch } = require('../index')
assert.equal(binarySearch([1], 1), 0)//成功
assert.equal(binarySearch([1], 1), 1)//失敗
assert.equal(binarySearch([1,2], 2), 1)//成功
assert.equal(binarySearch([1,2], 1), 0)//失敗
運行 node test/index.js
//失敗輸出
AssertionError: expected 0 to equal 1
at Object.<anonymous> (F:\learn\test\index.js:19:8)
“呃....我覺得這體驗,也青銅時代差不多”
我們可以看到,在第二個測試用例執行時,發現代碼執行失敗後,直接退出程序,同時提示你 期望值和實際運行值,以及對於錯誤代碼相關提示等等。錯誤提示方面比封裝equire方法強大不少;但是,依舊不能讓我願意使用它。
- 成功時,沒有任何提示
- 雖然有錯誤提示,但是運行到第一個錯誤的時候,就程序退出;開發者無法看到 自己的測試用例 錯誤多少個
- 依舊沒有高亮,可視化方面依舊蒼白
沒錯,斷言拿到非常重要錯誤信息;但是他沒有解決體驗問題;如果説 斷言是裏子,那測試框架 就是面子
“測試框架是什麼?”
測試框架 通俗的説就是專門 服務於代碼塊測試 的解決方案,他主要有以下功能
- 管理測試用例
- 生成測試報告
“常用的測試框架有哪些?”
- [jasmine]() -自帶斷言(assert),mock 功能
- [mocha]() -框架不帶斷言和mock功能,需要結合其他工具
通俗的説,測試框架 就是 管理/執行斷言,他和斷言一起使用將會更加強大
Mocha
mocha 是一款強大的測試框架,能夠運行在nodejs和瀏覽器中,能夠高效的管理測試用例,支持多種測試報告格式
- 支持多種斷言庫:chai/shouldjs/expectjs/assert
-
支持兩種測試風格:TDD/BDD
- TDD:基於測試用例 進行測試
- BDD:基於產品本身功能 進行測試
常用方法
- describe(string,callback) -主要用於對測試用例的分組,層級描述。
TDD使用suite - it(string [,callback])-測試用例,callback 包含一個或者多個斷言;當callback不存在時,表示這個測試用例需要寫,但目前還未寫入,狀態使用
pending表示 -
hook-用於協助
describe中測試用例的準備·安裝·卸載和回收等工作,Hook一般用於describe內,但也可以describe外,作為頂級Hook- before/after([ string ,]callback) - 分別在進入或者退出
describe時觸發執行 - beforeEach/afterEach([ string ,]callback) - 分別在
describe中每個測試用例執行前和執行後觸發執行
- before/after([ string ,]callback) - 分別在進入或者退出
Full example
test/index.js
describe('hooks', function() {
before(function() {
console.log('before')
});
after(function() {
console.log('after')
});
beforeEach(function() {
console.log('beforeEach')
});
afterEach(function() {
console.log('afterEach')
});
it('Test1',()=>{
console.log('test1')
})
it('Test2',()=>{
console.log('test2')
})
// test cases
});
運行 npm run test
{
"script":{
" test":"mocha"
}
}
hooks
before
beforeEach
test1
√ Test1
afterEach
beforeEach
test2
1) Test2
afterEach
after
1 passing (15ms)
1 failing
1) hooks
Test2:
AssertionError: expected 0 to equal 1
+ expected - actual
-0
+1
at Context.it (test\index.js:93:12)
我們可以看到 基於mocha後的斷言,他的提示體驗大大的提升
- 成功後,有相關提示
- 遇到失敗時,依舊可以執行下去,展示所有失敗用例信息
- 統計測試用例成功數和失敗數
4. **異步處理**
- done
it('should save without error', (done)=> {
var user = new User('Luna');
user.save((err)=> {
if (err) done(err);
else done();
});
//user.save(done);
});
```
- promise
```ts
it('respond with matching records', ()=> {
return db.find({type: 'User'}).should.eventually.have.length(3);
});
```
- async/await
```ts
it('responds with matching records', async function() {
const users = await db.find({type: 'User'});
users.should.have.length(3);
});
```
-
Only-屏蔽其他測試單元/測試用例,只執行標識為Only的測試單元/用例。一般用於 當你的單元測試越寫越多時,只想測試新寫的單元測試是否正確,這個屬性就可以幫你在執行時,幫你過濾掉其他測試,加快執行速度
describe.only('something', function() { // 只會跑包在裏面的測試 })或者
it.only('do do', () => { // 只會跑這一個測試 })
-
skip-表示執行時,跳過標識的測試單元/測試用例,可以作用於
describe和itit.skip('should return -1 unless present', function() { // 代碼不存被執行 }); it('should return the index when present', function() { // 代碼會執行 });可以
this.skip()在測試用例執行的時候,根據運行時過濾當前測試案例describe('outer', function() { before(function() { this.skip(); }); after(function() { // will be executed }); describe('inner', function() { before(function() { // will be skipped }); after(function() { // will be skipped }); }); }); - 其他
- timeout - 超時