Test Coverage on Dynamic Lazy Loading JavaScript – Test coverage – Web application



Test Coverage on Dynamic Lazy Loading JavaScript – Test coverage – Web application

0 0


research-slide-tcse

Research slide for TCSE

On Github RickyChien / research-slide-tcse

Test Coverage on Dynamic Lazy Loading JavaScript

Created by Ricky Chien

Test coverage

首先是Test Coverage

Why we need test coverage?

For better software quality, we need to write tests

Coverage tool can give us a statistics report after testing

Blanket.js - JavaScript test coverage tool

為什麼我們需要Test Coverage? 一般的programmer會寫測試來確保程式的品質, 不過寫好了測試後卻無法得知寫的測試是否有完整的涵蓋系統,這時programmer可以藉由 coverage tool來取得測試覆蓋的統計資料,進一步了解撰寫的測試涵蓋狀況。

Web application

接下來會開始介紹Web application

Web is changing

  • Nowadays, web is going to become more complicated

  • Browser can take more jobs than server

Web is changing。 我們知道現今的Web已經變得越來越複雜。 Browser端功能越來越強大,可以幫助Server分擔許多複雜的任務。 Gmail就是一個最著名的例子,有用過的人都知道,在使用Gmail時不像在瀏覽一般網頁,而更像是在使用一般桌面應用程式。 也因此,我們將它稱之為Web應用程式。

Web application depends on network

另外一方面,Web應用程式與一般桌面應用程式不同的地方在於,他非常依賴網路,甚至連背後的原始碼模組也都是經由網路下載的。

Dynamic lazy loading

為了要提升網路速度,Dynamic lazy loading的概念被提了出來。中文稱為動態載入。 左方圖中,一般JavaScript原始碼都是使用HTML script語法載入。 不過我們其實可以透過JavaScript的邏輯來判斷在適當的時機載入另一份JavaScript,以達成動態載入。 如此一來可以有效降低網路流量,提升使用者體驗品質。

Background

接下來是與本研究相關的Background

Source instrumentation

Before

After

首先,code coverage統計程式碼覆蓋率的背後原理主要是透過source instrumentation,我們簡稱instrumentation。 instrumentation是一種在不影響原程式行為的狀況下對原始碼進行增修,然後達成記錄程式走訪行為的目的。 直接看範例圖,原本的程式碼如上圖,經過instrumentation之後,下圖的程式碼新增了額外的變數,用來記錄原始碼的走訪過程。

Coverage mechanism in web

Browser instrumentation / Server instrumentation

接下來讓我們分析一下在Web上的code coverage實作方式,主要可以分為兩種方式 - Browser instrumentation 與 Server instrumentation。

Browser instrumentation

Browser instrumentation即為在瀏覽器上做instrument。如圖,在此架構中,測試與source instrumtation都是執行在瀏覽器端,一般測試環境背後須啟動server用來回應前端request, 然後由瀏覽器進行source instrumentation。 在使用非動態載入的原始碼可以正常的被instrument,不過缺點是他無法偵測dynamic lazy loading動態載入的原始碼。

Server instrumentation

Server instrumentation則將instrument移至額外的instrumentation server來做, 這樣可以讓前端瀏覽器在完全不知情的情況下下載已經被instrumted後的原始碼,然後就可以順利的分析程式碼覆蓋。 而且前端不論透過非動態或動態載入都可以取得instrumented版本的原始碼進行覆蓋率分析。

If situtaion is more complicated

Sometimes we need to interact with a real server

看起來server instrumentation似乎很完美,但有時測試環境情況會更加複雜。 例如:有時候我們會與後端的web server做互動,需要web server回傳的真實的資料來做測試。 這時後,單純為了code coverage而多加入了一個instrumentation server會使得整合發生困難。 不過若使用Browser instrumentation的方式時可以避開這種問題,而且在安裝與設定上, Browser instrumentation的方式相較起來也會比較容易。

Issue - Zero coverage

In browser instrumentation

剛剛我們已經說明了在Browser instrumentation方式上會有無法覆蓋動態載入原始碼的問題。 本研究將這個問題稱之為Zero Coverage。 然後接下來用兩個實際的範例來展示一下什麼是Zero coverage。

Case

Firefox OS email app should be covered 21 modules

去年我幫Firefox OS的unit testing framework導入了code coverage。 就剛好是使用了browser instrumentation的方式。 在導入完成後,竟然發現coverage report不正確。FFOS內建的email app共分了21個模組, 而且每個模組都有撰寫unit test,不過在結果圖中我們看到只有一個模組檔案被正確的覆蓋,而且coverage非常低。 在我觀察之下,這些無法覆蓋的模組檔案幾乎都是與動態載入有關。

Improve browser instrumentation

Make it possible to cover dynamic lazy loading scripts

那本研究目的就是在於改進使用browser insturmentation,修正zero coverage問題。 在JavaScript coverage tools中,較著名且是使用browser insturmentation的就是blanket。 所以我們想要直接改進blanket,使得dynamic lazy loading可以被偵測。

Analysis

Analyze web loading approachs

首先我們先來分析web上各種載入方式,找出方法來偵測動態載入的code coverage。

Script Loading - HTML Script

<script src="path-to/script.js"></script>
HTML script方法並非動態載入。 一般JavaScript載入方式都會透過這種在HTML加入script tag的方式來載入原始碼,

Script Loading - XHR (Ajax)

var xhr = new XMLHttpRequest();
xhr.open('GET', 'path-to/script.js'); // Assign script url
xhr.onload = function (script) {
  eval(script); // Execute script
};
xhr.send();
接下來的方式皆為動態載入方法。 首先,XHR是browser提供的Web API可以讓你透過JavaScript來下載原始碼,他也是Ajax技術的核心。 透過XMLHttpRequest物件並且指定要下載的路徑,接下來就能在取得原始碼後執行原始碼,達到動態載入。

Script Loading - Document.write

document.write('<script src="path-to/script.js"></script>');
document.write一樣是browser提供的API,它能夠重新改寫原始的HTML, 像是圖中重新寫入script tag來動態下載原始碼。 不過此種方式有performance與security問題,目前較不推薦使用。

Script Loading - DOM modification API

appendChild / insertBefore / replaceChild

var script = document.createElement("script");
script.src = url; // Assign script url

document.head.appendChild(script);
parentNode.insertBefore(script, node);
parentNode.replaceChild(script, oldNode);
這邊所說的DOM modification API一樣是Web API。 主要有appendChild, insertBefore, replaceChild這三種能對於DOM做新增元素的方法。 我們可以動態的新建一個script元素,然後指定其source路徑, 最後使用DOM modification API新增到HTML上面來達成下載。

Script Loading - Function Wrapping

Famous module loader library such as RequireJS using syntax :

require(["path-to/script.js"], function() {
  // This function is called after path-to/script.js has loaded.
});
JavaScript有許多不同的module loader libraries。像是requireJS就是滿著名的library。 這種library會使用固定形式的API來做動態載入,像圖中會定義一個require function call來載入原始碼。 我們將這種方式稱為function wrapping。

Summary

我們來總結一下這些載入方法。 圖中顯示了五種不同的動態載入方式,HTML script為非動態載入方式。 而document.write剛剛提到有bad performance與security問題,目前非常少人使用。 那下一個function wrapping方式其背後的實作原理大部份都是透過DOM modification API實作。 所以我們如果要偵測動態載入方式,那只需要針對XHR與DOM modification API這兩種方式下手。

Method

根據之前的分析結果,我們提出了解決zero coverage的方法。

Browser Instrumentation Process

首先,我們先分析一下browser instrumentation的執行流程。 一開始,我們會在HTML上定義好需載入的script元素,這包括原始碼與測試程式碼, 接下來coverage tool會在HTML載入完成後啟動,搜尋HTML上的script並進行instrumentation, 並執行被instrumented script,然後告訴testing tool開始跑測試,最後輸出coverage report。 不過在跑測試過程中,可能會發生dynamic lazy loading,所以使得這部分的模組檔案出現zero coverage,使得report不正確。

Solution

那我們的解決方法是,在要執行測試之前,先overwrite browser的動態載入Web API,也就是XHR與DOM modification API。 在下載script後直接執行source instrumentation並且執行這個instrumendted script, 這樣子在接下來的測試中,當有動態載入發生時,下載的程式碼就能夠被立即instrument,使得coverage tool可以正確分析走訪行為。

DOM modification API

Overwrite native appendChild / insertBefore / replaceChild

var originalAppendChild = Element.prototype.appendChild;

Element.prototype.appendChild = function(newElement) {
    // Do our hack here
    return originalAppendChild.apply(this, args); // invoke native method
};
就Overwrite DOM modification API裏面的appendChild為例子來說。 首先,我們必須先了解Browser Web API的prototype繼承結構, 然後找到了Element.prototype.appendChild這個function來進行overwrite。 JavaScript可以透過像C++中的function pointer形式將function存在變數中, 我們透過這個方法先將Element.prototype.appendChild暫存起來,然後直接overwrite成我們的邏輯。 在我們的邏輯中就會將下載來的script進行source instrumentation,依照情況invoke原始appendChild function call。 這種overwirte native API在開發中很罕見,所以能安全的並不影響原始程式行為需要做些驗證。

XHR API

Overwrite native open method in XHR object

var originalXHROpen = XMLHttpRequest.prototype.open;

XMLHttpRequest.prototype.open = function(method, url) {
  // Do our hack here
  return originalXHROpen.apply(this, args); // invoke native method
};
那XHR API的overwrite方式也如同上面的方式,所以這邊就不再多做解釋了。

Achievement

現在我們就來展示一下研究的成果

A simple dynamic lazy loading website

我們先來看一下前面提到的測試網站結果。 可以發現在加入的本研究方法後,模組A-F幾乎都已經被coverage tool涵蓋, 而且overwrite Web API的方法也沒有影響原始網站的動態載入行為,並且成功通過unit tests。

New feature has landed in Firefox OS

不過之前的範例規模太小,我們應該將使用真實的Web application來驗證結果, 所以將研究方法套用到Firefox OS的unit testing framework上,如圖。 我們一樣選用先前的email app當例子,在加入研究方法後,所有21個模組檔案都正常的coverage tool涵蓋, 經過檢驗後coverage結果也是正確的。 並且在套用到其他不同的Firefox OS 內建app上結果也正確,並且程式都運作良好,沒有影響到原始程式行為。

Conclusion

  • We demonstrated the zero coverage issue

  • Analyzed source instrumentation mechanism and dynamic lazy loading schemes

  • Proposed a solution to overwrite native Web APIs to detect dynamic lazy loading

  • Solution was succssful and safe that even integrated into FFOS testing framework

  • New feature has proposed to Blanket

那最後就是本研究的結論。 研究中展示了zero coverage問題。 然後我們分析了coverage tool中source instrumentation與dynamic lazy loading的的方式。 接下來提出一個overwrite native Web APIs的方式來解決zero coverage問題。 並且驗證了這個解決方法是可行的,coverage report在加入了研究方法後就能夠涵蓋到動態載入的原始碼。 評估過程中,我們將研究方法導入FFOS的testing framework上,驗證了本研究方法的安全性,並不會影響原始程式行為。 那這個新的研究方法也已經proposed給blanket.js,期望未來能夠整合成功,如此一來可以幫助到世界各地的開發者。

Q & A

1