1. 程式人生 > >Nodejs的測試和測試驅動開發

Nodejs的測試和測試驅動開發

測試是保證軟體質量必不可少的一環。測試有很多形式:手動、自動、單元測試等等。這裡我們只聊使用Mocha這個框架在Nodejs中實現單元測試。單元測試是測試等重要組成,這樣的測試只對於一個方法,這樣的一小段程式碼,實施有針對的測試。

這裡會逐步深入的講解單元測試。首先是最簡單的單元測試,沒有外部依賴,只有簡單的輸入。接著是實用Sino框架實現stub等有依賴的測試。最後講解如何單元測試非同步程式碼。

安裝Mocha 和Chai

安裝Mocha:

npm install mocha -g

Mocha和其他的javascript單元測試框架,如:jasmine和QUnit不同,他沒有assertion庫。但是,Mocha允許你實用你自己的。最流行的Assertion庫有should.js、expect.js和Chai,當然Nodejs內建的也可以使用。這裡我們用Chai。

首先建立一個package.json並安裝Chai:

touch package.json
echo {} > package.json
npm install chai --save-dev

Chai包含三種assertion方式:should方式、expect方式和assert方式。個人喜歡expect式的,所以下面就使用這個方式了。

第一個Test

專案程式碼

第一個例子,我們用測試驅動開發(TDD)的方式建立一個CartSummary的建構函式,這個函式會用來計算購物車的商品總數。測試驅動開發就是在實現功能之前先寫單元測試,這樣來驅動你設計可以與測試相適應的程式碼。

測試驅動開發的步驟:

  1. 寫一個測試,並且這個測試會失敗。
  2. 寫最少的程式碼來使整個測試可以通過。
  3. 重複。

來看程式碼:

// tests/part1/cart-summary-test.js
var chai = require('chai');
var expect = chai.expect; // we are using the "expect" style of Chai
var CartSummary = require('./../../src/part1/cart-summary');

describe('CartSummary', function() {
  it('getSubtotal() should return 0 if no items are passed in'
, function() { var cartSummary = new CartSummary([]); expect(cartSummary.getSubtotal()).to.equal(0); }); });

describe方法是用來建立一組測試的,並且可以給這一組測試一個描述。一個測試就用一個it方法。it方法的第一個引數是一個描述。第二個引數是一個包含一個或者多個assertion的方法。

執行測試只需要在專案的根目錄執行命令列:mocha tests --recursive --watchrecursive指明會找到根目錄下的子目錄的測試程式碼並執行。watch則表示Mocha會監視原始碼和測試程式碼的更改,每次更改之後重新測試。

我們測試不過,因為還沒有完成功能程式碼。新增程式碼:

// src/part1/cart-summary.js
function CartSummary() {}

CartSummary.prototype.getSubtotal = function() {
  return 0;
};

module.exports = CartSummary;

測試就可以通過了:

下一個測試:

it('getSubtotal() should return the sum of the price * quantity for all items', function() {
  var cartSummary = new CartSummary([{
    id: 1,
    quantity: 4,
    price: 50
  }, {
    id: 2,
    quantity: 2,
    price: 30
  }, {
    id: 3,
    quantity: 1,
    price: 40
  }]);

  expect(cartSummary.getSubtotal()).to.equal(300);
});

這個測試時失敗的。。。

下面就來修改程式碼,讓測試通過:

// src/part1/cart-summary.js
function CartSummary(items) {
  this._items = items;
}

CartSummary.prototype.getSubtotal = function() {
  if (this._items.length) {
    return this._items.reduce(function(subtotal, item) {
      return subtotal += (item.quantity * item.price);
    }, 0);
  }

  return 0;
};

Stub和Sinon

假設我們現在需要給CartSummary新增getTax方法。最終的使用看起來是這樣的:

var cartSummary = new CartSummary([ /* ... */ ]);
cartSummary.getTax('NY', function() {
  // executed when the tax API request has finished
});

getTax方法會使用量外的一個tax模組,包含一個calculate的方法。雖然我們還沒有實現tax模組,但是我們還是可以完成getTax的測試。該怎麼做呢?

首先,安裝Sinon:

npm install --save-dev sinon

安裝Sinon之後,我們就可以給出tax.calculate的定義了:


// src/part1/tax.js
module.exports = {
  calculate: function(subtotal, state, done) {
    // implemented later or in parallel by our coworker
  }
};

建立完成tax.calculate之後就可以使用Sinon的魔法了。用Sinon給出一個tax.calculate的零時實現。這個零時的實現就是Stub(也叫做樁)。程式碼:

// tests/part1/cart-summary-test.js
// ...
var sinon = require('sinon');
var tax = require('./../../src/part1/tax');

describe('getTax()', function() {
  beforeEach(function() {
    sinon.stub(tax, 'calculate', function(subtotal, state, done) {
      setTimeout(function() {
        done({
          amount: 30
        });
      }, 0);
    });
  });

  afterEach(function() {
    tax.calculate.restore();
  });

  it('get Tax() should execute the callback function with the tax amount', function(done) {
    var cartSummary = new CartSummary([{
      id: 1,
      quantity: 4,
      price: 50
    }, {
      id: 2,
      quantity: 2,
      price: 30
    }, {
      id: 3,
      quantity: 1,
      price: 40
    }]);

    cartSummary.getTax('NY', function(taxAmount) {
      expect(taxAmount).to.equal(30);
      done();
    });
  });
});

上面已經使用Sinon建立stub方法了。這裡再細講一下。使用sinon.stub方法建立Stub:

var stub = sinon.stub(object,'method', func);

object新增一個名稱為method(第二個引數)的方法,方法體的實現在第三個引數中給出。

上例中使用的方法體:

function(subtotal, state, done) {
  setTimeout(function() {
    done({
      amount: 30
    });
  }, 0);
}

setTimeout方法是用來模擬真實環境的,在實際使用的時候肯定會有一個非同步的網路請求來請求tax服務。方法體的替換在beforeEach裡,這些程式碼會在測試開始之前執行。在所有測試完成之後呼叫afterEach,並把tax.calculate恢復到原來的模樣。

上面的例子也展示瞭如何測試非同步程式碼。在it方法中指明一個引數(上例使用的是done)。Mocha會傳入一個方法,並等待非同步程式碼返回再結束測試。當然,這個等待是由超時時間的,一般是2000毫秒。如果非同步程式碼的測試,沒有按照上面的方法寫的話,那麼所有的測試都會通過。

Sinon的”間諜”

Sinon的間諜(spy)是用來完成另外一種替身測試的(test double),它可以用來記錄方法呼叫。包括方法的呼叫次數、呼叫的時候的引數是什麼樣的以及是否丟擲異常。下面就是更新後的測試:

it('getTax() should execute the callback function with the tax amount', function(done) {
  var cartSummary = new CartSummary([
    {
      id: 1,
      quantity: 4,
      price: 50
    },
    {
      id: 2,
      quantity: 2,
      price: 30
    },
    {
      id: 3,
      quantity: 1,
      price: 40
    }
  ]);

  cartSummary.getTax('NY', function(taxAmount) {
    expect(taxAmount).to.equal(30);
    expect(tax.calculate.getCall(0).args[0]).to.equal(300);
    expect(tax.calculate.getCall(0).args[1]).to.equal('NY');
    done();
  });
});

在測試中添加了兩個expect。getCall用來獲取tax.calculate的第一次呼叫的第一個引數值,第二個getCall用來獲取tax.calculate的第一次呼叫的第二個引數。主要可以用來檢測被測試方法的引數是否正確。

總結

在本文中探討了如何在Node中使用Mocha以及Chai和Sinon實現單元測試。希望各位喜歡。