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

要如何設置 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 <YOUR_GAIA_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 後,就是使用者看到的通過或失敗的測試通知囉!
既然 node.js 也是 javascript 的執行環境,為什麼不直接在 node.js 裡面執行單元測試呢?主要的原因是 Firefox Nightly 是一個接近 Firefox OS 的運行環境,也有些 API 在 Firefox nightly 才可以使用。所以在 Firefox nightly 裡面跑是比較合理的方式。
撰寫新的單元測試
看完了如何執行單元測試,那如果加了新功能要加入單元測試要怎麼作呢?正巧最近修改了 Gaia 的 Calendar app,為其加入 offline 的錯誤訊息的 Bug 809537 就需要為離線功能加入單元測試。在這個 bug 裡面我新增了兩個需要測試的部份:
- 新的 Errors View,用來顯示主頁的錯誤訊息
- 在 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 物件內,如下圖所示
原本的 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 物件進去來達到單元測試的效果。而且可別小看這些單元測試,當你的程式日益複雜的時候,這些單元測試就是保證軟體品質的第一道防線,也會讓開發的時候感到更加安心喔。


