跳到主要內容

Automate Unit-test of Web GUI

Automate Unit-test of Web GUI

目標

基於 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.

測試流程

回頭檢視一下整個流程

  1. RD 根據需求撰寫程式碼
  2. RD 使用 Jasmine 撰寫單元測試
  3. RD 使用一般流覽器或 PhantomJS 對 JSCover server 執行單元測試,直到測試通過
  4. RD 檢查 JSCover 報表,單元測試需涵蓋所有新增的程式碼
  5. 呈交程式碼 (若是多人開發的話就進行 code review)
  6. 自動化;使用 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.

留言

這個網誌中的熱門文章

得利油漆色卡編碼方式

得利油漆色卡編碼方式 類似 Munsell 色彩系統 ,編碼方式為 HUE LRV/CHROMA 例如 10GY 61/449 ( 色卡 ) 編碼數值 描述 10GY hue ,色輪上從 Y(ellow) 到 G(reen) 區分為 0 ~ 99 ,數值越小越靠近 Y,越大越靠近 G 61 LRV (Light Reflectance Value) 塗料反射光源的比率,數值從 0% ~ 100% ,越高越亮,反之越暗,也可理解為明度 449 chroma 可理解為彩度,數值沒有上限,越高顏色純度 (濃度) 越高 取決於測量儀器,對應至 RGB 並不保證視覺感受相同。 參考資料: 色卡對照網站 e-paint.co.uk Written with StackEdit .

C++17 新功能 try_emplace

C++17 新功能 try_emplace 回顧 emplace 大家的好朋友 Standard Template Library (STL) 容器提供如 push_back , insert 等介面,讓我們塞東西進去; C++11 之後,新增了 emplace 系列的介面,如 std::vector::emplace_back , std::map::emplace 等,差異在於 emplace 是在容器內 in-place 直接建構新元素,而不像 push_back 在傳遞參數前建構,下面用實例來說明: struct Value { // ctor1 Value ( int size ) : array ( new char [ size ] ) , size ( size ) { printf ( "ctor1: %d\n" , size ) ; } // ctor2 Value ( const Value & v ) : array ( new char [ v . size ] ) , size ( v . size ) { printf ( "ctor2: %d\n" , size ) ; memcpy ( array . get ( ) , v . array . get ( ) , size ) ; } private : std :: unique_ptr < char [ ] > array ; int size = 0 ; } ; struct Value 定義了自訂建構子 (ctor1),以指定大小 size 配置陣列,複製建構子 (ctor2) 則會配置與來源相同大小及內容的陣列,為了方便觀察加了一些 printf 。當我們如下使用 std::vector::push_back 時 std :: vector < Value > v ; v . push_back ( Value ( 2048 ) ) ; 首先 Value 會先呼叫 ctor1,傳給 push_ba...

UTF8 與 Unicode 的轉換 (C++)

UTF8 與 Unicode 的轉換 (C++) 先釐清一下這兩者的性質 Unicode: 為世界上所有的文字系統制訂的標準,基本上就是給每個字(letter)一個編號 UTF-8: 為 unicode 的編號制定一個數位編碼方法 UTF-8 是一個長度介於 1~6 byte 的編碼,將 unicode 編號 (code point) 分為六個區間如下表 1 Bits First code point Last code point Bytes Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6 7 U+0000 U+007F 1 0xxxxxxx 11 U+0080 U+07FF 2 110xxxxx 10xxxxxx 16 U+0800 U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx 21 U+10000 U+1FFFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 26 U+200000 U+3FFFFFF 5 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 31 U+4000000 U+7FFFFFFF 6 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 觀察上面的表應該可以發現 除了 7 bits 的區間外,第一個 byte 開頭連續 1 的個數就是長度,例如 110XXXXX 就是 2 byte 長,而 1110xxxx 就是 3 byte 除了第一個 byte 外,之後的 byte 前兩個 bit 一定是 10 開頭,這樣的好處在於確立了編碼的 self-synchronizeing,意即當編碼為多個 byte 時,任取一個 byte 無法正常解碼。 Note 第一點中的例外 (7 bits) 是為了與 ASCII 的相容性,而第二點會影響到 code point 至 UTF-8 的轉換。 為了與 UTF-16 的相容性,在 R...