2020年1月12日 星期日

[NodeJS 文章收集] 單元測試:Mocha、Chai 和 Sinon

Source From Here
Mocha
Mocha 是一個 JavaScript 的測試框架,目的是用來管理測試的程式碼。在這裡指定介面(Interface)為 BDD。若使用 BDD 則提供 describe()it()before()after()beforeEach() 與 afterEach() 方法。

語法說明
* describe():描述場景或圈出特定區塊,例如:標明測試的功能或 function。
* it():撰寫測試案例(Test Case)。
* before():在所有測試開始前會執行的程式碼區塊。
* after():在所有測試結束後會執行的程式碼區塊。
* beforeEach():在每個 Test Case 開始前執行的程式碼區塊。
* afterEach():在每個 Test Case 結束後執行的程式碼區塊。

語法範例
  1. describe('hooks', function() {  
  2.   // 測試區塊  
  3.   before(function() {  
  4.     // 在所有測試開始前會執行的程式碼區塊  
  5.   });  
  6.   
  7.   after(function() {  
  8.     // 在所有測試結束後會執行的程式碼區塊  
  9.   });  
  10.   
  11.   beforeEach(function() {  
  12.     // 在每個 Test Case 開始前執行的程式碼區塊  
  13.   });  
  14.   
  15.   afterEach(function() {  
  16.     // 在每個 Test Case 結束後執行的程式碼區塊  
  17.   });  
  18.   
  19.   // 撰寫個別 Test Case  
  20.   it('should ...', function() {  
  21.     // 執行 Test Case  
  22.   });  
  23. });  
Chai
Chai 提供 BDD 語法測試用的斷言庫(Assertion Library)。斷言庫是一種判斷工具,驗證執行結果是否符合預期,若實際結果和預測不同,就是測到 bug 了。以下分 Assert 和 Expect / Should 說明。

Assert
assert(expression, message):測試這個項目的 expression 'foo' === 'bar' 是否為真,若為假則顯示錯誤訊息 message:
test/test_assert1.js
  1. var { assert } = require("chai")  
  2.   
  3. describe('AssertTest', function() {  
  4.     var foo = 'Hello';  
  5.     var bar = "World";  
  6.   
  7.     it('should be equal', function() {  
  8.         assert(foo === bar, 'foo is not bar');  
  9.     });  
  10. });  


Expect / Should
預期 3 等於(===)2。這是使用可串連的 getters 來完成斷言。這些可串聯的 getters 有 to、is、have 等。它很像英文,用很口語的方式做判斷:
test/test_expect1.js
  1. const { expect } = require('chai')  
  2.   
  3. describe('ExpectTest', function(){  
  4.     it('should be equal', function(){  
  5.         expect(3).to.equal(2);  
  6.     })  
  7. });  


測試 add(兩數相加)和 sub(兩數相減)功能. 首先來看被測試模組:
op.js
  1. // op.js module  
  2. function _add(x, y) {  
  3.   return x + y;  
  4. }  
  5.   
  6. function _sub(x, y) {  
  7.   return x - y;  
  8. }  
  9.   
  10. module.exports = {  
  11.   add: _add,  
  12.   sub: _sub,  
  13. };  
接著是測試程式:
test/test_op.js
  1. const { expect } = require('chai')  
  2. const testModule = require('../op');  
  3.   
  4. describe('Test add', () => {  
  5.   it('1 + 2 = 3', () => {  
  6.     expect(testModule.add(12)).to.equal(3);  
  7.   });  
  8.   
  9.   it('3 + 4 = 7', () => {  
  10.     expect(testModule.add(34)).to.equal(7);  
  11.   });  
  12. });  
  13.   
  14. describe('Test sub', () => {  
  15.   it('1 - 2 = -1', () => {  
  16.     expect(testModule.sub(12)).to.equal(-1);  
  17.   });  
  18.   
  19.   it('11 - 4 = 7', () => {  
  20.     expect(testModule.sub(114)).to.equal(7);  
  21.   });  
  22. });  


比較 Assert、Expect、Should 的差異
三者基本上都可完成相同工作,除了
* Should 會修改 Object.prototype
* Should 在瀏覽器環境下,對 IE 有相容問題
* Should 無法客製化錯誤訊息

Sinon
用來產生 Test Double(測試替身),可當成假資料來看,分為 Spy、Stub 和 Mock。

Spy (Spies - Sinon.JS)
對 function call 蒐集資訊,便於對測試結果做驗證。sinon.spy() 會回傳一個 Spy 物件,這個 Spy 物件像蜜糖般包裹於原 function 外,讓我們可以像 function 一樣呼叫。而這個 Spy 物件的 property 會協助蒐集由 function call 得到的資訊,例如:取得第一次呼叫所輸入的參數、該 function 被呼叫的次數。Spy 是三者最簡單的部份,並且 Stub 和 Mock 是建構於 Spy 之上的。

底下為待測程式:
cmp.js
  1. function _comparePeople(people1, people2, callback){  
  2.     let rst = people1.localeCompare(people2)  
  3.     callback(null, rst)  
  4. }  
  5.   
  6. module.exports = {  
  7.     comparePeople: _comparePeople  
  8. }  
接著是測試程式:
test/test_spy.js
  1. const expect = require('chai').expect;  
  2. const sinon = require('sinon');  
  3. const testModule = require('../cmp');  
  4.   
  5. describe('Test comparePeople', function(){  
  6.     it('should call the callback function', function(){  
  7.         var nameList = ['Nina''Ricky'];  
  8.         var callback = sinon.spy();  
  9.         testModule.comparePeople(nameList[0], nameList[0], callback);  
  10.         console.log(callback.callCount); // The calling time  
  11.         expect(callback.callCount).to.equal(1)  
  12.     });  
  13. });  
執行測試:
# npm test -- -g 'comparePeople'



Stub (Stubs - Sinon.JS)
取代 function。與 Spy 不同的是,Spy 依然會執行真的 function,但 Stub 並不會。適用於 Ajax 和 Timer。
What are stubs?
Test stubs are functions (spies) with pre-programmed behavior.

They support the full test spy API in addition to methods which can be used to alter the stub’s behavior.

As spies, stubs can be either anonymous, or wrap existing functions. When wrapping an existing function with a stub, the original function is not called.

一個使用 stub 的測試範例如下:
test/test_stub1.js
  1. const { expect } = require('chai');  
  2. const sinon = require('sinon');  
  3. const uuid = require('node-uuid');  
  4.   
  5. describe('stub example', function(){  
  6.     it('check length of uuid', ()=> {  
  7.         var stub = sinon.stub(uuid, 'v4');  
  8.         var mockId = stub.v4();  
  9.         expect(mockId.length).to.equal(36);  
  10.         uuid.v4.restore();  
  11.     });  
  12. });  
執行測試如下:
# npm test -- -g 'stub example'



更多的 stub 使用範例如下:
test/test_stub2.js
  1. const sinon = require('sinon');  
  2. const { expect } = require('chai')  
  3.   
  4. class A{  
  5.     constructor(){}  
  6.   
  7.     add(a, b){  
  8.         let sum = a + b;  
  9.         return sum  
  10.     }  
  11. }  
  12.   
  13. describe('test A', ()=>{  
  14.     it('demo usage of stub API', function(){  
  15.         const a = new A();  
  16.   
  17.         // 1) API:callsFake  
  18.         sinon.stub(a, 'add').callsFake(function(a, b){  
  19.             return 0  
  20.         })  
  21.   
  22.         var sum = a.add(12)  
  23.         expect(a.add(12)).to.equal(0)  
  24.   
  25.         // 2) Restore add  
  26.         a.add.restore()  
  27.         expect(a.add(12)).to.equal(3)  
  28.   
  29.         // 3) API:withArgs,callThrough  
  30.         var obj = {};  
  31.         obj.sum = function sum(a, b){ return a + b; };  
  32.         sinon.stub(obj, 'sum')  
  33.         obj.sum.withArgs(22).callsFake(function foo(){ return 'bar'; });  
  34.         obj.sum.callThrough();  
  35.         expect(obj.sum(22)).to.equal('bar')  
  36.         expect(obj.sum(12)).to.equal(3)  
  37.         obj.sum.restore()  
  38.         expect(obj.sum(22)).to.equal(4)  
  39.     });  
  40. });  
執行測試如下:


Mock (Mocks - Sinon.JS)
取代整個物件,包含完整實作細節。一個簡單測試範例如下:
test/test_mock.js
  1. const { expect } = require('chai')  
  2. const sinon = require('sinon')  
  3.   
  4. var opts = {  
  5.     call: function(msg){ console.log(msg); }  
  6. };  
  7.   
  8. describe('test Mock', ()=>{  
  9.     it('should pass Hello World to run call()', function(){  
  10.         // 1) Creates a mock for the provided object.  
  11.         var mock = sinon.mock(opts);  
  12.   
  13.         mock  
  14.             .expects('call')  // 2.1 ) Overrides obj.call with a mock function and returns it.  
  15.             .once()  // 2.2) Expect the method to be called exactly once.  
  16.             .withExactArgs('Hello World'); // 2.3) Expect the method to be called with the provided arguments and no others.  
  17.   
  18.         // 3) Launch testing  
  19.         opts.call('Hello World');  
  20.   
  21.         // 4) Restores all mocked methods.  
  22.         mock.restore();  
  23.   
  24.         // 5) Verifies all expectations on the mock.  
  25.         mock.verify();  
  26.     });  
  27. });  


The repo for above sample code.

Supplement
NodeJS 文章收集 - A quick and complete guide to Mocha testing
Chai Assertion Library
FAQ - Why is the constructor invoked despite calling createStubInstance?

沒有留言:

張貼留言

[Git 常見問題] error: The following untracked working tree files would be overwritten by merge

  Source From  Here 方案1: // x -----删除忽略文件已经对 git 来说不识别的文件 // d -----删除未被添加到 git 的路径中的文件 // f -----强制运行 #   git clean -d -fx 方案2: 今天在服务器上  gi...