提高程式碼質量——使用Jest和Sinon給已有的程式碼新增單元測試

黃Java發表於2018-04-18

概述

在日常的功能開發中,我們的程式碼測試都依賴於自己或者QA進行測試。這些操作不僅費時費力,而且還依賴開發者自身的驅動。在開發一些第三方依賴的庫時,我們也沒有辦法給第三方提供完整的程式碼質量報告。

現在,我們可以使用單元測試來提高自己的程式碼質量。下面,我將自己在使用Jest和Sinon.js配置和編寫單元測試中的收穫的經驗和踩到的坑進行總結,根據從零開始配置和編寫單元測試這一條線來進行分享。

通過本文,你可以解決以下問題:

  • Jest與Sinon.js是什麼?
  • 如何配置Jest與Sinon.js,從而編寫單元測試?
  • 如何解決進行單元測試中遇到的常見問題?

Jest與Sinon.js是什麼

Jest是FaceBook推出的一個針對JavaScript進行單元測試的庫,它提供了斷言、函式模擬等API來對你自己編寫的業務邏輯程式碼進行測試後。

Sinon.js是一個用來做獨立測試和模擬的JavaScript庫。它在單元測試的編寫中通常用來模擬HTTP等相關請求。

為什麼沒有用其他的單元測試框架

在最開始的框架選擇中,我先嚐試了能夠並行測試,大大提高單元測試速度的ava框架。它能滿足日常的普通需求如utils工具集的測試,也能夠配置Sinon.js來進行HTTP模擬測試。

但是,在處理webpack alias的問題時,通過官方issue中的極其複雜的配置也沒有能夠解決出現Cannot find module的問題(其中一個解決此問題的外掛babel-plugin-webpack-loaders中竟然是推薦直接使用Jest,囧)。

而在Jest中,可以很方便的通過一些簡單配置,就能夠識別在檔案中使用的webpack alias,相關的具體方法將會在後面章節進行具體描述。

而對於其他的測試框架如:Mocha或者Chai等,沒有進行具體的瞭解,因此在這裡不多做評價。

如何配置Jest與Sinon.js,從而編寫單元測試?

Jest配置

安裝依賴包

需要使用Jest,首先你需要進行安裝,執行以下命令:

npm install jest -D
複製程式碼

如果你的專案中存在.babelrc檔案(使用了babel 6)時,不論你測試的程式碼是否通過babel進行編譯,你都需要安裝額外的幾個包:

npm install babel-jest babel-core regenerator-runtime -D
複製程式碼

如果你使用的是babel 7,則需要安裝下面幾個包:

npm install babel-jest 'babel-core@^7.0.0-0' @babel/core regenerator-runtime -D
複製程式碼

package.json檔案配置

在安裝完成依賴包以後,如果你有相關的jest配置項需要設定,你還可以在package.json檔案中配置如下欄位:

{
  "jest": {
    
  }
}
複製程式碼

.babelrc檔案只需要儲存之前的配置,不需要做任何修改即可生效。

Sinon.js配置

依賴包安裝

安裝配置完了Jest,讓我們來看下Sinon.js。需要使用Sinon.js,我們首先需要進行安裝:

npm install sinon -D
複製程式碼

配置完成後,需要在使用的地方進行引入,如下所示:

const sinon = require('sinon');
複製程式碼

在我的專案中,主要是使用Sinon.js來模擬HTTP請求。在Sinon.js的文件中,有專門關於XMLHttpRequest物件的模擬的章節,在下一章中,我們將會針對專案中sinon.js的使用進行簡單的介紹。

編寫單元測試

在本章中,我們會針對如何編寫單元測試檔案進行一個具體的講解,其中包含:

  • 同步函式測試
  • 非同步函式測試
  • HTTP測試

同時,我們會對當中使用到的Jest和Sinon.js的API會進行簡單介紹,如果需要使用其他的API,可以自行閱讀JestSinon.js的文件。

通過上面三類測試,我們基本能夠覆蓋現有專案中的所有程式碼。

同步函式測試

同步函式的測試過程是這幾個中最簡單的一部分,我們可以測試函式返回值,也能夠測試傳入的高階函式。下面我們通過一個具體的例子來看下。

原始碼檔案,一個純函式:

// user.js
export default function(obj) {
    return 'hjava';
}

export function handleUserData(callback) {
    callback('hjava');
}
複製程式碼

針對上面的原始碼檔案編寫的一個單元測試檔案:

// user.test.js
import userFunc, {handleUserData} from './user';

// test是一個註冊的全域性方法
test('user', () => {
    expect(userFunc()).toBe('hjava'); // 判斷userFunc的執行結果等於'hjava'
    
    let callback = jest.fn(); // jest是一個註冊的全域性變數
    handleUserData(callback);

    expect(callback.mock.calls.length).toBe(1); // 判斷callback函式被呼叫了一次
    expect(callback.mock.calls[0][0]).toBe('hjava'); // 判斷了callback函式的第一次被呼叫的第一個引數為'hjava'
});
複製程式碼

從上面的示例中我們可以看到,針對同步的純函式,我們可以通過很簡單的單元測試模型來驗證它的功能。

非同步函式測試

非同步函式主要分為兩種——Callback方式和Promise方式。這兩種方式都很簡單,下面我們對兩種方式進行具體的介紹。詳細內容可以見Jest文件中的測試非同步程式碼

Callback方式

// user.js
export default function(callback) {
    setTimeout(()=>{
        callback({username: 'hjava'});
    }, 1000);
}
複製程式碼
// user.test.js
import userFunc from './user';

test('user', () => {
    userFunc((data) => {
        expect(data).toEqual({username: 'hjava'}); // 物件比較用beEqual()
    });
});
複製程式碼

Promise方式

// user.js
export default function(callback) {
    return Promise.resolve({username: 'hjava'});
}

複製程式碼
// user.test.js
import userFunc from './user';

test('user', () => {
    userFunc().then((data) => {
        expect(data).toEqual({username: 'hjava'});
    });
});
複製程式碼

HTTP測試

在測試HTTP請求相關引數的過程中,我們需要模擬XMLHttpRequest物件,從而攔截相關的HTTP請求,獲取請求資料。正好Sinon.js能夠做到這一點。下面我們通過一個示例來看下相關的邏輯:

// user.js
export default function(callback) {
    this.sendRequest('/user/get', callback); // 傳送請求來獲取使用者資料,成功後執行callback回撥函式
}
複製程式碼
// user.test.js
import Sinon from 'sinon';
import userFunc from 'user';

let XHR;
let requests = [];
// beforeEach是Jest提供的函式,在每個測試執行前都會執行一次
beforeEach(() => {
    XHR = sinon.useFakeXMLHttpRequest(); //建立一個模擬的XMLHttpRequest物件

    XHR.onCreate = function (xhr) {
        requests.push(xhr);
    };
});

// afterEach是Jest提供的函式,在每個測試執行後都會執行一次
afterEach(() => {
    XHR.restore();
});

test('user', () => {
    let callback = jest.fn();

    HTTPCommon.deleteRemoteSession({
        data: {},
        success: callback
    });

    expect(requests.length).toBe(1);

    requests[0].respond(200, {"Content-Type": 'application/json'}, 'hjava'); // 模擬返回值

    expect(callback.mock.calls[0][0]).toBe('hjava');
});
複製程式碼

如何解決進行單元測試中遇到的常見問題?

在本章中,我們總結了如下問題來進行介紹,希望大家再遇到相同問題時能夠快速解決:

  • 如何統計Jest單元測試覆蓋率
  • 如何設定單元測試檔案不使用本地的babel配置
  • 如何設定單元測試檔案使用本地的babel配置
  • 如何處理程式碼中引用的webpack alias問題

如何統計單元測試覆蓋率?

不像ava一樣,需要使用syc來進行計算,Jest內建了統計單元測試覆蓋率的工具,只需要簡單配置即可達到相關的要求。具體配置如下:

// package.json
{
  "jest": {
    "collectCoverage": true, // 是否開啟統計單元測試覆蓋率
    "collectCoverageFrom": [ // 指定統計單元測試覆蓋率檔案
      "**/src/**.js"
    ],
  }
}
複製程式碼

如何設定單元測試檔案不使用ES2015配置

如果你的專案中有.babelrc檔案,而你不希望單元測試檔案受到babel檔案的影響,你可以在jest的配置項中增加transform欄位,具體配置如下:

// package.json
{
  "jest": {
    "transform": {}
  }
}
複製程式碼

如何設定單元測試使用ES2015配置

如果你的單元測試檔案中需要使用ES2015後通過babel來進行編譯,那麼需要對.babelrc檔案的配置進行部分修改。

如果你之前在.babelrc檔案中,把modules欄位設定為false,那麼你需要在test環境下重新開啟,具體程式碼如下:

// .babelrc
{
  "presets": [["env", {"modules": false}]],
  "env": {
    "test": {
      "presets": [["env"]]
    }
  }
}
複製程式碼

如果你使用的是babel 7的話(安裝時多安裝過相關依賴包),你需要設定的presets欄位的值應該為@babel/env,具體程式碼如下:

// .babelrc
{
  "presets": [["env", {"modules": false}]],
  "env": {
    "test": {
      "presets": [["@babel/env"]]
    }
  }
}
複製程式碼

如何處理程式碼中引用的webpack alias問題

如果我們在專案中使用了webpack,那麼我們很大概率會使用到alias相關屬性來定義路徑。但是,在單元測試框架中,它並不能夠識別這種路徑,就會出現Cannot find module 'xxx' from 'yyy'的報錯。

不像ava框架需要安裝外掛和進行復雜的配置,我們只需要在Jest中配置moduleNameMapper屬性即可滿足需求。具體示例如下:

// webpack.config.js
{
    alias: {
        '@__dir':process.cwd()
    }
}
複製程式碼
//package.json
{
    "jest": {
        "moduleNameMapper": {
        "@__dir(.*)$": "<rootDir>$1" //正則匹配方式,對應webpack alias
        }
    }
}
複製程式碼

總結

編寫測試是一個很好的習慣。

很多人經常都說要對自己的程式碼進行質量監控,但是又不知道該如何下手。通過這篇文章,你應該學會了如何針對已有程式碼從零開始編寫一套完整的單元測試用例。

如果有任何疑問,歡迎留言或者私信進行溝通與交流。

關於Jest是如何測試JavaScript程式碼以及Sinon是如何模擬XMLHttpRequest請求的,我們將會在後面幾篇部落格中給大家帶來相關的原始碼解析,有興趣的同學可以關注我,留意後續的文章。

附錄

相關文章