缺它不可!靈活運用 Firefox OS Gaia 的單元測試

單元測試一直以來都是確保軟體品質的一種方式,在日益錯綜複雜的軟體中更是重要。Firefox OS 的應用層 Gaia 理所當然的也由單元測試來確保軟體品質。設置妥當後,當你打開任意編輯器對 Javascript 檔案編輯,並且按下『Save』的那一刻,unit test agent 就會默默的被喚起,針對您的修改一項項的進行檢測。當所有測試項目都妥當的通過之後,您將會看到一個優雅的圖示跳出來,告知你的測試均已通過。

缺它不可!靈活運用 Firefox OS Gaia 的單元測試

Gaia 的單元測試將會針對修改的部份執行該部份的單元測試,可確保修改的時候所有的測項都可以通過。錯誤的時候當然也會跳出個讓你很難忽視的紅色圖示,提醒你本次修改沒有通過測試。

缺它不可!靈活運用 Firefox OS Gaia 的單元測試

要如何設置 Gaia 的單元測試環境呢?首先你會需要以下環境:

  1. node.js
  2. Firefox nightly
  3. Gaia 開發環境

因為本文重點是單元測試環境,上面環境的安裝方法就不再贅述。準備好上面的環境後,切換到 gaia 目錄,鍵入以下的指令即可執行 test agent server

$ make test-agent-server

接下來則需要建立 gaia 除錯環境,請開另外一個終端機視窗鍵入下面指令(本步驟將會建立 debug 版本的 gaia profile)

$ DEBUG=1 make

最後把 firefox nightly 套用由 DEBUG=1 make 生成的 profile 檔即可執行並完成設定

$ firefox-nightly -profile http://test-agent.gaiamobile.org:8080/

注意!如果你在 Mac OS 上面開發,profile 目錄必須使用絕對路徑。

這樣就設定完成了,你可以打開已經有撰寫單元測試的 javascript 檔案如 gaia/apps/calendar/js/app.js,修改之後儲存,就會看到相關的單元測試開始執行了!

如何運作?

其實運作原理很簡單。 gaia 利用 node.js 來監控檔案系統的變化,並且利用 websocket 通知 nightly browser 要執行哪個單元測試檔案,nightly 執行完畢後,再把結果回傳給 node.js。最後再由 node.js 發出 notification 後,就是使用者看到的通過或失敗的測試通知囉!

缺它不可!靈活運用 Firefox OS Gaia 的單元測試

既然 node.js 也是 javascript 的執行環境,為什麼不直接在 node.js 裡面執行單元測試呢?主要的原因是 Firefox Nightly 是一個接近 Firefox OS 的運行環境,也有些 API 在 Firefox nightly 才可以使用。所以在 Firefox nightly 裡面跑是比較合理的方式。

撰寫新的單元測試

看完了如何執行單元測試,那如果加了新功能要加入單元測試要怎麼作呢?正巧最近修改了 Gaia 的 Calendar app,為其加入 offline 的錯誤訊息的 Bug 809537 就需要為離線功能加入單元測試。在這個 bug 裡面我新增了兩個需要測試的部份:

  1. 新的 Errors View,用來顯示主頁的錯誤訊息
  2. caldav provider 裡面的 getAccount, findCalendars, syncEvents, createEvent, updateEvent, deleteEvent 新增偵測離線狀態的功能,當離線時 callback 會帶一個錯誤訊息。

Errors View 繼承自 View (js/view.js),其中最主要的功能是對處理來自 syncController 的  ‘offline’ event,當 handleEvent 收到 ‘offline’ 之後,會採用繼承自 View 的 showErrors 來顯示錯誤訊息,摘要重要的源碼如下:

Calendar.ns('Views').Errors = (function() {

function Errors() { Calendar.View.apply(this, arguments); this.app.syncController.on('offline', this); }

Errors.prototype = { __proto__: Calendar.View.prototype,

(ignore...)

handleEvent: function(event) { switch (event.type) { case 'offline': this.showErrors([{name: 'offline'}]); break; } } }; (ignore...) }());

針對 caldav provider 的修改,則是新增了 bailWhenOffline 在 offline 的時候新增一個 Error 並且作為 callback 的參數傳回。比如說下面的 findCalendars 就會先確認如果目前是 offline 就不會 request service。

findCalendars: function(account, callback) { if (this.bailWhenOffline(callback)) { return; } this.service.request('caldav', 'findCalendars', account, callback); },

(ignore...)

bailWhenOffline: function(callback) { if (!this.offlineMessage && 'mozL10n' in window.navigator) { this.offlineMessage = window.navigator.mozL10n.get('error-offline'); }

var ret = this.app.offline() && callback; if (ret) { var error = new Error(); error.name = 'offline'; error.message = this.offlineMessage; callback(error); } return ret; }

Erros View 裡面,最需要測試的是 handleEvent 到 showErrors 是否正確的傳入了 { name: “offline” },而 showErrors 已經在 view_test.js 裡面測試過了不需要重複測試,所以測試的方法是作一個 Mock 的 showErrors function 塞入原本的 Errors View 物件內,如下圖所示

缺它不可!靈活運用 Firefox OS Gaia 的單元測試

原本的 showErrors 會在手機上顯示錯誤訊息,但我們用 Mock 的 showErrors 之後就只會把傳入的 error name 直接 assign 到 errorName 裡面,如此一來我們對 syncController 發出 “offline” 事件,再確認 errorName 最後是不是拿到了 “offline” 來判斷 handleEvent 是否正常的運行。

requireApp('calendar/test/unit/helper.js', function() { requireLib('views/errors.js'); });

suite('views/errors', function() { var subject, app, errorName;

setup(function() { app = testSupport.calendar.app(); subject = new Calendar.Views.Errors({ app: app });

subject.showErrors = function(list) { errorName = list[0].name; } });

test('offline event', function() { subject.app.syncController.emit('offline'); assert.deepEqual(errorName, 'offline'); }); });

至於 offline 訊息基本上是從 navigator.onLine 這個屬性得知的,所以在撰寫程式的時候我們把偵測 offline 的功能封裝在 app:offline() 裡面:

/** * Returns the offline status. */ offline: function() { return (navigator && 'onLine' in navigator) ? !navigator.onLine : true; }

在執行單元測試的時候動態的把原本的 offline 儲存到 realOffline,接著塞一個每次都會回傳 true 的 offline(),如此一來就可以測試離線狀況時上述的 function 如 getAccount, findCalendar 會不會在 callback 裡面帶入 offline error 的錯誤,舉個測試 getAccount 離線狀況的例子:

test('offline handling', function(done) { var realOffline = app.offline; app.offline = function() { return true }; subject.getAccount(input, function cb(cbError, cbResult) { done(function() { app.offline = realOffline; assert.equal(cbError.name, 'offline'); }) }) });

我們將 Mock 的 offline function 替換進去,並且執行 getAcount,並且確定 cbError.name 是 offline 就可以確認我們加入的功能是可以正常運作的了。

感謝單元測試以及 Javascript 的動態特性!

我們可以在測試的時候非常容易的替換一些 Mock 物件進去來達到單元測試的效果。而且可別小看這些單元測試,當你的程式日益複雜的時候,這些單元測試就是保證軟體品質的第一道防線,也會讓開發的時候感到更加安心喔。