Nodejs的測(cè)試和測(cè)試驅(qū)動(dòng)開(kāi)發(fā)

測(cè)試是保證軟件質(zhì)量必不可少的一環(huán)。測(cè)試有很多形式:手動(dòng)、自動(dòng)、單元測(cè)試等等。這里我們只聊使用Mocha這個(gè)框架在Nodejs中實(shí)現(xiàn)單元測(cè)試。單元測(cè)試是測(cè)試等重要組成,這樣的測(cè)試只對(duì)于一個(gè)方法,這樣的一小段代碼,實(shí)施有針對(duì)的測(cè)試。

這里會(huì)逐步深入的講解單元測(cè)試。首先是最簡(jiǎn)單的單元測(cè)試,沒(méi)有外部依賴,只有簡(jiǎn)單的輸入。接著是實(shí)用Sino框架實(shí)現(xiàn)stub等有依賴的測(cè)試。最后講解如何單元測(cè)試異步代碼。

安裝Mocha 和Chai

安裝Mocha:

npm install mocha -g

Mocha和其他的javascript單元測(cè)試框架,如:jasmine和QUnit不同,他沒(méi)有assertion庫(kù)。但是,Mocha允許你實(shí)用你自己的。最流行的Assertion庫(kù)有should.js、expect.js和Chai,當(dāng)然Nodejs內(nèi)置的也可以使用。這里我們用Chai。

首先創(chuàng)建一個(gè)package.json并安裝Chai:

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

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

第一個(gè)Test

項(xiàng)目代碼

第一個(gè)例子,我們用測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(TDD)的方式創(chuàng)建一個(gè)CartSummary的構(gòu)造函數(shù),這個(gè)函數(shù)會(huì)用來(lái)計(jì)算購(gòu)物車的商品總數(shù)。測(cè)試驅(qū)動(dòng)開(kāi)發(fā)就是在實(shí)現(xiàn)功能之前先寫單元測(cè)試,這樣來(lái)驅(qū)動(dòng)你設(shè)計(jì)可以與測(cè)試相適應(yīng)的代碼。

測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的步驟:

  1. 寫一個(gè)測(cè)試,并且這個(gè)測(cè)試會(huì)失敗。
  2. 寫最少的代碼來(lái)使整個(gè)測(cè)試可以通過(guò)。
  3. 重復(fù)。

來(lái)看代碼:

// 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方法是用來(lái)創(chuàng)建一組測(cè)試的,并且可以給這一組測(cè)試一個(gè)描述。一個(gè)測(cè)試就用一個(gè)it方法。it方法的第一個(gè)參數(shù)是一個(gè)描述。第二個(gè)參數(shù)是一個(gè)包含一個(gè)或者多個(gè)assertion的方法。

運(yùn)行測(cè)試只需要在項(xiàng)目的根目錄運(yùn)行命令行:mocha tests --recursive --watch。recursive指明會(huì)找到根目錄下的子目錄的測(cè)試代碼并運(yùn)行。watch則表示Mocha會(huì)監(jiān)視源代碼和測(cè)試代碼的更改,每次更改之后重新測(cè)試。

我們測(cè)試不過(guò),因?yàn)檫€沒(méi)有完成功能代碼。添加代碼:

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

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

module.exports = CartSummary;

測(cè)試就可以通過(guò)了:

下一個(gè)測(cè)試:

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);
});

這個(gè)測(cè)試時(shí)失敗的。。。

下面就來(lái)修改代碼,讓測(cè)試通過(guò):

// 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

假設(shè)我們現(xiàn)在需要給CartSummary添加getTax方法。最終的使用看起來(lái)是這樣的:

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

getTax方法會(huì)使用量外的一個(gè)tax模塊,包含一個(gè)calculate的方法。雖然我們還沒(méi)有實(shí)現(xiàn)tax模塊,但是我們還是可以完成getTax的測(cè)試。該怎么做呢?

首先,安裝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
  }
};

創(chuàng)建完成tax.calculate之后就可以使用Sinon的魔法了。用Sinon給出一個(gè)tax.calculate的零時(shí)實(shí)現(xiàn)。這個(gè)零時(shí)的實(shí)現(xiàn)就是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();
    });
  });
});

上面已經(jīng)使用Sinon創(chuàng)建stub方法了。這里再細(xì)講一下。使用sinon.stub方法創(chuàng)建Stub:

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

object添加一個(gè)名稱為method(第二個(gè)參數(shù))的方法,方法體的實(shí)現(xiàn)在第三個(gè)參數(shù)中給出。

上例中使用的方法體:

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

setTimeout方法是用來(lái)模擬真實(shí)環(huán)境的,在實(shí)際使用的時(shí)候肯定會(huì)有一個(gè)異步的網(wǎng)絡(luò)請(qǐng)求來(lái)請(qǐng)求tax服務(wù)。方法體的替換在beforeEach里,這些代碼會(huì)在測(cè)試開(kāi)始之前執(zhí)行。在所有測(cè)試完成之后調(diào)用afterEach,并把tax.calculate恢復(fù)到原來(lái)的模樣。

上面的例子也展示了如何測(cè)試異步代碼。在it方法中指明一個(gè)參數(shù)(上例使用的是done)。Mocha會(huì)傳入一個(gè)方法,并等待異步代碼返回再結(jié)束測(cè)試。當(dāng)然,這個(gè)等待是由超時(shí)時(shí)間的,一般是2000毫秒。如果異步代碼的測(cè)試,沒(méi)有按照上面的方法寫的話,那么所有的測(cè)試都會(huì)通過(guò)。

Sinon的"間諜"

Sinon的間諜(spy)是用來(lái)完成另外一種替身測(cè)試的(test double),它可以用來(lái)記錄方法調(diào)用。包括方法的調(diào)用次數(shù)、調(diào)用的時(shí)候的參數(shù)是什么樣的以及是否拋出異常。下面就是更新后的測(cè)試:

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();
  });
});

在測(cè)試中添加了兩個(gè)expect。getCall用來(lái)獲取tax.calculate的第一次調(diào)用的第一個(gè)參數(shù)值,第二個(gè)getCall用來(lái)獲取tax.calculate的第一次調(diào)用的第二個(gè)參數(shù)。主要可以用來(lái)檢測(cè)被測(cè)試方法的參數(shù)是否正確。

總結(jié)

在本文中探討了如何在Node中使用Mocha以及Chai和Sinon實(shí)現(xiàn)單元測(cè)試。希望各位喜歡。

原文地址:https://www.codementor.io/nodejs/tutorial/unit-testing-nodejs-tdd-mocha-sinon

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,401評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開(kāi)封第一講書人閱讀 176,069評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 62,873評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,635評(píng)論 6 408
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 55,128評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,365評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,881評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,733評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,935評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,172評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 34,582評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 35,821評(píng)論 1 282
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,595評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,908評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容