並不是不會做,而是怎樣做得有效率,基本流程有三個步驟,首先要模擬環境、然後模仿實際輸入,最後核對結果。
要是架構設計失誤,光是第一步的環境模擬就夠嗆了,模仿輸入及核對結果就是一項會隨test coverage要求上昇而變得更加吃力的苦力工作。
最慘的是每當UI設計修改,以前寫的test case就有可能要面臨推倒重來的困境。
在UI改動頻繁的項目,我甚至會直接放棄自動化的GUI測試,盡量簡化UI的元件,只把力氣放在程式邏輯及數據上,直至我見到Jest的Snapshot Testing。
Jest - Snapshot Testing
有一種GUI的自動化測試方法會制作元件的Screenshot圖像,再跟之前的版本進行對比,若然二者不符測試就會失敗,要麼修改程式、要麼更新Screenshot。
Jest的Snapshot Testing的概念也是一樣,但不弄screenshot圖像,而是把視覺元件轉換成一個像XML/HTML的文字描述的snapshot,然後作出比較。
這樣子就不用找地方存放圖像,可以直接把文字描述直接存放在版本控制內。
在見識到這種GUI自動化測試方法後,我就一直在想能不能拿到QML上用呢?會不會可以解決很多問題呢?
QML - Snapshot Testing
為了把Snapshot Testing帶進QML中,我試著做了這個項目。QML Snapshot Testing
https://github.com/e-fever/snapshottesting
跟Jest的版本不一樣,文字描述並沒有使用XML的形式,改為使用像QML語法的表達方法,同時加入了GUI版本的”比較視窗”,可以提供更加詳細的資訊。
安裝及使用方法等就不在這裹談了,可以看Github的說明,這裹想寫的是有關於TDD的配合。
Test Driven Development
在Jest的FAQ提過TDD並不適合配合Snapshot Testing使用,主要問題是人手寫Snapshot file(即是那個UI的文字描述)太麻煩,Snapshot Testing的原意並不是提供設計指引,而是找出有沒有出乎意料的修改。但QML的Snapshot並非完全跟Jest一樣,有二項很大的分別:
- QML版提供GUI的介面,不單提供Diff,還可以瀏灠完整版本的Snapshot
- 與此同時,被截取Snapshot的UI元件依然可以運作,開發者可以手動輸入並用肉眼視察結果
就著這二點的不同,要進行TDD式的做法並非不可能的事。
TDD的主要精神有2個 -「Write Test code first」、「Red-Green-Refactor Cycle」
我會用以下的例子說明如何利用Snapshot Testing並配合TDD的2個要求
任務:寫一個元件,最初會顯示”Ready?”,點擊後會變成”Go!"
工作一:Write (Failing) Test code first
大概是這個樣子。(程式代碼)tst_CustomItem.qml
import QtQuick 2.0 import QtTest 1.0 import SnapshotTesting 1.0 Item { id: root width: 320 height: 240 CustomItem { // Don't put this under TestCase object. id: customItem anchors.fill: parent } TestCase { name: "CustomItem" when: windowShown function test_CustomItem() { var snapshot = SnapshotTesting.capture(customItem); SnapshotTesting.matchStoredSnapshot("test_CustomItem_default", snapshot); mouseClick(customItem) snapshot = SnapshotTesting.capture(customItem); SnapshotTesting.matchStoredSnapshot("test_CustomItem_clicked", snapshot); } } }
至於CustomItem的內容就先讓他一個預設的內容 - Item {}
這樣測試程式⋯⋯就完成了!
咦,不用測試顯示的文字嗎?
正常來說需要這個步驛,還要提供介面,告訢元件以外的人正在顯示的文字.
/// CustomItem.qml Item { property alias displayText : text.text Text { id: text } }
但這個屬性本身僅僅是為測試提供服務,本身是沒有需要的。
在用了Snapshot Testing後就不再需要用到這個屬性,即管跑一跑以上的程式吧。
因為是第一次執行,並沒有儲存過任何snapshot,所以Snapshot.matchStoredSnapshot會彈一個”比較視窗"出來詢問,概然CustomItem還只是個空白的元件,當然回答”No”,測試程式回報失敗,我們寫了一個失敗的測試,第一項工作完成。
工作二:Red-Green-Refactor Cycle
現在測試程式亮紅燈,無法通過,開始寫CustomItemimport QtQuick 2.0 MouseArea { Text { id: text text: qsTr("Ready?") font.pixelSize: 28 anchors.fill: parent verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } }
再跑一次測試程式,同樣地停在相同的地方,但不同的是程式的視寫中顯示了”Ready?”的字句。
經過肉眼的判斷,程式符合預期,所以按下yes.
之後程式繼續跑,模擬滑鼠輸入後,又進行了一次snapshot test,基於MouseArea沒有實現所需的功能,故此元件仍然顯示”Ready?”,選擇”No”
再做一次修改吧,這次加入所需要的功能。
import QtQuick 2.0 MouseArea { Text { id: text text: qsTr("Ready?") font.pixelSize: 28 anchors.fill: parent verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } onClicked: { text.text = "Go!"; } }
因為UI沒有修改,第一個Snapshot Test會直接通過,再次停在第二項測試。
這次沒錯了,所以按”Yes”,大功告成,可以commit push上伺服器了
任務三:UI規格修改
對開發者來說規格的修改是最為苦惱的, 改動太大或許會讓前功盡廢,其中受到影響最大的多數是自動化GUI測試。假設現在要一項小的修改,文字要要改成大寫”READY!?” / “GO?”,所以CustomItem變成
(註:其實用font.capitalization便行,但示範就裹就不用這個方法了)import QtQuick 2.0 MouseArea { Text { id: text text: qsTr("READY?") font.pixelSize: 28 anchors.fill: parent verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter } onClicked: { text.text = "GO!"; } }
再跑一次Test Case,因為Snapshot不符的關係,比較用的UI又再次彈出來詢問,當然全部都回答”Yes”。
在就這種小改動的情況下,用上Snapshot Test的測試項目甚至不用作出任何的修改,程式會自動找出被改動過的元件,經開發者確認後便自動修新,這大大滅輕維護的成本。