關於前端開發談談單元測試

單元測試Unit Test

很早就知道單元測試這樣一個概念,但直到幾個月前,我真正開始接觸和使用它。究竟什麼是單元測試?我想也許不少使用了好久的人也不必定能描述的十分清楚,因此寫了這篇文章來嘗試描述它的特徵和原則,以幫助更多人。html

什麼是單元測試?

先來看看單元測試的定義,在維基百科英文版中能夠找到Kolawa Adam在 Automated Defect Prevention: Best Practices in Software Management 一書中對單元測試的定義:前端

In computer programming, unit testing is a method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures are tested to determine if they are fit for use.node

重點在於最後,單元測試的目的顯而易見,用來肯定是否適合使用。而測試的方法則包括控制數據,使用和操做過程。那麼以個人理解,每一個單元測試就是一段用於測試一個模塊或接口是否能達到預期結果的代碼。開發人員須要使用代碼來定義一個可用的衡量標準,而且能夠快速檢驗。jquery

很快我發現有一個誤區,許多人認爲單元測試必須是一個runner集中運行全部單元的測試,並一目瞭然。不,這僅僅是一種自動化單元測試的最佳實踐,在一些小型項目中單元測試可能僅僅是一組去除其餘特性的接口調用。甚至在一些圖形處理或佈局的項目中單元測試能夠結合自身特性變的十分有趣,好比Masonry,一個網格佈局庫,在它的單元測試中不是一個紅或綠的條目,而是一行一行的小格佈局用以說明佈局被完成的事實,這樣比代碼檢查佈局是否正確再以顏色顯示結果來得更直觀高效,也避免了測試程序自己的bug致使的失誤。git

打個比方,單元測試就像一把尺子,當測量的對象是一個曲面時,也許能夠花費大力氣去將它抽象成平面,但我更提倡量身定作一把彎曲的尺子去適應這個曲面。不管怎樣,單元測試是爲了生產代碼而寫,它應當足夠的自由奔放,去適應各類各樣的生產代碼。github

爲何要單元測試?

也許定義中已經很清楚的指明瞭其意義,確認某段代碼或模塊或接口是否適合使用,但我想會有更多的人認爲,直接在測試環境中使用軟件能夠更加確保軟件是否可用。不,在實際使用過程當中會伴隨着一大批的附帶操做大量增長測試時間,而且沒法保證其測試覆蓋率。因此我認爲單元測試的目的並不只僅是確認是否可用,而是更高效更穩定的確認其是否可用。web

隨着項目規模的增長,函數、方法、變量都在遞增,尤爲是進度的不足,來自產品經理的壓力,還有QA所帶來的各類Bug報告會讓本來整潔的代碼變得一片混亂。我甚至見過同一個接口以不一樣的名稱出如今8個不一樣的控制器中。這時也許咱們首先想到的是重構,但是等等,在重構結束時咱們如何肯定項目僅僅是被重構了,而不是被改寫了?此時單元測試將是一根救命稻草,它是一個衡量標準,告訴開發人員這麼作是否將改變結果。ajax

不只僅是這樣。許多人認爲單元測試,甚至整個測試都是在編碼結束後的一道工序,而修復bug也不過是在作垃圾掩埋一類的工做。但測試應該伴隨整個編碼或軟件週期進行,還有將在後面提到的TDD這樣有趣的東西,單元測試將超前於編碼。個人意思是,單元測試應該是一個框架、標準,常常被形容被腳手架,像建築同樣,腳手架的高度至少應該和大樓高度不相上下,甚至一開始就搭好腳手架。正則表達式

如何作單元測試?

弄清了單元測試的目的和意義,但如何開始?很簡單,首先它是一個檢驗,因此應該只有pass或fail兩種狀況。而檢驗的對象應該是某個接口或模塊,因此應該調用它得到一個結果。檢驗這個結果就是單元測試的基本動做,就拿一個除法函數來作例子:數據庫

function division (a, b) {
    return a / b;
}
var result = division(4, 2);
if (result === 2) {
    alert('pass');
} else {
    alert('fail');
}

顯然,將會提示pass經過。可是問題來了,這個測試的用例太單一和普通了,若是使用0作除數呢?是NaN?仍是Infinity?或者在實際使用時,產品須要一個0來代替這樣一個不符合數學概念的結果去適應必須爲數字類型的某種計算,因而division出現了一個bug。另外當覆蓋率增長,也意味着用例的增長,咱們須要把if條件語句提出來作成一個函數屢次調用。還有alert方法,若是用例太多,我相信你會點確認點到手軟,也許能夠直接顯示在頁面上。

因此我添加了一個關於除數爲0的用例,並重構了代碼:

function division (a, b) {
    if (b === 0) {
        return 0;
    } else {
        return a / b;
    }
}
function matcher (name, result, expect) {
    if (result === expect) {
        _print(name + '- pass');
    } else {
        _print(name + '- fail');
    }
    function _print (str) {
        var _bar = document.createElement('p');
        _bar.innerText = str;
        document.body.appendChild(_bar);
    }
}
matcher('normal', division(4, 2), 2);
matcher('zero', division(5, 0), 0);

如今可使用matcher方法添加許多測試用例,而且還能爲該用例命名,在頁面中直接顯示每一個用例是否經過。這樣一個基本的單元測試就完成了,固然它的覆蓋率還遠遠不夠,這裏僅做爲一個例子。另外爲了提升效率還應該使用顏色來標記是否經過,能夠一目瞭然。

測試驅動開發

TDD是Test Driven Development 的縮寫,也就是測試驅動開發。

一般傳統軟件工程將測試描述爲軟件生命週期的一個環節,而且是在編碼以後。但敏捷開發大師Kent Beck在2003年出版了 Test Driven Development By Example 一書,從而確立了測試驅動開發這個領域。

TDD須要遵循以下規則:

  • 寫一個單元測試去描述程序的一個方面。
  • 運行它應該會失敗,由於程序還缺乏這個特性。
  • 爲這個程序添加一些儘量簡單的代碼保證測試經過。
  • 重構這部分代碼,直到代碼沒有重複、代碼責任清晰而且結構簡單。
  • 持續重複這樣作,積累代碼。

另外,衡量是否使用了TDD的一個重要標準是測試對代碼的覆蓋率,覆蓋率在80%如下說明一個團隊沒有充分掌握TDD,固然高覆蓋率也不能說必定使用了TDD,這僅僅是一個參考指標。

在我看來,TDD是一種開發技術,而非測試技術,因此它對於代碼構建的意義遠大於代碼測試。也許最終的代碼和先開發再測試寫的測試代碼基本一致,但它們仍然是有很大不一樣的。TDD具備很強的目的性,在直接結果的指導下開發生產代碼,而後不斷圍繞這個目標去改進代碼,其優點是高效和去冗餘的。因此其特色應該是由需求得出測試,由測試代碼得出生產代碼。打個比方就像是自行車的兩個輪子,雖然都是在向同一個方向轉動,可是後輪是施力的,帶動車子向前,而前輪是受力的,被向前的車子帶動而轉。

行爲驅動開發

所謂的BDD行爲驅動開發,即Behaviour Driven Development,是一種新的敏捷開發方法。它更趨向於需求,須要共同利益者的參與,強調用戶故事(User Story)和行爲。2009年,在倫敦發表的「敏捷規格,BDD和極限測試交流」[3]中,Dan North對BDD給出了以下定義:

BDD是第二代的、由外及內的、基於拉(pull)的、多方利益相關者的(stakeholder)、多種可擴展的、高自動化的敏捷方法。它描述了一個交互循環,能夠具備帶有良好定義的輸出(即工做中交付的結果):已測試過的軟件。

另外最主觀的區別就是用詞,‘example’取代了‘test’,‘describe’取代了‘class’,‘behaviour’取代了‘method’等等。這正是其特徵之一,天然語言的加入,使得非程序人員也能參與到測試用例的編寫中來,也大大下降了客戶、用戶、項目管理者與開發者之間來回翻譯的成本。

簡單來講,我認爲BDD更加註重業務需求而不是技術,雖然看起來BDD確實是比ATDD作的更好,但這是一種誤導,這僅僅是就某種環境下而言的。並且以國內的現狀來看TDD要比BDD更適合,由於它不須要全部人員的理解和加入。

單元測試框架

不管如何,單元測試永遠是少不了的。其實在單元測試中測試代碼和生產代碼應該是等量的,正如Robert C. Martin在其 Clean Code: A Handbook of Agile Software Craftsmanship 一書中所寫:

測試必須隨生產代碼的演進而修改,測試越髒就越難修改

因而新的測試很難被加入其中,測試代碼的維護變得異常困難,最終在各類壓力之中只有扔掉測試代碼組。可是沒有了測試代碼,就失去了確保對代碼的改動能如願以償的能力,各類問題隨之而來。所以,單元測試也須要一種行之有效的實踐來確保其質量和可維護性。

因此正如生產代碼同樣,測試代碼也有框架,下面介紹幾種主流的Javascript的單元測試框架。

Jasmine

有一類框架叫作xUnit,來源於著名的JAVA測試框架JUnit,xUnit則表明了一種模式,而且使用這樣的命名。在Javascript中也有這樣的一個老牌框架JsUnit,他的做者是Edward Hieatt來自Pivotal Labs,但在幾年前JsUnit就已經中止維護了,他們帶來了新的BDD框架Jasmine。

Jasmine不依賴於任何框架,因此適用於全部的Javascript代碼。使用一個全局函數 describe 來描述每一個測試,而且能夠嵌套。describe函數有2個參數,一個是字符串用於描述,一個是函數用於測試。在該函數中可使用全局函數 it 來定義Specs,也就是單元測試的主要內容, 使用 expect 函數來測試:

describe('A suite', function () {
    it('is a spec', function () {
        var a = true;
        expect(a).toBe(true);
    });
});

另外若是想去掉某個describe,無須註釋掉整段代碼,只須要在describe前面加上x便可忽略該describe。

Matcher

toBe方法是一個基本的 matcher 用來定義判斷規則,能夠看得出來Jasmine的方法是很是語義化的,「expect ‘a’ to be true」,若是想判斷否認條件,則只須要在toBe前調用 not 方法:

expect(a).not().toBe(false);

除了toBe這樣基本的還有許多其餘的Matcher,好比 toEqual 。不少初學Jasmine會弄不清和toBe的區別,一個簡單的例子就能明白它們的區別:

expect({}).not().toBe({});
expect({}).toEqual({});

一個新建的Object不是(not to be)另外一個新建的Object,可是它們是相等(to equal)的。還有 toMatch 可使用字符串或者正則表達式來驗證,以及其餘一些特殊驗證,好比undefined或者boolean的判斷, toThrow 能夠檢查函數所拋出的異常。另外Jasmine還支持自定義Matcher,以NaN的檢查爲例,像這樣使用beforeEach方法在每一個測試執行前添加一個matcher:

beforeEach(function () {
    this.addMatchers({
        toBeNaN: function (expected) {
            return isNaN(expected);
        }
    });
});

能夠想到,其參數expected是傳入的一個指望的字面量,而在expect方法中傳入的參數,能夠經過 this.acturl 獲取,是否調用了 not 方法則能夠經過 this.isNot 獲取,這是一個boolean值。最後測試輸出的失敗信息應該使用 this.message 來定義,不過它是一個function,而後在其中返回一個信息。因此繼續增進toBeNaN:

beforeEach(function () {
    this.addMatchers({
        toBeNaN: function (expected) {
            var actual = this.actual;
            var not = this.isNot ? ' not' : '';
            this.message = function () {
                return 'Expected ' + actual + not + ' to be NaN ' + expected;
            };
            return isNaN(expected);
        }
    });
});

這樣一個完整的matcher就建立成了。

另外須要說明的是對應beforeEach是在每一個spec以前執行, afterEach 方法則是在每一個spec以後執行。這是一種AOP,即面向方面的編程(Aspect Oriented Programming)。好比有時候爲了測試一個對象,可能須要屢次建立和銷燬它,因此爲了不冗餘代碼,使用它們是最佳選擇。

還可使用 jasmine.any 方法來表明一類數據傳入matcher中,好比

expect(123).toEqual(jasmine.any(Number));
expect(function () {}).toEqual(jasmine.any(Function));
Spy方法

一個Spy能監測任何function的調用和獲取其參數。這裏有2個特殊的Matcher, toHaveBeenCalled 能夠檢查function是否被調用過,還有 toHaveBeenCalledWith 能夠傳入參數檢查是否和這些參數一塊兒被調用過,像這樣使用 spyOn 來註冊一個對象中的方法:

var foo, a = null;
beforeEach(function () {
    var foo = {
        set: function (str) {
            a = str;
        }
    }
    spyOn(foo, 'set');
    foo.set(123);
});
it('tracks calls', function () {
    expect(foo.set).toHaveBeenCalled();
    expect(foo.set).toHaveBeenCalled(123);
    expect(foo.set.calls[0].args[0]).toEqual(123);
    expect(foo.set.mostRecentCall.args[0]).toEqual(123);
    expect(a).toBeNull();
});

在測試時該function將帶有一個被調用的數組 calls ,而 args 數組就是調用時傳入的參數,另外特殊屬性 mostRencentCall 則表明最後一次調用,和calls[calls.length]一致。須要特別注意的是,這些調用將不會對變量產生做用,因此 a 仍爲null。

若是須要調用產生實際的做用,能夠在spyOn方法後調用 andCallThrough 方法。還能夠經過調用 andReturn 方法設定一個返回值給function。 andCallFake 則能夠傳入一個function做爲參數去代替本來的function。

spyOn(foo, 'set').andCallThrough();

甚至在沒有function的時候可使用Jasmine的 createSpycreateSpyObj 建立一個spy:

foo = jasmine.createSpy('foo');
obj = jasmine.createSpyObj('obj', [set, do]);

foo(123);
obj.set(123);
obj.do();

其效果至關於spyOn使用在了已存在的function上。

時間控制

上面的方法都在程序順序執行的前提下執行,但 setTimeout 以及 setInterval 兩個方法會使代碼分離在時間軸上。因此Jasmine提供了 Clock 方法來模擬時間,以獲取setTimeout的不一樣狀態。

beforeEach(function () {
    jasmine.Clock.useMock();
});
it('set time', function () {
    var str = 0;
    setTimeout(function () {
        str++;
    }, 100);
    expect(str).toEqual(0);
    jasmine.Click.tick(101);
    expect(str).toEqual(1);
    jasmine.Click.tick(200);
    expect(str).toEqual(3);
});

使用Clock的方法 useMock 來開始時間控制,而後在it中使用 tick 方法來推動時間。

異步

Javascript最大的特點之一就是異步,以前介紹的方法若是存在異步調用,大部分測試時可能會不經過。所以,須要等異步回調以後再進行測試。

Jasmine提供了 runswaitsFor 兩個方法來完成這個異步的等待。須要將waitsFor方法夾在多個runs方法中,runs方法中的語句會按順序直接執行,而後進入waitsFor方法,若是waitsFor返回false,則繼續執行waitsFor,直到返回true才執行後面的runs方法。

var cb = false;
var ajax = {
    success: function () {
        cb = true;
    }
};
spyOn(ajax, 'success');
it('async callback', function () {
    runs(function () {
        _toAjax(ajax);
    });
    waitsFor(function () {
        return ajax.success.callCount > 0;
    });
    runs(function () {
        expect(cb).toBeTruthy();
    });
});

如此,只要在waitsFor中判斷回調函數是否被調用了便可完成異步測試。上面代碼中我使用一個方法名直接代替了ajax請求方法來縮減沒必要要的代碼。在第一個runs方法中發出了一個ajax請求,而後在waitsFor中等待其被調用,當第二個runs執行時說明回調函數已經被調用了,進行測試。

Qunit

它是由jQuery團隊開發的一款測試套件,最初依賴於jQuery庫,在2009年時脫離jQuery的依賴,變成了一個真正的測試框架,適用於全部Javascript代碼。

Qunit採用斷言(Assert)來進行測試,相比於Jasmine的matcher更加多的類型,Qunit更集中在測試的度上。 deepEqual 用於比較一些縱向數據,好比Object或者Function等。而最經常使用的 ok 則直接判斷是否爲true。異步方面Qunit也頗有趣,經過 stop 來中止測試等待異步返回,而後使用 start 繼續測試,這要比Jasmine的過程化的等待更自由一些,不過有時也許會更難寫一些。Qunit還擁有3組AOP的方法( done 和 'begin' )來對應於整個測試,測試和模塊。

對於Function的跟蹤測試,Qunit彷佛徹底沒有考慮。不過可使用另一個測試框架爲Qunit帶來的插件 sinon-qunit。這樣就能夠在test中使用 spy 方法了。

Sinon

Sinon並非一個典型的單元測試框架,更像一個庫,最主要的是對Function的測試,包括 SpyStub 兩個部分,Spy用於偵測Function,而Stub更像是一個Spy的插件或者助手,在Function調用先後作一些特殊的處理,好比修改配置或者回調。它正好極大的彌補了Qunit的不足,因此一般會使用Qunit+Sinon來進行單元測試。

值得一提的是,Sinon的做者Christian Johansen就是 Test-Driven JavaScript Development 一書的做者,這本書針對Javascript很詳細的描述了單元測試的每一個環節。

Mocha

它的做者就是在Github上粉絲6K的超級Jser TJ Holowaychuk,能夠在他的頁面上看到過去一年的提交量是5700多,擁有300多個項目,不管是誰都不可思議他是如何進行coding的。

理所固然的,Mocha充滿了Geek感,不但能夠在bash中進行測試,並且還擁有一整套命令對測試進行操做。甚至使用 diff 能夠查看當前測試與上一次成功測試的代碼不一致。

不只僅是這樣,Mocha很是得自由。Mocha將更多的方法集中在了describe和it中,好比異步的測試就很是棒,在it的回調函數中會獲取一個參數 done ,類型是function,用於異步回調,當執行這個函數時就會繼續測試。還可使用 onlyskip 去選擇測試時須要的部分。Mocha的接口也同樣自由,除了 BDD 風格和Jasmine相似的接口,還有 TDD 風格的 (suite test setup teardown suiteSetup suiteTeardown),還有AMD風格的 exports,Qunit風格等。同時測試報告也能夠任意組織,不管是列表、進度條、仍是飛機跑道這樣奇特的樣式均可以在bash中顯示。

前端測試工具

Client/Server 測試

相比於服務端開發,前端開發在測試方面始終面臨着一個嚴峻的問題,那就是瀏覽器兼容性。Paul Irish曾發表文章Browser Market Pollution: IE[x] Is the New IE6闡述了一個奇怪的設想,將來你可能須要在76個瀏覽器上開發,由於每次IE的新版本都是一個特別的瀏覽器,並且還有它對以前全部版本的兼容模式也是同樣。雖然沒人認爲微軟會繼續如此愚蠢,不過這也說明了一個問題,前端開發中瀏覽器兼容性是一個永遠的問題,並且我認爲即便解決了瀏覽器的兼容性問題,將來在移動開發方面,設備兼容性也是一個問題。

因此在自動化測試方面也是如此,即便全部的單元測試集中在了一個runner中,前端測試仍然要面對至少4個瀏覽器內核以及3個電腦操做系統加2個或更多移動操做系統,況且還有令移動開發人員頭疼的Android的碎片化問題。不過能夠安心的是,早已存在這樣的工具能夠捕獲不一樣設備上的不一樣瀏覽器,並使之隨時更新測試結果,甚至能夠在一個終端上看到全部結果。

工具介紹

JSTD(Javascript Test Driver)是一個最先的C/S測試工具,來自Google,基於JAVA編寫,跨平臺,使用命令行控制,還有很好的編輯器支持,最經常使用於eclipse。不過它沒法顯示測試對象的設備及瀏覽器版本,只有瀏覽器名是不夠的。另外JSTD已經慢慢再也不活躍,它的早正如它的老。

Google的新貴Karma出現了,它使用Nodejs構建,所以跨平臺,還支持PhantomJS瀏覽器,還支持多種框架,包括以上介紹的Jasmine、Qunit和Mocha。一次能夠在多個瀏覽器及設備中進行測試,並控制瀏覽器行爲和測試報告。雖然它不支持Nodejs的測試,不過沒什麼影響,由於Nodejs並不依賴於瀏覽器。

還有TestSwarm,出自jQuery之父John Resig之手,看來jQuery的強大果真不是偶然的,在測試方面很是到位,各類工具齊全。它最特別的地方在於全部測試環境由服務器提供,包括各類版本的主流瀏覽器以及iOS5的iphone設備,不過目前加入已經受限。

最受矚目的當屬Buster,其做者之一就是Christian Johansen。和Karma很像,也使用Nodejs編寫跨平臺而且支持PhantomJS,一次測試全部客戶端。更重要的是支持Nodejs的測試,一樣支持各類主流測試框架。不過目前還在Beta測試中,不少bug並且不能很好的兼容Windows系統。它的目標還包括整合Browser Stack

基於網頁的測試

到目前爲止咱們的測試看起來十分的完美了,可是別忘了,在前端開發中存在交互問題,不能期待QA玩了命的點擊某個按鈕或者刷新一個頁面並輸入一句亂碼之類的東西來測試代碼。即便是開發者自己也會受不了,若是產品自己擁有一堆複雜的表單和邏輯的話。

Selenium是一個測試工具集,由Thoughtworks開發,分爲兩部分。Selenium IDE是一個Firefox瀏覽器的插件,能夠錄製用戶行爲,並快速測試。

而Selenium WebDriver是一個多語言的驅動瀏覽器的工具,支持Python、Java、Ruby、Perl、PHP或.Net。而且能夠操做IE、Firefox、Safari和Chrome等主流瀏覽器。經過 open , type , click , waitForxxx 等指令來模擬用戶行爲,好比用Java測試:

public void testNew() throws Exception {
    selenium.open("/");
    selenium.type("q", "selenium rc");
    selenium.click("btnG");
    selenium.waitForPageToLoad("30000");
    assertTrue(selenium.isTextPresent("Results * for selenium rc"));
}

首先跳轉到跟目錄,而後選擇類型,點擊按鈕G,並等待頁面載入30秒,而後使用斷言測試。這樣就完成了一次用戶基本行爲的模擬,不過複雜的模擬以及在一些非連接的操做還須要格外注意,好比Ajax請求或者Pjax的無刷新等等。

另外還有一款能夠模擬用戶行爲的網頁測試工具WATIR,是Web Application Testing in Ruby的縮寫,顯然它只支持Ruby語言來操做瀏覽器模擬用戶行爲。官方聲稱它是一個簡單而靈活的工具,不管怎樣至少就官方網站的設計來看要比Selenium簡約多了。一樣支持模擬連接點擊,按鈕點擊,還有表單的填寫等行爲。不過WATIR不支持Ajax的測試。和其餘Ruby庫同樣須要gem來安裝它:

gem install watir --no-rdoc --no-ri

而後使用它

require 'rubygems'
require 'watir'
require 'watir-webdriver'
browser = Watir::Browser.new
browser.goto 'http://www.example.com/form'
browser.test_field(:name => 'entry.0.single').set 'Watir'
browser.radio(:value => 'Watir').set
browser.radio(:value => 'Watir').clear
browser.checkbox(:value => 'Ruby').set
browser.checkbox(:value => 'Javascript').clear
browser.button(:name => 'submit').click

這樣就使用watir完成了一次表單填寫。

持續集成測試

持續集成就是一般所謂的CI(Continuous integration),持續不斷的自動化測試新加入代碼後的項目。它並不屬於單元測試,而是另外的範疇,不過經過使用CI服務能夠很容易的在Github上測試項目,而這也就是持續集成的意義。

下面以個人jQ小插件Dialog爲例介紹一下Travis-CI的使用方法,註冊Travis,而後連接本身的Github,選擇要進行持續集成的項目。此時會顯示build failing,那是由於尚未在項目中進行相關配置。

首先須要使用Grunt等工具配置好測試框架的自動化測試,細節能夠參考我以前的文章改進個人Workflow。而後在 package.json 中添加一下代碼來指定執行的腳本:

"scripts": {
    "test": "grunt jasmine:test"
}

接着添加一個文件 .travis.yml 來配置travis:

language: node_js
node_js: 
  - "0.8"
before_script:
  - npm install -g grunt-cli

language 是集成測試所使用的語言,這裏前端開發固然是使用Nodejs,在 node_js 中指定版本便可。固然Travis還支持其餘多種語言,以及後端數據庫等。

before_script 則是在測試前執行的腳本程序,這裏在全局安裝Grunt-cli便可,由於默認的Travis會執行 npm install 將package.json中指定的Node包安裝到項目。

最後在Github中還須要在項目的Setting中的Service Hooks中配置Travis,輸入Token並保存。或者直接在Travis中點擊該項目條目中的扳手圖標進入Github,會自動配置好。

另外,若是在Github上爲README文件添加一行

[![Build Status](https://travis-ci.org/tychio/dialog.png?branch=master)](https://travis-ci.org/tychio/dialog)

就能夠持續直觀的顯示其測試結果。

博客原文:http://www.tychio.net/tech/2013/07/10/unit-test.html

相關文章
相關標籤/搜索