缺它不可!靈活運用 Firefox OS Gaia 的單元測試
單元測試一直以來都是確保軟體品質的一種方式,在日益錯綜複雜的軟體中更是重要。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
注意!如果你在 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 物件進去來達到單元測試的效果。而且可別小看這些單元測試,當你的程式日益複雜的時候,這些單元測試就是保證軟體品質的第一道防線,也會讓開發的時候感到更加安心喔。