基於PhantomFlow的自動化UI測試

網易考拉前端團隊發表於2017-09-19

基於PhantomFlow的自動化UI測試

本文的目錄結構:

  1. 自動化測試的意義
  2. 可測試方向分析
  3. 競品分析&技術選型
  4. PhantomFlow介紹
  5. 持續整合

自動化測試的意義

  • 一個專案最終會經過快速迭代走向以維護為主的狀態,在合理的時機以合理的方式引入自動化測試能有效減少人工維護成本。
  • 自動化的收益 = 迭代次數 全手動執行成本 – 首次自動化成本 – 維護次數 維護成本
  • 另一方面,當我們需要對程式碼進行重構或者完善,在修改結束時我們如何確定專案僅僅是被重構了,而不是被改寫了?此時測試將是一根救命稻草,它是一個衡量標準,告訴開發人員這麼做是否將改變結果。

可測試方向分析

前端自動化測試的方向有:

  • 單元測試
  • UI迴歸測試
  • 功能測試
  • 效能測試
單元測試
  • 在計算機程式設計中,單元測試(Unit Testing)又稱為模組測試, 是針對程式模組(軟體設計的最小單位)來進行正確性檢驗的測試工作。
  • 單元測試已經有非常完善的工具體系,借用2016 JavaScript 之星的圖,常用的單元測試框架有:

UI迴歸測試

UI迴歸測試通常採用的方法是畫素對比:

  • 畫素對比基本的思想認為,如果網站沒有因為你的改動而介面錯亂,那麼在截圖上測試頁面應當跟正常頁面保持一致。
  • 畫素對比比較出名的工具是PhantomCSS,它結合了 CasperJS 截圖和 ResembleJs 影象對比分析。從易用性和對比效果來說是很不錯的。
畫素對比 - PhantomCSS

初次執行的時候,會截圖並作為baseline,後面再執行的時候,再生成截圖,並與baseline比較,生成diff結果。

畫素對比需要注意的事項:

  • 推薦對某些區域進行測試而不是整個頁面。影象越大對比越慢。
  • 如果測試區域內有動態元素,可以通過選擇器來隱藏。
  • 介面對比只是一個環節,需與其他測試相結合,合理結合才是關鍵。
功能測試
  • 僅僅對介面進行測試是不夠的,即使介面正確,功能不正確也是斷然不能接受的。
  • 最直接的功能測試就是通過模擬使用者操作流程來判斷頁面的展現是否符合預期。
  • 有時,我們需要瀏覽器處理網頁,但並不需要瀏覽,比如生成網頁的截圖、抓取網頁資料等操作。PhantomJS的功能,就是提供一個瀏覽器環境,你可以把它看作一個“虛擬瀏覽器”,除了不能瀏覽,其他與正常瀏覽器一樣。它的核心是WebKit引擎,不提供圖形介面,我們可以用它完成一些特殊的用途。
PhantomJS和CasperJS
  • CasperJS是對PhantomJS的封裝,提供了更加易用的API, 增強了測試等方面的支援。
  • 如下圖,很方便的實現了一個百度貼吧自動發帖的功能。

效能測試
  • 效能測試通常來測試網站的效能,如白屏時間、首屏時間等。
  • 通常的工具有:chrome devtool,PageSpeed等線上測試網站。

    考慮到我們主題是nek-ui元件庫的測試,效能測試的部分,這裡不做贅述。

競品分析&技術選型

我們的測試物件是NEK-UI元件庫,這一部分分析了其他元件庫的測試方法並選擇了最終的測試方案。

RegualrUI測試方案分析:

RegularUI使用的測試方案是karma + mocha的黃金搭檔

這種方式存在的問題:

  • 沒有UI部分的測試,這也就是單元測試與UI測試的差別。
  • 雖然可以通過呼叫元件的某些方法,達到使用者操作同樣的效果,但是跟真實的使用者操作還是有差別的。比如,這個時候,template的這個方法根本沒繫結,或者傳參錯誤,這種情況是覆蓋不到的。
Ant-design測試方案分析:
  • Ant-design是螞蟻金服的一套企業級的 UI 設計語言和 React 實現,目前是Github上一個很火的專案:

  • Ant-design作為一個基於react的元件庫,使用的測試框架是同樣出自Facebook的Jest。

  • Ant-design使用的是Jest中稱為snapshot testing的測試方案。

  • Jest的官方文件上介紹到,Jest的Snapshot Testing與典型的snapshot test不同,不是生成截圖並比較圖片的差異,而是直接輸出React tree 的最終渲染dom結構。

    Snnpshot Testing介紹:

再來看看Ant-design中的實際使用:

測試某個元件的時候,就會引入改元件資料夾裡demo資料夾下的所有md檔案,這個md檔案是元件的各種示例,同時也用於ant-design的官方文件。然後,使用enzyme和enzyme-to-json提供的方法經過render->renderToJson->toMatchSnapshot, 第一次執行的時候會輸出如下的.snap檔案:

這個檔案要隨著程式碼一起提交到倉庫,下次執行測試的時候,就和這個.snap檔案做比較。

當然僅僅測試dom結構不變是不夠的,ant-design的測試裡,還有模擬使用者操作的測試。如下兩個檔案,demo.test.js是上面的snapshot部分,index.test.js是模擬使用者操作部分。

Index.test.js裡做了什麼呢?

在元件上繫結事件方法,然後模擬事件,判斷方法是否被呼叫。

這種方式存在的問題:

  • DOM結構不變並不完全等於樣式不變。
  • 很多相關工具都是React專用。

分析完了2個元件庫的測試方案,那麼我們期望的測試方案應該包含什麼呢?

  • 元件庫同一般的純JS庫不用的地方,使得單純的單元測試是不夠用的,最好要包含UI測試的部分。
  • 有模擬使用者操作的部分。
  • 能方便的管理test case。

    基於此,我們最終選擇了PhantomFlow。

PhantomFlow介紹

  • PhantomFlow是基於決策樹(decision tree)的ui test 框架,是對PhantomJS、CasperJS、PhantomCSS的包裝。

  • PhantomFlow假定如果頁面正常,那麼在相同的操作下,每次頁面所展現的應該是一樣的。基於這點,使用者只需要定義一系列的操作流程和決策分支,然後利用PhantomCSS進行截圖和影象對比。最後將測試結果在一個視覺化報表中展現出來。

這裡採用倒序的方式先來看一下PhantomFlow生成的測試報告,再介紹具體的使用:

這是PhantomFlow的母公司Huddle在他們實際的業務中使用的報告截圖:

同時PhantomFlow也提供了單獨檢視某一個操作流的功能:

圖中的每一條線代表一個使用者操作流。綠色的點表示截圖對比通過,紅色的點表示截圖對比失敗,灰色的點表示這僅僅是PhantomFlow流程中的一步,並沒有真正的操作。
黃色的表示是一個操作,但是操作裡面並沒有進行截圖。我們只要關心其中綠色的點和紅色的點。

PhantomFlow是基於決策樹的,那麼什麼是決策數呢?沒必要吧它想的那麼神祕,我們可以認為它就是普通的流程圖。

PhantomFlow方法介紹
  • flow (string, callback):初始化一個test suite,回撥函式中可以包含step, chance 和 decision。

  • step (string, callback):一個單獨的步驟,回撥函式中可以包含PhantomCSS的截圖,CasperJs的操作事件和斷言

  • decision (object):定義一個使用者的決定,引數是一個物件,key用來描述decision的名稱,value是一個function,裡面可以包含後續的decision, chance和step

  • chance (object):功能上同decision一樣,只是在語義上區分decision,用來描述不是使用者主動的行為。

step對應決策樹中的矩形,表示使用者具體的某一個操作。decision和chance對應決策樹中的菱形,表示使用者的選擇。

這是用PhantomFlow描述使用者喝咖啡的一個場景:

PhantomFlow在NEK-UI元件測試中的使用
  • 以ui.select元件為例:

PhantomFlow提供了簡單的方法來描述使用者的操作流,具體的操作使用回撥函式裡的CasperJS來完成:

    function goToPage() {
        casper.thenOpen("http://localhost:9001/test/index.html", function() {
            this.echo('PageTitle: ' + this.getTitle());
            phantomCSS.turnOffAnimations();
        });
    }

    function injectModule(json) {
        casper.evaluate(function(json) {
            console.log(JSON.stringify(json));
            new NEKUI.UISelect(json).$inject('#module');
        }, json);
        casper.onConsoleMessage = function(msg) {
            console.log(msg);
        }
    }

    function goToModule() {
        casper.waitForSelector(
            '#module .u-select2',
            function success() {
                phantomCSS.screenshot('#module .u-select2');
                casper.test.pass('Should see the uiselect module' );
            },
            function timeout() {
                casper.test.fail('Should see the uiselect module');
            }
        )
    }

    function clickModule() {
        casper.click('#module .dropdown_hd');
        casper.waitForSelector(
            '#module .dropdown_bd',
            function success() {
                phantomCSS.screenshot('body');
                casper.test.pass('Should see the options of module');
            },
            function timeout() {
                casper.test.fail('Should see the options of module');
            }
        )
    }

    function selectAnOption(optionIndex) {
        casper.click('#module .m-listview li:nth-child(' + (optionIndex+1) + ')');
        phantomCSS.screenshot('body');
    }複製程式碼
測試報告使用介紹:

執行測試的常用引數:

在npm test後帶上如下引數即可

  • report:開啟瀏覽器,生成測試報告。

  • debug:輸出更多的log資訊,強制切換到單執行緒執行。

  • earlyexit: 預設為false,設定為true的話,遇到第一個failure就會終止測試。

  • threads:設定多執行緒來執行測試,預設為4。

常用CasperJS方法介紹
  • casper.thenOpen(String location[, mixed options]): 用來開啟一個地址,當網頁載入完成之後,執行一個方法。

  • casper.waitForSelector(String selector[, Function then, Function onTimeout, Number timeout]):等到DOM裡有一個元素匹配選擇器,可以傳入成功的方法和失敗的方法,和等待的毫秒數(預設5000)。

  • casper.click(String selector, [Number|String X, Number|String Y]):在匹配選擇器的第一個元素上執行一次click

  • casper.mouseEvent(String type, String selector, [Number|String X, Number|String Y]):在匹配選擇器的第一個元素上觸發滑鼠事件。支援的事件有:mouseup、mousedowm、click、dblclick、mousemove、mouseover、moustout、mouseenter、mouseleave and contextmenu

  • casper.getHTML([String selector, Boolean outer]):獲取匹配選擇器裡的元素的內容。

  • casper.evaluate(Function fn[, arg1[, arg2[, …]]]):在開啟的當前頁面環境下執行方法。

  • casper.test.fail(String message):新增一個fail test。

  • casper.test.pass(String message):新增一個pass test。

  • casper.test.assertEquals(mixed testValue, mixed expected[, String message]):斷言兩個值嚴格相等。

使用PhantomFlow要注意的地方
  • 資料確定性:同樣的測試用例在元件上執行多次,產生的結果應該相同。如果測試方法裡面包含有Date.now()這種“資料不確定”的因素,會導致每次執行測試,頁面顯示的都不相同,這個時候可以引入sinon,用它的stub來託管資料不確定的方法。
  • 適當的新增斷言:截圖測試的特性決定了baseline一定要正確。假如首次執行的時候截圖就錯誤,後面的執行錯誤一樣是不會報錯的。因此需要新增一些dom取值斷言。

持續整合

  • 經常手動執行npm test?很麻煩有沒有
  • 別人專案裡的這兩個徽章怎麼來的?這兩個徽章是專案可靠度的體現。

  • 持續整合(Continuous integration,CI),一種軟體工程流程,指工程師將自己對於軟體的複本,每天整合數次到主幹上。在測試驅動開發(TDD)的做法中,通常還會搭配自動單元測試。

Travis CI

Travis-ci是一款持續整合服務,它能夠很好地與Github結合,每當程式碼更新時自動地觸發整合過程。Travis CI

開啟Travis CI的官網,用Github賬號登入。

選擇需要開啟Travis CI服務的倉庫:

開通了服務的倉庫,每當有push程式碼的時候,Travis CI就會為我們執行相關的操作。這裡可以檢視執行的進度和結果等。

在Github提交記錄裡也會顯示CI執行的結果。

要告訴Tracvis執行什麼,需要在我們的專案裡新增一個.travis.yml檔案,其最簡單的配置如下:

這裡指定了CI執行的語言,語言版本,哪些分支,install執行npm install, script是具體的操作部分,這裡讓CI執行 npm test。

Travis CI在執行完之後,會將結果郵件通知給使用者, 預設規則如下:

By default, email notifications are sent to the committer and the commit author when they are members of the repository, 
that is they have

 - push or admin permissions for public repositories.
 - pull, push or admin permissions for private repositories.
Emails are sent when, on the given branch:

 - a build was just broken or still is broken.
 - a previously broken build was just fixed.複製程式碼

關於travis ci的生命週期等更多配置可以查閱這裡

Coveralls 程式碼覆蓋率託管平臺
  • 程式碼覆蓋率通常被用來衡量測試好換的指標。Coveralls就是將測試匯出的覆蓋率檔案進行分析,以視覺化的形式展現出來的一個工具。
  • 使用Coveralls的專案包括:React、Express、Gulp、Ant-design等等。

同樣的使用Github賬號登陸:

選擇開啟服務的倉庫:

在專案的package.json檔案script裡新增一條coverage的命令, 即將istanbul等覆蓋率工具生成的lcov檔案給coveralls:

在travis.yml檔案的after_script中執行npm run coverage,告訴CI伺服器執行這條命令:

總結

  • 自動化測試不僅能有效的減少人工維護成本,同時為程式碼的維護迭代提供保障。

  • 前端自動化測試的方向有:單元測試、UI迴歸測試、功能測試、效能測試。

  • RegularUI採用karma+mocha的單元測試,ant-design使用Jest的snapshot測試與模擬使用者的功能測試相結合的方式。

  • PhantomFlow是基於決策樹的,對PhantomJS, CasperJS, PhantomCSS的包裝。以簡單的方式描述使用者操作流。並配以CasperJS的頁面操作,PhantomCSS的截圖,達到非常好的自動化測試效果。

  • 測試時要保證資料的確定性和新增適當的斷言。

  • CI是一種好的軟體工程思想。Travis CI簡單易用,解放了開發人員手動執行測試,非常值得在專案中引入。

相關文章