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

Tychio發表於2013-10-09

單元測試Unit Test

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

什麼是單元測試?

先來看看單元測試的定義,在維基百科英文版中可以找到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.

重點在於最後,單元測試的目的顯而易見,用來確定是否適合使用。而測試的方法則包括控制資料,使用和操作過程。那麼以我的理解,每個單元測試就是一段用於測試一個模組或介面是否能達到預期結果的程式碼。開發人員需要使用程式碼來定義一個可用的衡量標準,並且可以快速檢驗。

很快我發現有一個誤區,許多人認為單元測試必須是一個runner集中執行所有單元的測試,並一目瞭然。不,這僅僅是一種自動化單元測試的最佳實踐,在一些小型專案中單元測試可能僅僅是一組去除其他特性的介面呼叫。甚至在一些圖形處理或佈局的專案中單元測試可以結合自身特性變的十分有趣,比如Masonry,一個網格佈局庫,在它的單元測試中不是一個紅或綠的條目,而是一行一行的小格佈局用以說明佈局被完成的事實,這樣比程式碼檢查佈局是否正確再以顏色顯示結果來得更直觀高效,也避免了測試程式本身的bug導致的失誤。

打個比方,單元測試就像一把尺子,當測量的物件是一個曲面時,也許可以花費大力氣去將它抽象成平面,但我更提倡量身定做一把彎曲的尺子去適應這個曲面。無論怎樣,單元測試是為了生產程式碼而寫,它應當足夠的自由奔放,去適應各種各樣的生產程式碼。

為什麼要單元測試?

也許定義中已經很清楚的指明瞭其意義,確認某段程式碼或模組或介面是否適合使用,但我想會有更多的人認為,直接在測試環境中使用軟體可以更加確保軟體是否可用。不,在實際使用過程中會伴隨著一大批的附帶操作大量增加測試時間,並且無法保證其測試覆蓋率。所以我認為單元測試的目的並不僅僅是確認是否可用,而是更高效更穩定的確認其是否可用。

隨著專案規模的增加,函式、方法、變數都在遞增,尤其是進度的不足,來自產品經理的壓力,還有QA所帶來的各種Bug報告會讓原本整潔的程式碼變得一片混亂。我甚至見過同一個介面以不同的名稱出現在8個不同的控制器中。這時也許我們首先想到的是重構,可是等等,在重構結束時我們如何確定專案僅僅是被重構了,而不是被改寫了?此時單元測試將是一根救命稻草,它是一個衡量標準,告訴開發人員這麼做是否將改變結果。

不僅僅是這樣。許多人認為單元測試,甚至整個測試都是在編碼結束後的一道工序,而修復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

相關文章