前端測試框架

 

一.爲何要進行測試?

一個 bug 被隱藏的時間越長,修復這個 bug 的代價就越大。大量的研究數據指出:最後才修改一個 bug 的代價是在 bug 產生時修改它的代價的10倍。因此要防患於未然。html

從語言的角度講

JavaScript 做爲 web 端使用最普遍的編程語言,它是動態語言,缺少靜態類型檢查,因此在代碼編譯期間,很難發現像變量名寫錯調用不存在的方法, 賦值或傳值的類型錯誤等錯誤。前端

例以下面的例子, 這種類型不符的狀況在代碼中很是容易發生

function foo(x) {
  return x + 10
}

foo('Hello!') //'Hello!10'
在JavaScript語言中,除了做爲數字的加運算外,也能夠看成字符串的鏈接運算符。這固然不是咱們想要的結果。

 當開發完一個功能模塊的時候,如何肯定你的模塊有沒有 bug 呢?一般的作法是根據具體的業務,執行 debug 模式,一點一點深刻到代碼中去查看。若是你一直都是這樣,那麼你早就已經 OUT 了。如今更先進的作法是自動化測試, 寫好測試用例, 執行一個指令,就可快速知道代碼有沒有缺陷,以及出錯的地方。vue

從工程的角度講

在平常的開發中,代碼的完工其實並不等於開發的完工。若是沒有單元測試,不能保證代碼可以正常運行。node

測試不可能保證一個程序是徹底正確的,可是測試卻能夠加強程序員對程序健壯性,穩定性的信心,測試可讓咱們相信程序作了咱們指望它作的事情。測試可以使咱們儘早的發現程序的 bug 和不足。作完開發後,用測試框架轟擊系統,可以經受住測試框架挑戰過的代碼,纔是健壯的代碼。  單元測試能加強開發人員對代碼的信心。git

測試人員作的只是業務上的集成測試,也就是黑盒測試,測試出的 bug 的範圍相對而言比較廣,很難精確到單個方法, 不可以精準地定位問題。程序員

 

二. 測試分類

JavaScript代碼測試有不少分類,好比單元測試(unit test)集成測試(integration test)功能測試(functional test)端到端測試(end to end test)迴歸測試(regression test)瀏覽器測試(browser test)github

單元測試

單元測試指的是測試小的代碼塊,一般指的是獨立測試單個函數若是某個測試依賴於一些外部資源,好比網絡或者數據庫,那它就不是單元測試。單元測試是從程序員的角度編寫的,保證一些方法執行特定的任務,給出特定輸入,獲得預期的結果。web

單元測試通常很容易寫。一個單元測試一般是這樣的:爲某個函數提供某些輸入值,而後驗證函數的返回值是否正確。然而,若是你的代碼設計很是糟糕,則單元測試會很難寫。從另外一個角度理解,單元測試能夠幫助咱們寫更好的代碼。單元測試能夠幫助咱們避免一些常見的BUG。一般,程序員會在同一個細節上反覆犯錯,若是爲這些Bug添加單元測試,則能夠有效避免這種狀況。固然,你也可使用集成測試功能測試來解決這個問題,可是單元測試更加適合,由於單元測試更加細緻,能夠幫助咱們快速定位和解決問題。數據庫

集成測試

集成測試就是測試應用中不一樣模塊如何集成,如何一塊兒工做,這和它的名字一致。集成測試與單元測試類似,可是它們也有很大的不一樣:單元測試是測試每一個獨立的模塊,而集成測試剛好相反。好比,當測試須要訪問數據庫的代碼時,單元測試不會真的去訪問數據庫,而集成測試則會編程

單元測試不夠時,這時就須要集成測試了。當你須要去驗證兩個獨立的模塊,好比數據庫和應用,保證它們可以正確的一塊兒工做,這時就須要集成測試了。爲了驗證測試結果,你就須要經過查詢數據庫驗證數據正確性。

集成測試一般比單元測試慢,由於它更加複雜。而且,集成測試還須要配置測試環境,好比配置測試數據庫或者其餘依賴的組件。這就使得編寫和維護集成測試更加困難,所以,你應該專一於單元測試,除非你真的須要集成測試。

你須要的集成測試應該少於單元測試。除非你須要測試多個模塊,或者你的代碼太複雜時,你才須要集成測試。而且,當你的代碼過於複雜時,建議優化代碼以便進行單元測試,而不是直接寫集成測試。

一般,咱們可使用單元測試工具編寫集成測試。

功能測試

功能測試有時候也被稱做端到端測試,或者瀏覽器測試,它們指的是同一件事。功能測試是從用戶的角度編寫的,測試確保用戶執行它所指望的工做。

功能測試指的是測試應用的某個完整的功能,它從一個用戶的角度出發,認爲整個系統都是一個黑箱,只有UI會暴露給用戶對於網頁應用,功能測試意味着使用工具模擬瀏覽器,而後經過點擊頁面來測試應用。

單元測試能夠測試單個函數,集成測試能夠測試兩個模塊一塊兒工做。功能測試則徹底是另一個層次。你能夠有上百個單元測試,可是一般你只有少許的功能測試。這是由於功能測試太複雜了,難於編寫和維護。功能測試很慢,由於它須要模擬真實用戶進行網頁交互。

事實上,你不須要編寫很是詳細的功能測試。功能測試並不意味着你須要測試每個功能,其實,你只須要測試一些常見的用戶行爲。若是你須要在瀏覽器中手動測試應用的某個流程,好比註冊帳號,這時你能夠編寫一個功能測試。

對於單元測試,你會使用代碼去驗證結果,在功能測試中也應該這樣作。以註冊帳號爲例,你能夠驗證瀏覽器是否跳轉到了」感謝註冊」頁面。

當有些測試你須要手動在瀏覽器下重複進行時,你應該編寫功能測試。注意不要寫得太細緻了,不然維護這些測試將是一個噩夢。

測試JavaScript代碼時,應該着重於單元測試,它很是容易編寫和維護,除了能夠減小BUG還有不少益處。而集成測試與功能測試應該做爲補充。

 

三.單元測試的好處: 

  • 提升代碼質量         

        代碼有測試用例,雖不能說百分百無bug,但至少說明測試用例覆蓋到的場景是沒有問題的。有測試用例,發佈前跑一下,能夠杜絕各類疏忽而引發的功能bug。若是能經過單元測試,那麼經過後續測試且軟件總體正常運行的機率大大提升                       

  • 快速反饋,減小調試時間

       自動化測試另一個重要特色就是快速反饋,反饋越迅速意味着開發效率越高。拿UI組件爲例,開發過程都是打開瀏覽器刷新頁面點點點才能肯定UI組件工做狀況是否符合本身預期。接入自動化測試之後,經過腳本代替這些手動點擊,接入代碼watch後每次保存文件都能快速得知本身的的改動是否影響功能,節省了不少時間,畢竟機器幹事情比人老是要快得多。若是程序有bug,咱們運行一次所有單元測試,找到不經過的測試,能夠很快地定位對應的執行代碼。單元測試發現的問題定位到細節,容易修改,節省時間。修復代碼後,運行對應的單元測試;如還不經過,繼續修改,運行測試.....直到測試經過。

  • 放心重構

       重構後把代碼改壞了,對總體系統構成破壞的狀況並很多見。因爲大多數狀況下,全部模塊或業務功能不是孤立的,可謂牽一髮動全身,你改一個方法可能致使整個項目運行不起來

若是你有單元測試,狀況大不相同。寫完一個類,把單元測試寫了,確保這個類邏輯正確;每一個類保證邏輯正確,拼在一塊兒確定不出問題。能夠放心一邊重構,一邊運行項目;而不是總體重構完,提心跳膽地run。

 

四.測試系統構成

        測試主要是測試框架、斷言庫,   代碼覆蓋率工具,仿真工具 , 測試驅動(測試任務管理工具)組成:

  1. 測試框架: 如何組織測試,主要由Mocha、Jasmine,Jest ,AVA, Tape等,測試主要提供了清晰簡明的語法來描述測試用例,以及對測試用例分組,測試框架會抓取到代碼拋出的AssertionError,並增長一大堆附加信息,好比那個用例掛了,爲何掛等等。測試框架一般提供TDD(測試驅動開發)或BDD(行爲驅動開發)的測試語法來編寫測試用例。不一樣的測試框架支持不一樣的測試語法,好比Mocha既支持TDD也支持BDD,而Jasmine只支持BDD。當前流行 BDD 的測試結構。

  2. 斷言庫:Should.jschaiexpect.js等等,斷言庫提供了不少語義化的方法來對值作各類各樣的判斷。固然也能夠不用斷言庫,Node.js中也能夠直接使用原生assert庫。

  3. 代碼覆蓋率:istanbul等爲代碼在語法級分支上打點,運行了打點後的代碼,根據運行結束後收集到的信息和打點時的信息來統計出當前測試用例對源碼的覆蓋狀況。

  4. 仿真工具  模擬方法,模塊,甚至服務器 , 獲取方法的調用信息,先來講說爲何須要仿真吧:須要測試的單元依賴於外部的模塊,而這些依賴的模塊具備一些特色,例如不能控制、實現成本較高、操做危險等緣由,不能直接使用依賴的模塊,這樣狀況下就須要對其進行mock,要完整運行前端代碼,一般並不須要完整的後端環境。能僞造出前端頁面渲染所須要的數據就行,這類工具我用過的有sinon,easy-mock,RAP, 甚至手工僞造一些假數據均可以。
  5.  測試驅動(測試任務管理工具)

    karma:   是一個基於 Node.js 的 JavaScript 測試執行過程管理工具(Test Runner)。設置測試須要的框架、環境、源文件、測試文件等,配置完後,就能夠輕鬆地執行測試,該工具可用於測試全部主流 Web 瀏覽器,

                這個測試工具的一個強大特性就是,它能夠監控 (Watch) 文件的變化,而後自行執行,經過 console.log 顯示測試結果。


    buster.js: 另一個工具,不過目前處於deta版本,不只能夠在瀏覽器端,還能夠在node端

  6. 類瀏覽器測試環境   這類工具備Protractor, Nightwatch, Phantom, Casper 

五.選擇單元測試框架

測試框架作的事情:

  • 描述你要測試的東西
  • 對其進行測試
  • 判斷是否符合預期

單元測試應該:簡單,快速執行,有清晰的錯誤報告。

選擇框架要考慮下面這些方面:

  • 斷言:有些框架內置了斷言庫,有的框架能夠本身選擇斷言庫。
  • 測試風格:支持的測試風格 測試驅動型 / 行爲驅動型 是否喜歡。
  • 異步測試支持:測試框架對異步測試支持是否良好。
  • 使用的語言:測試框架使用的語言,前端測試框架選擇JS語言。
  • 社區是否活躍,  有沒有完整的API文檔, 使用的公司多很少,有沒有大公司維護 。

注:測試驅動型和行爲驅動型的區別

TDD:站在程序員的角度,寫測試代碼。測試驅動型的開發方式,先寫測試代碼,以後編寫能經過測試的業務代碼,能夠不斷的在能經過測試的狀況下重構 。

BDD:站在用戶的角度,寫測試代碼。 是測試驅動開發的進化,測試代碼的風格是預期結果,更關注功能和設計,看起來像需求文檔。定義系統的行爲是主要工做,而對系統行爲的描述則變成了測試標準

其實都是先寫測試代碼,感受BDD 風格更人性。

各框架特色

Mocha

  • 靈活,擴展性好,不包括斷言和仿真,測試報告,流行的選擇:chai,sinon,istanbul
  • 社區成熟用的人多,測試各類東西社區都有示例
  • 可使用快照測試,須要額外配置
  • 功能很是豐富,支持運行在 Node.js 和瀏覽器中, 對異步測試支持很是友好
  • Mocha性能更勝一籌
  • 終端顯示友好

Jasmine

  • 開箱即用(支持斷言和仿真)
  • 全局環境,好比 describe 不須要引入直接用
  • 比較'老',坑基本都有人踩過了
  • 對低版本瀏覽器支持性比較好
  • 沒有自帶mockserver, 若是須要這功能的得另外配置

Jest

  • 基於 Jasmine 至今已經作了大量修改添加了不少特性
  • 開箱即用配置少,API簡單
  • 支持斷言和仿真
  • 較新,社區不十分紅熟
  • 較多用於 React 項目(但普遍支持各類項目)

AVA

  • 異步,性能好
  • 簡約,清晰
  • 快照測試和斷言須要三方支持

Tape

  • 體積最小,只提供最關鍵的東西
  • 對比其餘框架,只提供最底層的 API

總結一下,Mocha ,Jasmine用的人最多,社區最成熟,靈活,可配置性強易拓展,Jest 開箱即用,裏邊啥都有提供全面的方案,Tape 最精簡。

Mocha 跟 Jasmine 是目前最火的兩個單元測試框架,基本上目前前端單元測試就在這兩個庫之間選了。總的來講就是Jasmine功能齊全,配置方便,Mocha靈活自由,自由配置。 二者功能覆蓋範圍粗略能夠表示爲:

Jasmine(2.x) === Mocha + Chai + Sinon - mockserver

實際使用後以爲jasmine因爲各類功能內建,斷言方式或者異步等風格相對比較固定,沒有自帶mockserver, 須要這功能的得另外配置,  Cha i和 Sinon(賽蘭)畢竟是專門作特定功能的框架,用 Mocha + Chai + Sinon 這種方式會想對舒爽一點。

 

六.斷言庫的風格

Assert

var assert = require('chai').assert , foo = 'bar' , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] }; 
assert.typeOf(foo, 'string'); // without optional message 
assert.typeOf(foo, 'string', 'foo is a string'); // with optional message 
assert.equal(foo, 'bar', 'foo equal `bar`'); 
assert.lengthOf(foo, 3, 'foo`s value has a length of 3'); 
assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');

 

BBD風格的斷言庫

expect

var expect = require('chai').expect
  , foo = 'bar'
  , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };

expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.lengthOf(3);
expect(beverages).to.have.property('tea').with.lengthOf(3);

should

var should = require('chai').should() //actually call the function
  , foo = 'bar'
  , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };

foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.lengthOf(3);
beverages.should.have.property('tea').with.lengthOf(3);

 建議使用expect,should不兼容IE

expect斷言語法

// equal 相等或不相等
expect(4 + 5).to.be.equal(9);
expect(4 + 5).to.be.not.equal(10);
expect('hello').to.equal('hello');  
expect(42).to.equal(42);  
expect(1).to.not.equal(true);  
expect({ foo: 'bar' }).to.not.equal({ foo: 'bar' });  
expect({ foo: 'bar' }).to.deep.equal({ foo: 'bar' });

// above 斷言目標的值大於某個value,若是前面有length的鏈式標記,則能夠用來判斷數組長度或者字符串長度
expect(10).to.be.above(5);
expect('foo').to.have.length.above(2);  
expect([ 1, 2, 3 ]).to.have.length.above(2); 
相似的還有least(value)表示大於等於;below(value)表示小於;most(value)表示小於等於

// 判斷目標是否爲布爾值true(隱式轉換)
expect('everthing').to.be.ok;
expect(1).to.be.ok;  
expect(false).to.not.be.ok;
expect(undefined).to.not.be.ok;  
expect(null).to.not.be.ok; 

// true/false 斷言目標是否爲true或false
expect(true).to.be.true;  
expect(1).to.not.be.true;
expect(false).to.be.false;  
expect(0).to.not.be.false;

// null/undefined 斷言目標是否爲null/undefined
expect(null).to.be.null;  
expect(undefined).not.to.be.null;
expect(undefined).to.be.undefined;  
expect(null).to.not.be.undefined;


// NaN  斷言目標值不是數值
expect('foo').to.be.NaN;
expect(4).not.to.be.NaN;

// 判斷類型大法(能夠實現上面的一些例子):a/an
expect('test').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');
expect(foo).to.be.an.instanceof(Foo);
expect(null).to.be.a('null');  
expect(undefined).to.be.an('undefined');
expect(new Error).to.be.an('error');
expect(new Promise).to.be.a('promise');

// 包含關係:用來斷言字符串包含和數組包含。若是用在鏈式調用中,能夠用來測試對象是否包含某key 能夠混着用。
expect([1,2,3]).to.include(2);
expect('foobar').to.contain('foo');
expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo');

// 判斷空值
expect([]).to.be.empty;
expect('').to.be.empty;
expect({}).to.be.empty;

// match
expect('foobar').to.match(/^foo/);
    
// exist 斷言目標既不是null也不是undefined
var foo = 'hi' , bar = null, baz;
expect(foo).to.exist;  
expect(bar).to.not.exist;  
expect(baz).to.not.exist;

// within斷言目標值在某個區間範圍內,能夠與length連用
expect(7).to.be.within(5,10);  
expect('foo').to.have.length.within(2,4);  
expect([ 1, 2, 3 ]).to.have.length.within(2,4);

// instanceOf 斷言目標是某個構造器產生的事例
var Tea = function (name) { this.name = name; } , Chai = new Tea('chai');
expect(Chai).to.be.an.instanceof(Tea);  
expect([ 1, 2, 3 ]).to.be.instanceof(Array); 

// property(name, [value])  斷言目標有以name爲key的屬性,而且能夠指定value斷言屬性值是嚴格相等的,此[value]參數爲可選,若是使用deep鏈式調用,能夠在name中指定對象或數組的引用表示方法
// simple referencing
var obj = { foo: 'bar' };  
expect(obj).to.have.property('foo');  
expect(obj).to.have.property('foo', 'bar');// 相似於expect(obj).to.contains.keys('foo')

// deep referencing
var deepObj = {  
  green: { tea: 'matcha' },
  teas: [ 'chai', 'matcha', { tea: 'konacha' } ]
};
expect(deepObj).to.have.deep.property('green.tea', 'matcha');  
expect(deepObj).to.have.deep.property('teas[1]', 'matcha');  
expect(deepObj).to.have.deep.property('teas[2].tea', 'konacha'); 

// ownproperty 斷言目標擁有本身的屬性,非原型鏈繼承
expect('test').to.have.ownProperty('length'); 

// throw 斷言目標拋出特定的異常
var err = new ReferenceError('This is a bad function.');  
var fn = function () { throw err; }  
expect(fn).to.throw(ReferenceError);  
expect(fn).to.throw(Error);  
expect(fn).to.throw(/bad function/);  
expect(fn).to.not.throw('good function');  
expect(fn).to.throw(ReferenceError, /bad function/);  
expect(fn).to.throw(err);  
expect(fn).to.not.throw(new RangeError('Out of range.'));  

// satisfy(method) 斷言目標經過一個真值測試
expect(1).to.satisfy(function(num) { return num > 0; })

 

 

七. 測試覆蓋率

  • 行覆蓋率(line coverage):是否每一行都執行了

         可執行語句的每一行是否都被執行了,不包括註釋,空白行 行覆蓋經常被人指責爲「最弱的覆蓋」,爲何這麼說呢,舉一個例子

function foo(a, b)
{
   return  a / b;
}

TeseCase: a = 10, b = 5

測試人員的測試結果會告訴你,他的代碼覆蓋率達到了100%,而且全部測試案例都經過了。咱們的語句覆蓋率達到了所謂的100%,可是卻沒有發現最簡單的Bug,好比,當我讓b=0時,會拋出一個除零異常。

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

 

4個指標當中,行覆蓋率和語句覆蓋率很相近;在代碼規範的狀況下,規範要求一行寫一個語句 它們應該是同樣的

4個指標當中,分支覆蓋率是最重要的,它包括: !&&||?: ; if 和 else-if else switch - case 等等各類包含分支的狀況

 

  •  覆蓋率數據只能表明你測試過哪些代碼,不能表明你是否測試好這些代碼。(好比上面第一個除零Bug)
  •  不要過於相信覆蓋率數據。
  •  分支覆蓋率 > 函數覆蓋 > 語句覆蓋
  • 測試人員不能盲目追求代碼覆蓋率,而應該想辦法設計更多更好的案例,哪怕多設計出來的案例對覆蓋率一點影響也沒有。

 

八.利弊權衡

近幾年前端工程化的發展風起雲涌,可是前端自動化測試這塊內容你們卻彷佛不過重視。雖然項目迭代過程當中會有專門的測試人員進行測試,但等他們來進行測試時,代碼已經開發完成的狀態。與之相比,若是咱們在開發過程當中就進行了測試會有以下的好處:

  • 保障代碼質量,減小bug
  • 提高開發效率,在開發過程當中進行測試能讓咱們提早發現 bug ,此時進行問題定位和修復的速度天然比開發完再被叫去修 bug 要快許多
  • 便於項目維護,後續任何代碼更新也必須跑通測試用例,即便進行重構或開發人員發生變化也能保障預期功能的實現

固然,凡事都有兩面性,好處雖然明顯,卻並非全部的項目都值得引入測試框架,畢竟維護測試用例也是須要成本的。對於一些需求頻繁變動、複用性較低的內容,好比活動頁面,讓開發專門抽出人力來寫測試用例確實得不償失。

而適合引入測試場景大概有這麼幾個:

  • 須要長期維護的項目。它們須要測試來保障代碼可維護性、功能的穩定性
  • 較爲穩定的項目、或項目中較爲穩定的部分。給它們寫測試用例,維護成本低
  • 被屢次複用的部分,好比一些通用組件和庫函數。由於多處複用,更要保障質量
 
單元測試確實會帶給你至關多的好處,但不是馬上體驗出來。正如買重疾保險,交了不少保費,沒病沒痛,十幾年甚至幾十年都用不上,最好就是一生用不上理賠,身體健康最重要。單元測試也同樣,寫了能夠買個放心,對代碼的一種保障,有bug儘快測出來,沒bug就最好,總不能說「寫那麼多單元測試,結果測不出bug,浪費時間」吧。
 

 參考連接

1.https://www.jianshu.com/p/f200a75a15d2  Chai.js斷言庫API中文文檔

2.http://www.ruanyifeng.com/blog/2015/06/istanbul.html    代碼覆蓋率工具 Istanbul 入門教程

3.https://segmentfault.com/a/1190000012654035   Vue單元測試實戰教程(Mocha/Karma + Vue-Test-Utils + Chai)

4.http://www.ruanyifeng.com/blog/2015/12/a-mocha-tutorial-of-examples.html 測試框架 Mocha 實例教程

5.https://vue-test-utils.vuejs.org/zh/guides/#%E8%B5%B7%E6%AD%A5   Vue Test Utils教程

6.https://www.jianshu.com/p/c7c86b8f376c  mocha 的基本介紹&&expect風格斷言庫的基本語法

相關文章
相關標籤/搜索