1. 程式人生 > >[gist]用 jest 輕鬆測試 JavaScript

[gist]用 jest 輕鬆測試 JavaScript

from oyanglul.us

Javascript 的測試, 不管在用 jasmine 還是 mocha,
都是很頭疼的事情. 但是自從有了 jest, 一口氣寫7個測試, 腰也不疼了, 頭也不疼了.

只需要 3 個理由

在說用 jest 測為什麼好之前,我們先來看我們要測的一個例子.

栗子

比如我要寫一個模組要去取github 使用者的follower 和他所有 repo 的 follower 數量.

那麼我們應該有一個 User 的 Model.

// user.js
var $ = require('jquery');
function User(name) {
  this.name = name;
  this.followers = 0;
}
User.prototype.fetch = function(){
  return $.ajax({
    url: 'https://api.github.com/users/' + this.name,
    method: 'get',
    dataType: 'json'
  }).then(function(data){
      this.followers = data.followers;
  }.bind(this));
};
module.exports = User;

我們還需要一個 repo 的 model, 大同小異略去

最後, 整合這倆我要的東西, 並顯示在頁面上

// follower.js
var $ = require('jquery');
function followerOf(user, repo) {
  user.fetch().then(repo.fetch).then(function(_){
    $('#content').text(user.name +"'s followers: " + user.followers +
                       " and his repo "+ repo.name +"'s followers:" + repo.followers);
    });
};

module.exports = followerOf;

--

1. Auto Mock

自動 mock 實在是最大的亮點, jest 重寫了 require, 所以你的程式碼裡的所有 require 來的東西都自動 mock.

因為在你的測試中往往只關心一個模組, 對於他的所有依賴其實都是無所謂的.

在例子中, 如果我們在測 repo.js 的時候完全不關心那兩個 jquery 的 ajax 方法到底
寫對沒寫對,反正我們期望能從 ajax 裡面拿到我們想要的東西就對了. 因此, 我希望 jquery 的
所有方法都是 mock 的. jest 讓你很輕鬆的做到這點, 因為是自動mock所有require 的東西, 而
對於目標測試模組, 只需要說我dontMock

我的目標模組就好了.

jest.dontMock('../repo');
describe('Repo Model', function(){
  var repo;
  beforeEach(function(){
        var $ = require('jquery').setAjaxReturn({stargazers_count: 23});
        var Repo = require('../repo');
    repo = new Repo('jcouyang', 'gira');

    });

  it('should populate properties with data from github api', function(){
        repo.fetch();
        expect(repo.followers).toBe(23);
  });
});

所以這個測試看起來就跟文件一樣了,

  1. dontMock('./repo') 說明我關心repo
    這個模組, 其他我都不 care.
  2. before 是我要進行操作所需要的東西.

    • 我要 jquery ajax 請求給我想要的資料
    • 我要一個我要測的 Repo 類的例項
  3. it 說明我關心地行為是神馬

    • 我關心 fetch 的行為,是去取資料並給我把資料填充到我的 repo 例項中

你可能要問 segAjaxReturn 是哪裡冒出來的. 忍一忍稍後告訴你.

有沒有看雖然我顯式的 mock jquery, 但是 Repo 裡面 require 的 jquery 其實是假的, 不然我們就真的訪問
github api 了. 那樣就不會每次都返回 23 個 follower 了.

2. jsdom

好了現在我們來測 follower.js, 先看 follower 到底幹了什麼, 拿到 user 和 repo
的資訊然後組成一句話放到頁面 id 為 content 的元素下面.

好, 所以我們關心
- 組出來的話對不對
- 有沒有放到 content 元素下, 所以 jquery 的操作對不對也是我們關心的一部分

我們不關心
- user 幹了什麼
- repo 幹了什麼

這樣,關心的就是不能 mock 的

jest.dontMock('../follower')
    .dontMock('jquery');
describe('follower', function(){
  var user, repo, follower;
    var $ = require('jquery');
  beforeEach(function(){
        var Repo = require('../repo');
        var User = require('../user');
        follower = require('../follower');
        user = new User('jcouyang');
    repo = new Repo('jcouyang', 'gira');
    // 我們不關心 user, 但是我們希望他能返回一個 deferred 型別
      user.fetch.mockReturnValue($.Deferred().resolve('dont care'));
    // 我們讓我們不關心的 user 和 repo 返回我們期望的東西就好
        user.name ='jcouyang';
        user.followers = 20;
        repo.name = 'gira';
        repo.followers = 21;
    // 期待頁面上有一個  id 為 content 的元素
        document.body.innerHTML = '

<div id="content"></div>

';
    });

  it('should populate properties with data from github api', function(){
        follower(user,repo);
    // 希望 content 上能得到想要的內容
        expect($("#content").text()).toBe('jcouyang\'s followers: 20 and his repo gira\'s followers:21');
  });
});

3. Manual Mock

好了, 說好的解釋 setAjaxReturn是怎麼回事的

嗯嗯, 是這樣的, 雖然 jest 自動 mock 了我們不關心的模組, 但是我們還是會希望
這個 mock 的玩意能有一些我們期望的行為, 也就是按我們的期望返回一些東西. 比如
這裡就是我們不關心 ajax 的邏輯, 但是我們需要他能給我們返回一個東西,並且可以
thenable. 所以單純的 mock 物件或函式都不能做到, 所以有了 manual mock 這種東西.

用 manual mock 需要建一個__ mocks__ 資料夾,然後把所有的 mock 都扔進去. 比如
我想 mock jquery, 那麼我建一個jquery.js 扔進去

var data = {};
var mockDefered = function(data){
    return {
        then: function(cb){
            return mockDefered(cb(data));
        }
    };
};

function ajax() {
  return mockDefered(data);
}

function setAjaxReturn(shouldbe){
    data = shouldbe;
}
exports.setAjaxReturn = setAjaxReturn;
exports.ajax = ajax;

終於看見setAjaxReturn在哪裡定義了:sweat_smile: 這裡暴露兩個函式
- setAjaxReturn: 可以設定我希望 ajax 返回的值
- ajax: 單純的返回這個 thenable.

所以我也不需要顯示的宣告 mock jquery什麼什麼的, 直接在測試裡設定ajax 的返回值就好了.

var $ = require('jquery').setAjaxReturn({stargazers_count: 23});

這是 repo 裡面 require 的 jquery 已經被 mock 並且只要掉 ajax 都會返回我
期望的值.

etc

  • 並行測試:
    還用說麼, 既然已經如此模組化好了, user repo 以及 follower 的測試完全是互不依賴.
    沒有什麼理由一個一個測. 因此3個測試的耗時取決於最長時間的那個. 所以如果有
    那個測試特別耗時,說明模組還不夠細, 多拆幾個就快了.
  • promise: 使用 pit() 來測試 thenable 的物件, 比如 repo 的例子,就 keyi
    寫成
pit('should populate properties with data from github api', function(){
  return repo.fetch().then(
    expect(repo.followers).toBe(23);
  );
});
  • Timer mocks: 可以使用 mock 的 timer 和 ticks, 也就是你可以加速
    所有的setTimeout, setInterval, clearTimeout, clearInterval行為. 不需要等待.
setTimeout(function() { callback(); }, 1000);
 expect(callback).not.toBeCalled();
 jest.runAllTimers();
expect(callback).toBeCalled()

Wrapup

所以說白了, jest 其實也是個概念, 推薦使用模組化的思想, 這樣我只需要保證每個介面的 IO 正確, 就可以保證整個程式沒問題. 這樣劃分下來測試就會變得簡單到只需要關心當然模組的 IO 從而
可以 mock 掉所有其他依賴. 真正模組化好的程式碼單純的只用 jasmine 或者 mocha
都應該是很好測的. 只是在這個概念之上省去了很多不必要的 mock 程式碼, 因為要 mock 的
依賴總是佔大多數的, 而關心的, 往往只是那麼一兩個.