目標
基於 ExtJS MVC的範例
- 建立手/自動測試
- 納入持續整合流程
套件
Jasmine 2.0 (standalone distribution)
jasmine-reporters
Jenkins
JSCover
PhantomJS
Note
由於 Jasmine 2.0 在 2013 年底才釋出,與 ExtJS 或是 jasmine-reporters 的整合有些細節並不廣為人知(比較難 google 到啦),這篇是以 Jasmine 2.0 為主。
Application Files
Root | Second | Third | … |
---|---|---|---|
MyApp | / | ||
+ | ext-4 | /<extjs_code> | |
+ | index.html | ||
+ | app.js | ||
+ | app | / | |
+ | data | / | |
+ | + | main.json | |
+ | + | updatemain.json | |
+ | unittest.js | ||
+ | unittest.html | ||
+ | unittest | / | |
+ | + | boot.js | |
+ | + | spec.js | |
+ | + | lib | / |
+ | + | + | <jasmine2.0.0 source> |
+ | + | + | <jasmine-reporter source> |
Unit-test 單元性測試
ExtJS
/unittest.js
Ext.require('Ext.app.Application');
Ext.Loader.setConfig({ enabled: true });
Ext.application({
name: 'Hello',
appFolder: 'app',
controllers: ['Main'],
launch: function() {
}
});
從 app.js 簡化而來;由於 Jasmine 2.0 將啟動的部分改到了 boot.js ,這裡不需要額外執行 jasmine。
Jasmine 2.0
/unittest.html
<html>
<head>
<title id="page-title">Hello Tester</title>
<link rel="stylesheet" type="text/css" href="unittest/lib/jasmine-2.0.0/jasmine.css">
<!-- Dynamic loading doesn't work with PhantomJS, use *-all-*.js -->
<script type="text/javascript" src="ext-4/ext-all-debug.js"></script>
<script type="text/javascript" src="unittest/lib/jasmine-2.0.0/jasmine.js"></script>
<script type="text/javascript" src="unittest/lib/jasmine-2.0.0/jasmine-html.js"></script>
<!-- 額外的 JUnit XML 報表格式,與 Jenkins 整合用 -->
<script type="text/javascript" src="reporter/src/jasmine.junit_reporter.js"></script>
<!-- 啟動 Jasmin -->
<script type="text/javascript" src="unittest/lib/jasmine-2.0.0/boot.js"></script>
<!-- include specs here -->
<script type="text/javascript" src="unittest/spec.js"></script>
<!-- test launcher -->
<script type="text/javascript" src="unittest.js"></script>
</head>
<body>
</body>
</html>
/unittest/spec.js
撰寫 testcase 的地方。
// test suite
describe('Ext Basics', function(){
// test case
it('Ext is loaded', function(){
expect(Ext).toBeDefined();
expect(Ext.getVersion().major).toEqual(4);
});
it('app is loaded', function(){
expect(Ext.ClassManager.get('Hello')).not.toBe(null);
});
it('Intended failure', function(){
expect(Ext.ClassManager.get('NotDefined')).not.toBe(null);
});
});
當然可以切成多個檔案撰寫,只要記得在 unittest.html 裡面引入就好。
/unittest/boot.js
Jasmine 2.0 套件裡有包含一個 boot.js,直接複製後修改
// 使用預設設定
var junitReporter = new jasmineReporters.JUnitXmlReporter({});
// 保留原本的 reporters
env.addReporter(jasmineInterface.jsApiReporter);
env.addReporter(htmlReporter);
// 新增我們初始化過的 junitReporter
env.addReporter(junitReporter);
到這時候就可以用一般瀏覽器測試了
由於我們保留了 HtmlReporter (Jasmine 內建),瀏覽器可以直接看到測試報告,讓我們得以在開發中隨時執行測試。
利用 PhantomJS 執行測試
PhantomJS 是一個基於 Webkit 的 headless (無頭!?) 瀏覽器,能執行網頁中的 JS 與使用者自定的 JS ,並提供 DOM 的操作,我們使用它來執行測試及儲存測試結果。同時 PhantomJS 也提供額外的模組讓 JS 操作,比如說 JUnitXmlReporter 就是使用這類功能將檔案儲存到瀏覽器所在的客戶端。
我們只使用到 PhamtomJS 命令列的功能來產生報表,這邊還需要一個額外的 phantomjs-testrunner.js 讓 PhantomJS 得以支援 JUnitXmlReporter,這個檔案可以在 jasmine-reporters 套件包裡找到,位於 jasmine-reporters/bin/phantomjs-testrunner.js
。
一切就緒後執行
phantomjs phantomjs-testrunner.js http://<my-domain>/unittest.html
第一個參數是 phantomjs 要執行的 script ,第二個參數通常是該 script 的參數。成功執行後會產生一個 junitxmlreport.xml 檔案。
Continuous Integration 持續整合
測試報表
之前見到的 JUnitXmlReporter 是屬於 jasmine-reporter 專案中的報表產生器,它藉由 PhantomJS, Rhino, 或 Node.js 輸出報表至檔案。可以由原始碼驗證這個論點。
// Rhino
try {
// turn filename into a qualified path
if (path) {
filename = getQualifiedFilename(java.lang.System.getProperty("file.separator"));
// create parent dir and ancestors if necessary
var file = java.io.File(filename);
var parentDir = file.getParentFile();
if (!parentDir.exists()) {
parentDir.mkdirs();
}
}
// finally write the file
var out = new java.io.BufferedWriter(new java.io.FileWriter(filename));
out.write(text);
out.close();
return;
} catch (e) {}
// PhantomJS, via a method injected by phantomjs-testrunner.js
try {
// turn filename into a qualified path
filename = getQualifiedFilename(window.fs_path_separator);
__phantom_writeFile(filename, text);
return;
} catch (f) {}
// Node.js
try {
var fs = require("fs");
var nodejs_path = require("path");
var fd = fs.openSync(nodejs_path.join(path, filename), "w");
fs.writeSync(fd, text, 0);
fs.closeSync(fd);
return;
} catch (g) {}
也因為它需要 PhantomJS 等支援,用一般瀏覽器打開是不會看到 XML 報表的。
測試涵蓋度報表
測試本身的品質很大一部分取決於涵蓋度,這裡我們選用 JSCover 來統計涵蓋度,運作原理我猜大致上是這樣:
執行 JSCover 並把 document-root 參數設定為我們專案的目錄就可以進行涵蓋度測試了
java -jar /usr/local/bin/JSCover-all.jar -ws --document-root=MyApp --port=8001
用一般瀏覽器開啟
JSCover 本身使用 JSON 儲存統計數據,也能夠轉換成 Cobertura 的 XML 格式,這個格式能夠很容易地跟 Jenkins 整合。另外,為了支援測試用的瀏覽器 PhantomJS,需要額外在 phantomjs-testrunner.js 裡面呼叫 JSCover 提供的函式來儲存涵蓋度統計。
function getXmlResults(page, key) {
return page.evaluate(function(){
jscoverage_report('phantom'); // add this line
return window["%resultsObj%"] || {};
}, {resultsObj: key});
}
Note
因為採用 JUnitXMLReporter 的原因,這個函式正常下會被呼叫,就直接加在裡面,節省一次 page.evaluate()。
JSCover 官方建議做法是在 phantom.exit() 前多呼叫一次
page.evalute(function(){ jscoverage_report('phantom'); });
Tip
To speedup your testing, use –no-instrument to exclude libraries and test code. Also note URL param of the option need to be start with/
. e.g.--no-instrument=/lib
.
測試流程
回頭檢視一下整個流程
- RD 根據需求撰寫程式碼
- RD 使用 Jasmine 撰寫單元測試
- RD 使用一般流覽器或 PhantomJS 對 JSCover server 執行單元測試,直到測試通過
- RD 檢查 JSCover 報表,單元測試需涵蓋所有新增的程式碼
- 呈交程式碼 (若是多人開發的話就進行 code review)
- 自動化;使用 PhantomJS 進行無人測試 (Server 為 JSCover,以同時進行涵蓋度統計)
Note
3, 4 與 6 的差異在於範圍的不同,開發者可以只測試自己新撰寫的部分(省略回歸測試),自動化的 6 則是要求全面性的測試與統計。
流程中只寫了 RD ,我認為以 GUI 來說, QA 主要工作在整合性測試,使用 Selenium 類型的工具做自動化,以及手動測試無法自動化的部分。
自動化
自動化的工作當然是交給 Jenkins,建置指令基本上就是操作前面介紹的工具,建置完畢 (post build) 的指令指定好 JUnit XML report 與 Cobertura 的檔案,用於呈現測試報表與涵蓋度報表。
Future Work
Sandbox for Ext’s Store class
Unit-test for web GUI should exclude effect of server logic. IOW, we have to offer GUI a fake or a static server. In case of ExtJS, it looks the proxy config of Store is a good candidate.
Written with StackEdit.
留言
張貼留言