Nodejs測試:從0到90(理論篇)

tralight發表於2016-07-19

最近在負責一個基於nodejs的應用,在很多方面都經歷了一個從無到有的過程,測試也是如此。剛開始時,程式碼都寫不好,更別提測試了,那時測試為0。經歷過一段時間後,尤其是看到 npm 上優秀的庫的測試覆蓋率都在100%時,痛下決心開始學習 nodejs 的測試, 目前這個應用的測試覆蓋率在90%+。這就是我的從0到90。剩下的10,還有很長的路,且待下回分解。

寫在前面的

對於開發者,測試的重要性毋庸置疑, 所謂”無測試不上線“,”無測試不重構“等。但在實踐的過程中,測試總是有這樣的那樣的問題,隨便列舉下:

  • 不寫測試。一般是覺得寫測試浪費時間,也有可能是不會寫;
  • 亂寫測試。隨意的寫,沒有規範,沒有覆蓋率,沒有整合;
  • 寫了跟沒寫一樣。這樣的測試通常是不可讀、不可維護、不可信任,不可重複或獨立執行(強依賴某些特定的環境或條件)、不執行(通常只是開發的時候執行一下,之後再也不會執行,或者執行太慢,沒有人願意執行)。

一個好的測試

一千個人眼中有至少二千種測試理念或方法,很難定義什麼是一種好的測試。在團隊一直負責測試的推進,我們的理念很簡單,包括以下幾個原則:

  • 覆蓋75%+

一個好的測試,最重要的衡量標準就是測了多少(覆蓋率),75%是最低的標準。這個標準對java來說基本可行,但對nodejs不太適用,javascript是弱型別的、動態語言,沒有編譯階段,很多錯誤只能在執行時才會被發現,所以需要更高的覆蓋率,最好是100%,目前個人的標準是90%+。

  • 可重複執行

每一個測試用例,無論在任何環境下,都應該可以反覆執行,併產生相同的結果。只有這樣,才能夠相任你的測試,進而發現真正的bug。這也是整合測試最低要求。

  • 保持獨立

一個測試用例只測試程式碼的某一方面,如一個分支,且不強依賴某些特定的環境或條件。

  • 可讀、可維護、可信任
  • 快快快

無論是單個測試用例,還是整合測試,必須要保證測試執行足夠的快。

測什麼

測試測什麼主要依據具體的需求、業務、成本、語言等,但也有一定的共性,單元測試準則 給出了一些準則可供參考,這裡不再討論。

怎麼測

又是一個非常大的話題,本文僅從個人角度給出nodejs測試的工具及方法。

概述

框架

Nodejs 測試框架眾多,目前使用最廣的是Mocha。本文詳細說明下 Mocha, 並簡要介紹幾種其它框架。本文的示例,如無特別說明,都是基於Mocha.

Mocha

mocha_logo

A simple, flexible, fun JavaScript test framework for node.js and the browser.

Mocha 是一個功能豐富的Javascript測試框架,它能執行在Node.js和瀏覽器中,支援BDD、TDD式的測試。

快速開始

  • 安裝
npm install -g mocha
  • 寫一個簡的測試用例
var assert = require(`chai`).assert;
describe(`Array`, function() {
  describe(`#indexOf()`, function () {
    it(`should return -1 when the value is not present`, function () {
      assert.equal(-1, [1,2,3].indexOf(5));
      assert.equal(-1, [1,2,3].indexOf(0));
    });
  });
});
  • 執行
$ mocha

輸出結果如下:

  .

   1 test complete (1ms)

使用

斷言

Mocha 允許你使用任何你想用的斷言庫,包括:

  • should.js BDD style shown throughout these docs
  • expect.js expect() style assertions
  • chai expect(), assert() and should style assertions
  • better-assert c-style self-documenting assert()
  • unexpected the extensible BDD assertion toolkit
鉤子 Hooks

Mocha 提供了 before(), after(), beforeEach(), afterEach()等 hooks,用於設定前置條件及清理測試,示例如下:

describe(`hooks`, function() {
  before(function() {
    // runs before all tests in this block
  })
  after(function(){
    // runs after all tests in this block
  })
  beforeEach(function(){
    // runs before each test in this block
  })
  afterEach(function(){
    // runs after each test in this block
  })
  // test cases
})
專用測試 或 跳過測試

專用測試允許只測試指定的測試集或用例,只需在測試集或用例前加.only(),示例如下:

describe(`Array`, function(){
  describe.only(`#indexOf()`, function(){
    ...
  })
})

跳過類似於 junit 的 @Ignore, 用於跳過或忽略指定的測試集或用例,只需在測試集或用例前加.skip(),示例如下:

describe(`Array`, function(){
  describe.skip(`#indexOf()`, function(){
    ...
  })
})
編輯器外掛

除了使用 Mocha 提供的命令列外,還可以使用 編輯器外掛 執行,目前支援了:

  • TextMate
  • JetBrains
  • Wallaby.js
  • Emacs

以JetBrains為例,JetBrain 為其 IDE 套件(IntelliJ IDEA, WebStorm等)提供了 NodeJS, 可以直接執行或除錯 mocha 測試用例。基本步驟:

  • 安裝 NodeJS 外掛(如果沒有安裝的話,預設IntelliJ IDEA, WebStorm都已安裝):通過 Preferences > Plugins 查詢名為 NodeJS 的外掛並安裝;
  • 新增 mocha 測試。通過 Edit Configuration 新增 Mocha, 示例如下:

add_mocha_test

  • 執行或除錯。支援按目錄、檔案、測試集、測試用例等執行或除錯,執行示例如下:

run_mocha_test

其它

以下列舉幾個基於 nodejs 的測試框架或工具,它們可用於 nodejs 或 瀏覽器端javascript 程式碼的測試,不在本文討論範圍,詳見官方文件。

Jasmine

jasmine_logo

A Behavior Driven Development JavaScript testing framework

Jasmine is a behavior-driven development framework for testing JavaScript code. It does not depend on any other JavaScript frameworks. It does not require a DOM. And it has a clean, obvious syntax so that you can easily write tests.

更多見 官方文件.

Karma Runner

karma_logo

To bring a productive testing environment to developers

The main goal for Karma is to bring a productive testing environment to developers. The environment being one where they don`t have to set up loads of configurations, but rather a place where developers can just write the code and get instant feedback from their tests.

更多見 官方文件.

工具

mocha 提供了測試的基本框架,在特定的場景下的測試還需要其它工具做以輔助,這個列舉幾個常用的工具。

SuperTest

SuperTest 提供對 HTTP 測試的高度抽象, 極大地簡化了基於 HTTP 的測試。

The motivation with this module is to provide a high-level abstraction for testing HTTP, while still allowing you to drop down to the lower-level API provided by super-agent.

安裝
$  npm install supertest --save-dev
使用示例
  • 簡單的 HTTP 請求
var request = require(`supertest`);

describe(`GET /user`, function() {
  it(`respond with json`, function(done) {
    request(app)
      .get(`/user`)
      .set(`Accept`, `application/json`)
      .expect(`Content-Type`, /json/)
      .expect(200, done);
  });
});
  • 上傳檔案
request(app)
    .post(`/`)
    .field(`name`, `my awesome avatar`)
    .attach(`avatar`, `test/fixtures/homeboy.jpg`)
    // ..
  • 修改響應頭或體
describe(`GET /user`, function() {
  it(`user.name should be an case-insensitive match for "tobi"`, function(done) {
    request(app)
      .get(`/user`)
      .set(`Accept`, `application/json`)
      .expect(function(res) {
        res.body.id = `some fixed id`;
        res.body.name = res.body.name.toUpperCase();
      })
      .expect(200, {
        id: `some fixed id`,
        name: `TOBI`
      }, done);
  });
});

更多見 文件

Istanbul

程式碼覆蓋(Code coverage)是軟體測試中的一種度量,描述程式中原始碼被測試的比例和程度,所得比例稱為程式碼覆蓋率。它有四個測量維度:

  • 行覆蓋率(line coverage):是否每一行都執行了
  • 函式覆蓋率(function coverage):是否每個函式都呼叫了
  • 分支覆蓋率(branch coverage):是否每個if程式碼塊都執行了
  • 語句覆蓋率(statement coverage):是否每個語句都執行了

Istanbul 是 JavaScript 語言最流行的程式碼覆蓋率工具。

快速開始
  • 安裝
$ npm install -g istanbul
  • 執行

最簡單的方式:

$ cd /path/to/your/source/root
$ istanbul cover test.js

輸出執行結果:

  ..
  test/app/util/result.test.js
     should static create
     should be success
     should be static success
     should be error
     should be static error

  299 passing (13s)

[mochawesome] Report saved to /opt/source/node_modules/.mochawesome-reports/index.html

=============================== Coverage summary ===============================
Statements   : 92.9% ( 1505/1620 )
Branches     : 85.42% ( 410/480 )
Functions    : 94.33% ( 133/141 )
Lines        : 93.01% ( 1504/1617 )
================================================================================
Done

這條命令同時還生成了一個 coverage 子目錄,其中的 coverage.json 檔案包含覆蓋率的原始資料,coverage/lcov-report 是可以在瀏覽器開啟的覆蓋率報告,如下圖所示:

icov

模式

常見的測試模式包括TDD, BDD。

TDD (Test Driven Development)

測試驅動開發是敏捷開發中的一項核心實踐和技術,也是一種設計方法論。TDD的原理是在開發功能程式碼之前,先編寫單元測試用例程式碼,測試程式碼確定需要編寫什麼產品程式碼。TDD的基本思路就是通過測試來推動整個開發的進行,但測試驅動開發並不只是單純的測試工作,而是把需求分析,設計,質量控制量化的過程。整體流程如下:

tdd_flowchart

以一個階乘的小程式為例。先寫出的測試用例,如下所示。此時執行肯定會報錯,因為被測試的程式還沒有寫。

var assert = require(`assert`),
    factorial = require(`../index`);

suite(`Test`, function (){
    suite(`#factorial()`, function (){
        test(`equals 1 for sets of zero length`, function (){
            assert.equal(1, factorial(0));
        });

        test(`equals 1 for sets of length one`, function (){
            assert.equal(1, factorial(1));
        });

        test(`equals 2 for sets of length two`, function (){
            assert.equal(2, factorial(2));
        });

        test(`equals 6 for sets of length three`, function (){
            assert.equal(6, factorial(3));
        });
    });
});

開始寫階乘的邏輯,如下所示。

module.exports = function (n) {
    if (n < 0) return NaN;
    if (n === 0) return 1;

    return n * factorial(n - 1);
};

此時再執行之前的測試用例,看其是否通過,如果全部通過則表示開發完成,如不通過則反覆修改被測試的邏輯,直到全部的通過為止。

BDD (Behavior Driven Development)

行為驅動開發是一種敏捷軟體開發的技術,它鼓勵軟體專案中的開發者、QA和非技術人員或商業參與者之間的協作。主要是從使用者的需求出發,強調系統行為。其最顯著的特徵是,通過編寫行為和規格說明來驅動軟體開發。行為和規格說明看上去與測試十分相似,但它們之前還是有顯著的不同。

還是以上面的階乘為例,BDD模式的測試用例如下:

var assert = require(`assert`),
    factorial = require(`../index`);

describe(`Test`, function (){
    before(function(){
        // Stuff to do before the tests, like imports, what not
    });

    describe(`#factorial()`, function (){
        it(`should return 1 when given 0`, function (){
            factorial(0).should.equal(1);
        });

        it(`should return 1 when given 1`, function (){
            factorial(1).should.equal(1);
        });

        it(`should return 2 when given 2`, function (){
            factorial(2).should.equal(2);
        });

        it(`should return 6 when given 3`, function (){
            factorial(3).should.equal(6);
        });
    });

    after(function () {
        // Anything after the tests have finished
    });
});

從示例上看,BDD的測試用例與TDD最大的不同在於其措辭,BDD讀起來更像是一個句子。因此BDD的測試用例可作為開發者、QA和非技術人員或商業參與者之間的協同工具,對開發者來說,如果你可以非常流暢地讀測試用例,自然能夠寫出更好、更全面的測試。

TDD vs BDD

TDD和BDD本質和目標都是一致的。只是在實施方法上,進行了不同的探討來完善整個敏捷開發的體系。TDD的迭代反覆驗證是敏捷開發的保障,但沒有明確如何根據設計產生測試,並保障測試用例的質量,而BDD倡導大家都用簡潔的自然語言描述系統行為的理念,恰好彌補了測試用例(即系統行為)的準確性。

幾乎所有基於Nodejs的庫或應用都選擇了BDD, 至於為什麼,我還沒有搞明白。

斷言

目前比較流行的斷言庫包括:

  • should.js BDD style shown throughout these docs
  • expect.js expect() style assertions
  • chai expect(), assert() and should style assertions
  • better-assert c-style self-documenting assert()
  • unexpected the extensible BDD assertion toolkit

這些斷言庫除了風格上略有不同外,實現在功能幾乎完全一致,可根據個人喜好或應用需求選擇。

下一篇:Nodejs測試:從0到90(實踐篇)


相關文章