Visual Studio Apache Cordova Apps 單元測試二部曲
這篇文章中您可以閱讀到以下資訊:
這系列文章的第一篇使用 Visual Studio 撰寫 Apache Cordova app,透過一個基本範例說明了單元測試的本質。在這篇文章中,我們將以測試驅動開發 (test-driven development) 的過程,針對範例程式進行許多改善,這會有助於幫助你解析思考如何挑戰一個單元程式碼從失敗到成功的過程。我們也會討論一點點關於單元測試偵錯。
同樣的這二個部份都是我們最近新增到 Visual Studio Tools for Apache Cordova 文件, Author & run tests 章節中的精簡版本。
測試驅動開發 Test-driven development
很明顯的它是一個空的測試 (過於簡化),normalizeData 的實作是遠遠不足的。一個健全的 normalizeData 需要處理各種會引發像是 JSON.parse 函式或屬性失敗的不良 JSON 。當然我們需要測試所有我們能想的到各種變化資料。
問題是,從哪裡開始?撰寫程式碼,撰寫測試,或是來回二者之間?你可能做了很多後者,在 normalizeData 裡寫了一些很好的程式碼,然後寫了一些測試,測驗失敗情況下,然後寫更多的程式碼,寫更多那些你還沒有涵蓋到的測試,再次修改程式碼等等….。儘管這是可控制的,它促使你來回思考程式碼及資料,並試著讓程式碼在正常運作以及想辦法讓它失敗間來回轉變。這確實是二種不同的思考過程。
這是測試驅動開發方式所顯示出的價值。充分的執行任何一段單元程式碼,最終你必須考慮所有必要輸入值的變化。測試驅動開發進行這種前置思考,是非常有效的,因為一旦你想到一些測試案例,你可以很快的連想到其它的。例如,如果你想到 JSON 裡包含一個物件,你會很自然的認為包括陣列。假如你想到包括一個整數,而單元程式碼可能預期是一個字串,它自然也就預期是一個整數的字串。
在 normalizeData 函式,我發現它可能是花了15分鐘思考一套完整的不同輸入值測試,來驗證 JSON.parse ,驗證 JSON 結構假設,以及驗證有關資料型別的假設。這裡有份清單:
'{"Name": "Maria", "PersonalIdentifier": 2111858}'
null
''
'blahblahblah'
'{{}'
'{{[]}}}'
'document.location="malware.site.com"'
'drop database users'
'{"Name": "Maria"}'
'{"PersonalIdentifier": 2111858}'
'{}'
'{"name": "Maria", "personalIdentifier": 2111858}'
'{"nm": "Maria", "pid": 2111858}'
'{"Name": "Maria", "PersonalIdentifier": 2111858, "Other1": 123, "Other2": "foobar"}'
'{"Name": "Maria", "PersonalIdentifier": "2111858"}'
'{"Name": "Maria", "PersonalIdentifier": -1}'
'{"Name": "Maria", "PersonalIdentifier": 123456789123456789123456789123456789}'
'{"Name": , "PersonalIdentifier": 2111858}'
'{"Name": 12345, "PersonalIdentifier": 2111858}'
'{"Name": {"First": "Maria"}, "PersonalIdentifier": 2111858}'
'{"Name": "Maria", "PersonalIdentifier": {"id": 2111858}}'
'{"Name": {"First": "Maria"}, "PersonalIdentifier": {"id": 2111858}}'
'{"Name": ["Maria"], "PersonalIdentifier": 2111858}'
'{"Name": "Maria", "PersonalIdentifier": [2111858]}'
'{"Name": ["Maria"], "PersonalIdentifier": [2111858]}'
'{"Name": "Maria", "PersonalIdentifier": "002111858"}'
'{"Name": "Maria", "PersonalIdentifier": 002111858}'
同樣的,一旦你開始思索關於輸入值的變化,測試案例自然會一個一個被引導出來。然後一旦你制定完成 JSON 變化形成一個單元測試,本實上你已經建立一個可重覆使用於其它任何需要用到 JSON 的專案測試資源了。
隨著手邊的輸入值清單,接著現在是件簡單的事,在每一組輸入的必要測試結構上,重覆的複製及貼上,例如:
it('accepts golden path data', function () {
var json = '{"Name": "Maria", "PersonalIdentifier": 2111858}';
var norm = normalizeData(json);
expect(norm.name).toEqual("Maria");
expect(norm.id).toEqual(2111858);
});
it ('rejects non-JSON string', function () {
var json = 'blahblahblah';
var norm = normalizeData(json);
expect(norm).toEqual(null);
});
it('accepts PersonalIdentifier only, name defaults', function () {
var json = '{"PersonalIdentifier": 2111858}';
var norm = normalizeData(json);
expect(norm.name).toEqual("default"); //Default
expect(norm.id).toEqual(2111858);
});
it('ignores extra fields', function () {
var json = '{"Name": "Maria", "PersonalIdentifier": 2111858, "Other1": 123, "Other2": "foobar"}';
var norm = normalizeData(json);
expect(norm.name).toEqual("Maria");
expect(norm.id).toEqual(2111858);
});
it('truncates excessively long Name', function () {
//Create a string longer than 255 characters
var name = "";
for (var i = 0; i < 30; i++) {
name += "aaaaaaaaaa" + i;
}
var json = '{"Name": "' + name + '", "PersonalIdentifier": 2111858}';
var norm = normalizeData(json);
equal(norm.Name).toEqual(name.substring(0, 255));
equal(norm.Name.length).toEqual(255);
expect(norm.id).toEqual(2111858);
});
it('rejects object Name and PersonalIdentifier', function () {
var json = '{"Name": {"First": "Maria"}, "PersonalIdentifier": {"id": 2111858}}';
var norm = normalizeData(json);
expect(norm).toEqual(null);
});
注意測試的名稱(it函式的第一個參數),它會呈現在 UI 裡,例如測試總管,所以它應該要能辨示出是測試什麼以及測試的基本性質 (例如, “rejects”或“accepts”) 。此外,當然它可以把輸入及預期結果放到一個集合陣列,來取代逐一撰寫每個測試。目前在這裡所呈現的樣子。
這裡的重點是,首先花 30 分鐘專注於進行輸入值的變化,然後裝載到單元測試裡,你是完全無拘束的專注在撰寫程式碼,不需要去懷疑(或擔心!)是否已經真的處理了所有可能的輸入。
事實上,如果你針對像 normalizeData 這樣尚未實作的函式,進行全部的測試,很明顯大部份的結果都會是失敗的。但是這只是表示那些結果失敗的測試是你必須在程式碼裡的待處理事項,反應出實際上還沒有被妥善處理的輸入情況。增加更多的實作程式碼,然後通過更多的測試。當這個函式可以通過所有的測試時,你就可以完全信任它可以處理所有應該被處理的情況。針對整個完整的示範演練,包含完成 normalizeData 實作,請參閱 Improving the unit tests: an introduction to test-driven development 文件。
測試驅動開發 (Test-driven development) ,簡單來說,就是明確的針對這些輸入的情況,分別撰寫程式碼來處理它們。最後,如果你真的想做好你的程式碼測試工作,你就必須拆解這些任務。在這個前題之下,測試驅動開發 (Test-driven development) 可以在開發初期階段就建立更強壯健全的程式碼,整體來看是可以降低成本的。
偵錯測試及執行期變數值
先前的單元測試揭漏了一個 bug 。你有看出是哪一個嗎?我以為已經寫完了 normalizeData 函式的實作,包含處理"截斷過長的名稱 (truncates excessively long Name)"的測試案例,但這個案例還是失敗了,我無法立即看出問題所在。
幸運的是, Visual Studio 提供你可以單元測試偵錯的能力,就像其它類型的程式碼一樣,設置中斷點,檢查變數值,然後逐步遍歷測試程式碼。測試程式碼仍然還是程式碼,只是出現 bugs !
無論如何,在 Visual Studio 編輯器設置中斷點是不夠的。透過測試總管執行測試無法掛起那些中斷點,因為像 Chutzpah 這類的測試執行器 (test runner) 是在獨立的 PhantomJS 程序中去載入 JavaScript 並執行的,但是 Visual Studio IDE 沒有辦法關聯這些引擎進行偵錯。
你必須進入 "測試>偵錯>執行 " ,在這裡你可以看有 "選取的測試 " 及 "所有測試 " 選項。你也可以在失敗的測試上按右鍵,然後選擇偵錯所選擇的測試。這些命令可以驅使測試執行器 (test runner) 在瀏覽器中執行 JavaScrip t程式碼, Visual Studio 就可以捕抓到偵錯器。測試框架 (test framework) 的報告也會顯示在瀏覽器。
提醒!在"]測試>偵錯>所有測試 "以及"測試總管>全部執行 "命令,會自動儲存你在專案裡所做的任何程式碼變更。但是,執行個別測試的命令,是不會儲存變更的,這可能會造成預期應改變卻沒有生效的困惑。所以執行個別測試前先確認已經儲存程式碼變更。
在偵錯器 (debugger),我看到我的單元測試錯誤引用了norm.Name 而不是 norm.name :
修正這個問題之後,我剩一個測試還未通過,它是測試在 JSON 裡帶有 0 開頭的整數值。我不知道為何它是失敗的,因此我啟用偵錯器 (debugger) 對它進行偵錯。但是在偵錯器 (debugger) ,它通過了測試!這是怎麼一回事?
原來,我發現到 JSON.parse在PhantomJS 與 Internet Explorer 的執行環境,在實作上有些微不同,在偵錯器 (debugger) 以外的 PhantomJS 執行環境運作時針對 0 開頭的會引發例外,而在偵錯器 (debugger) 內部採用 Internet Explorer 執行環境進行則不會引發例外。詳細原因請參閱 Debugging unit tests 文件。
最重要值得注意的是,你也許會發現在單元測試所使用的環境與 App 真正執行所使用的環境,執行時有些許的差異。正因為如此,所以好的建議是偶爾在所有行動平台上執行你的單元測試。要做到這一點,在你的 App 開發版本加入一個特殊頁面,然後在瀏覽該頁面時執行你的單元測試。在這個情況下,瀏覽行為就像是個測試執行器 (test runner) ,你必須要引用參考測試框架 (test framework libraries) ,以便裝載該頁面。
尾聲
包含第一篇和第二篇的文章內容,都是我們發佈在文件裡 Author & run tests 章節內容的精簡版本,你可以在那裡找到更詳細的演練示範,以及談論有關使用 “mocks” 來處理呼叫平台 APIs ,還有其它延伸未談及到的單元測試執行環境內容。歡迎告訴我們你的想法。
我也希望聽聽你是如何在 Cordova apps 進行單元測試,你是如何進行 UI 測試(手動及自動),以及我們可以如何進一步提升我們的支援透過 Visual Studio Tools for Apache Cordova ,歡迎到 https://visualstudio.uservoice.com/. 提出建議。
本文翻譯自 Unit Testing Apache Cordova Apps with Visual Studio, Part 2
[延伸閱讀 - 影片教學]
使用 Visual Studio 2015 TACO 建置 Windows 10 應用程式
Visual Studio 2015 tools for Apache Cordova Build apps for Windows 10