Travis CI 與自動化 JavaScript 單元測試

此篇文章最近更新時間為2013-04-08 06:54:02 目前共有9篇留言

關於作者 - JosephJ

任職於 Faria。喜好戶外運動、2008 年 5 月完成「跑步環島」。對於新技術跟程式碼有著強烈的偏執狂。

單元測試在軟體開發流程中是很重要的一環,它能協助開發、也能避免後續改版而造成的錯誤。但網站開發者通常不太注重單元測試這個議題,這或許是因網站開發有很大的彈性:有 Bug 可以隨時修正、也不需使用者重新安裝,或許會認為導入單元測試沒太大必要。若講到 JavaScript 單元測試、更因其執行環境複雜無比、自動化門檻超高,過去寫 JavaScript Test Case 的人更是少之又少。

這兩年網站技術日新月異,尤其是 JavaScript 越來越複雜龐大,其實更需要單元測試來確保品質。此外上面提到的門檻,也在這兩年間迅速弭平了,藉由 PhantomJS、Travis CI 等工具… 寫 JavaScript 單元測試變得輕鬆許多。希望能用這篇文章帶領新手一同進入單元測試的世界,一起提昇 JavaScript 的品質!

基礎介紹

擔心部分新手沒接觸過這個議題,在此先簡單介紹「CI」、「單元測試」這兩個主題。若有經驗者可跳過此章節。

單元測試 - Unit Test

我們常寫一些工具類的 JavaScript Function、甚至包裝成元件、以方便未來重複使用。單元測試 (Unit Test) 是另外一份程式碼、用來檢查這些程式碼的正確性。例如過去我們得替自己撰寫 trim 的方法、以達成去除頭尾的多餘空白,你可以替它寫以下的 Unit Test 做檢查。

// 以下的程式碼得搭配一些工具才能執行,這邊僅用來示意。
TestCases = {
    "應該要移除左側的空白": function () {
        var value = "     Awoo~", // 輸入值
            expected = "Awoo~";  // 預期得到的結果          
        Assert.areEqual(expected, trim(value)); // 經過 trim 後兩者必須相同
    },
    "應該要移除右側的空白": function () {
        var value = "Awoo~    ", // 輸入值
            expected = "Awoo~"; // 預期得到的結果           
        Assert.areEqual(expected, trim(value)); // 經過 trim 後兩者必須相同
    }
};

單元測試一句話解釋就是:「檢查 Input(參數) 是否會得到預期的 Output(回傳) 」。不管程式碼是如何實作的,只管最後的結果是否正確。在軟體開發流程中,單元測試相當於規格書、需要在實際的程式碼撰寫前就準備好。好處是什麼?對於開發者來說,省下了手動刻測試介面的時間,只需專注於程式的開發、重複地跑單元測試來做 Input/Output 驗證、直到通過所有的 Test Case。此即為 Test-driven Development (測試驅動開發模式)。

什麼是 CI?

CI 的全名是 Continous Integration,中文是「持續整合」。軟體開發常見的問題就是不同的開發人員不斷地增加 Code,但卻缺乏規範及完整測試,所以經過一段時間的累積程式往往變的難以辨認及維護、其他人也沒辦法接手;或者由於規模變大必須要對程式做翻新以因應需求。所以我們最常有的想法就是「打掉重練」,常常因此這樣浪費了許多的時間。

CI 的主要思路就是將程式碼拆解成小且易維護的片段,藉由每天開發人員的 Commit,CI 系統會持續自動抓取一份來做專案建置 (Build),並且去跑各式各樣的測試與檢查、產出報告寄送給相關人。以確保程式碼的品質、也方便未來持續成長。

市面上常見的 CI 系統有 HudsonJenkins 這兩套軟體,你可以建置在公司內部的伺服器上。若你的專案是 Open Source,則可以使用我們今天要講的 Travis CI:一個別人已經建置好、可直接使用、免費的 CI 系統。

單元測試 + CI = 自動化

其實就是讓 CI 系統幫忙自動地做單元測試。這樣一來不管開發者是否有乖乖使用 Unit Test 來檢驗正確性,系統都會忠實地幫你檢查,若有測試失敗則發信告訴相關人。

JavaScript 做自動化測試的困難點、如何解決?

「讓 CI 自動做單元測試」聽起來很簡單,但 JavaScript (Node.js 除外) 跟其他程式語言不太一樣的地方在於:開發常會牽涉到瀏覽器、得透過 DOM API 做事件或節點的存取。若要執行我們寫好的單元測試,勢必得打開瀏覽器才能做 (看看傳統的 JavaScript Unit Test)。 這讓我們想利用 CI 達成自動單元測試、增加非常高的難度。CI 系統得要很聰明:得自動打開瀏覽器、能執行你的單元測試、還要能得存取測試結果。要串起整個流程有太多事情要做,這也是過去 JavaScript 單元測試先前非常不普及的主因。

不過,軟體開發就是不斷地累積,3 年前做不到的事情,今天已經可以用多個工具串接解決!

PhantomJS

PhantomJS Logo

對瀏覽器,我們原本的認知都是有個視窗介面,讓人去看並操作,。而 PhantomJS 徹底顛覆了這樣的思維:讓開發者可以開啟一個看不見的 Webkit 瀏覽器、並用它設計好的 API 介面 去載入任何 URL、獲得資料。通常會用於網頁效能的檢測(ex. 取得大量 HAR 檔做分析)、以及我們目前在討論的自動測試。

Grover

雖然 PhantomJS 能夠載入我們的測試網頁,但它並不知道該如何執行 Test Cases、也不知道執行後到底是成功還是失敗。要自己寫嗎?這樣實在太累人了 :p

我所使用的 Unit Test 工具是 YUI Test (非 YUI 的 JavaScript 專案也可用),它可搭配一個叫 Grover 的 Node.js 命令行工具使用。他是YUI Test 與 PhantomJS 之間的橋樑 ,可以替你執行並取得結果!用法如下:

grover <Unit Test 路徑> --server 

Grover 會以當前所在目錄為 DocumentRoot、在本機啟動一個簡單的 HTTP Server、載入你的 Unit Test 所在網頁、最後取得測試的結果。

See? 本來得用瀏覽器執行的 Unit Test,現在已經完全可在命令行中跑完啦!

Travis CI

因為我們已經克服了主要問題:命令行即可跑 Unit Test。接下來要達成自動化,其實不管是哪一套 CI 工具都可以達成。但要找台 Server、設定一堆東西把 Hudson 或 Jenkins 跑起來仍然還是個苦差事 :p

Travis Logo

Travis 是個提供給 Open Source 社群使用的免費 CI 服務,他很慷慨地捐出頻寬、CPU 運算,讓所有的 Open Source 專案都能無痛使用 CI、大幅減少設置時間,真是佛心來著!當然唯一的限制就是你的專案得是公開的,不然有商業機密的程式碼交給 Travis… 後果自負啊 XD

開始實作

困難點都已經克服了,剩下的就是實作的細節。

第 1 步:撰寫 Unit Test

其實 Unit Test 的撰寫並不容易,尤其像 JavaScript 又會牽扯到許多使用者行為、非同步、與資料交換...。對於想入門的人可以參考 Nicholas Zakas 所撰寫的 Writing Effective JavaScript Unit Tests with YUI Test,它雖然掛了 YUI Test 的字樣,但實際上卻有許多非常值得參考的 Unit Test 心法。

這邊的例子是:同事小莊開發了一個 YouTube Iframe 的元件,主要目的是讓 YouTube Iframe Player API 跟我們的其它 Player 有一致的 API 介面。而負責 Reviewer 的我,就撰寫了 17 條 Test Case 來驗證這個元件的正確性 (緣起)。

JavaScript Unit Test

能用 CLI 命令行執行是自動化成功的關鍵,幾行指令就可以輕鬆達陣(系統必須有 Git 與 Node.js):

# 安裝 PhantomJS
sudo npm install phantomjs -g

# 安裝 Grover
sudo npm install grover -g

# 下載 YouTube Iframe 元件
git clone git://github.com/josephj/youtube-iframe.git
cd youtube-iframe

# 執行 Unit Test
grover tests/unit/index.html --server

是不是有感覺得多?但目前為止都還是手動測試,接下來我們要讓 Travis CI 替我們自動執行!

第 2 步 - 登入

請先連至 https://travis-ci.org,點選 Travis CI 網站右上角的 "Sign in with Github",它會將你導到 GitHub Auth 頁,允許一些權限後它會重新將你導回 Travis 首頁。你會看到目前正在進行 Build 的專案。

第 3 步 - 啟用 GitHub Service Hook

大多數的版本控制系統都有 Hook 的機制,讓開發者可以在這個時間點去用程式去做一些事情。像 miiiCasa 就是在 Commit 前 (pre-commit) 去執行 PHPCS 與 JSLint 確保開發者都有按照規範來寫 Code,不改好就不給 Commit。而 Travis 則是利用 post-receive (每次 Push 後) Hook 去驅動自動建置的流程(抓取你的程式碼、Build、測試)

你需要手動去啟用這個 Hook(一次性),請點選右上角你的大頭貼,接著就會列出你所有可讀寫的 Repos:

針對你想自動測試的專案(以小莊的 youtube-iframe 為例),點一下設定為 On:

如此一來,在你每次執行 git push 到 GitHub 的這個 Repo 中時,就會自動觸發 Travis CI 來做事情了!

第 4 步:增加 .travis.yml 檔

.travis.yml 是 Travis CI 設定檔,你需要透過此檔告訴 Travis CI 這是何種程式語言的專案、Build 前後該做哪些事情等,格式如下:

language: node_js
node_js:
    - "0.8"
branches:
    only:
        - master
notifications:
    email:
        - <通知人 Email 1>
        - <通知人 Email 2>

以下是各個欄位的定義:

  • language: 指定專案是為何種程式語言,你一定會覺得很奇怪,為什麼這邊要指定 Node.js 呢?小莊的 YouTube Iframe 跟 Node.js 一點關係也沒有啊。這其實是因為 Travis CI 的支援程式語言的列表上,它的 JavaScript 是指 Node.js、而不是指所有的 JavaScript 都可以使用… 但請放心地指定 node_js,*只要可以用來驅動測試的流程不就好啦*?
  • node_js: Node.js 執行環境的版本編號,可指定多個。對我們來說其實也沒直接關係,就指定比較穩定的 0.8 吧!
  • branch: 哪些 git branch 有修改時需要被 Travis 處理?這邊只處理 master 的。
  • notification: 執行完畢的報告要寄給哪些 Email?
  • 更多屬性請參考 Build a Node.js Project

第 5 步:增加 package.json 檔

Node.js 的 Build 流程是透過一個叫 NPM (Node.js Package Management) 的東東。而前面一步我們指定專案為 node_js,是因為 NPM 也包含了測試的部分,只要在專案下執行這個指令,它就會去幫你做測試:

npm test

它怎麼知道你要去做什麼測試呢?其實 NPM 會讀取專案根目錄的 package.json 檔案,Node.js 開發者最常用的就是指定此專案的 Dependencies,以及實際測試會執行到的指令:

{
    "name": "youtube-iframe",
    "devDependencies": {
        "grover": "*"
    },
    "scripts": {
        "test": "grover ./tests/unit/index.html --server"
    }
}
  • devDependencies : 由於我們會需要 grover 以進行測試,請在這邊指定,會在 Build 時自動安裝此套件。(PhantomJS 是內建的,不需特別安裝)
  • scripts > test : 需要指定我們前面利用 grover 在命令行跑 Unit Test 的指令。

Travis CI 在得知是 Node.js 專案後,自然就會使用 NPM 進行建置 (npm install .) 與測試 (npm test)。

第 6 步:執行看看

設置到此已經全部完成,你接著可以用 git push 把修改過後的程式推到 GitHub 上,它會自動觸發 Travis CI 去建置與測試你的專案(不會立刻做,有時得排隊等個一兩分鐘)。做完後它會寄通知信到你 .travis.yml 所設定的信箱:

  • 錯誤訊息:
    錯誤訊息
  • 成功訊息:
    成功訊息

能夠看到後面綠色的訊息,表示你的修正並沒有影響到任何 Test Case。

結語

單元測試一開始寫起來是很辛苦的一件事,但它會對長遠的品質有很大的幫助。同事 Hawk 講過,老闆可能會問程式設計師改完後會不會出包,通常我們的把握程度可能不到八成。但如果已經具備了充足的單元測試 Case、跑過也沒問題,你就真的有十足的把握跟老闆講沒問題(這樣感覺好有 Guts!)

這邊文章是自動測試的一個入門,其實 Travis CI 也支援了 Chrome、Opera、Firefox 等瀏覽器,我們更能整合 Selenium 做 Functional Testing。測試整個網站的功能更是我未來希望努力的目標。

工程師的使命就是在自動化,讓機械去取代昂貴又沒效率的人工,我們才能有更多的時間投入在有意義的事情上。



Comments

  1. josephj 2015-06-28 21:15:41
    max 你好,

    測試內容這邊是用 Node.js 去跑,所以是指定在 package.json 即可
    "test": "grover ./tests/unit/index.html --server"

    grover 是 Test Runner
    ./tests/unit/index.html 是所有 Test Cases 測試內容

    跟資料庫有關係的話,通常就是在 Travis 掛載測試資料庫,可參考:
    http://docs.travis-ci.com/user/database-setup/

    我猜你是要做 Model 的 Unit Test,那測試的 Test Case 的 setup() 就是要塞好基本資料到 Travis 上的 DB instance、teardown() 則清除,確保你每一個 Test Case 中不會產生 dependencies

    對於 Travis,其實把它想成另一個跑測試的環境即可,基本上你還是要先在自己的開發環境跑測試成功、再利用以上教學讓 Travis 幫你自動化。所需要的軟體應該由 Travis 替你在跑測試前安裝完畢,若牽涉到外界的 hooks 測試環境就太複雜了。
  2. max 2015-06-28 15:51:40
    您好,想請教
    不太懂的地方是

    單元測試這邊還懂,就是攥寫測試 input/ output 的結果是不是正確,透過一些指令工具去實驗單元驗證。

    但在 travis ci 中,他只是單純設定語言與相關版本與需要測試的 branch
    怎麼會知道,要測試的東西內容?
    如果今天跟資料庫有關係,要怎麼驗證?

    感覺似乎可以直接用 hook 去掛載自己的單元測試,但您沒寫到這一步,所以也想請教
  3. 颜海镜 2014-06-05 10:15:06
    赞一个
  4. Ko-Chih Wu 2014-04-21 16:09:33
    PhantomJS除了不需GUI之外,在效率上不知有沒有比chromedriver好? 我之前在server上測試是用chrome headless的方式來跑
  5. 一條小龍 2014-03-20 18:31:52
    寫得很仔細,對我很有幫助,給各讚。
    要做 unit test 尤其是一個團隊的,如何獲得各方支持,有時反而比學習使用工具還複雜...
  6. ocean 2014-02-26 10:11:15
    謝謝分享!
  7. Amo 2013-12-12 18:55:45
    感謝分享 很受用的文章
  8. Amo 2013-05-31 23:25:53
    很棒的文章 感謝分享:-)
  9. a f2e 2013-04-11 14:19:35
    很受益的分享! Thanks sharing.

    一些補充個人想法:
    當unittest的測試code覆蓋率不是100%,
    或說 寫unittest的code的人 對於某業務或某feature漏寫了,
    那當下的「你就真的有十足的把握跟老闆講沒問題」就不成立了XD(或 那個十足≠100%)。
    這時 加入review code流程就是多一層保險。
    再 加入人工QA點擊目視測試 也是多一個保險。

    所以單以「要100%保證每人每次改的code不會出包」,可能還需加入其它條件來使它成立或接近成立~
暱稱: 必填。
Email: 非必填。若填寫為不公開欄位,僅供站長參考聯繫。
內容: 必填。限 255 個字元以內。
驗證碼:
送出

Facebook Comment