almost 3 years ago

Translated from "Test JavaFX Apps with TestFX - Simple JUnit-style testing of JavaFX UIs" by Bennet Schulz, Java Magazine, September/October 2015, page 14. Copyright Oracle Corporation.

使用TestFX測試JavaFX應用程式

JUnit風格的JavaFX UIs測試

TestFX是測試JavaFX使用者介面的API,透過模擬使用者的互動,例如點擊按鈕、在文字框中輸入文字,和其他可以在JavaFX應用程式中執行的操作,來自動化測試JavaFX應用程式。這篇文章會先以TestFX簡短的背景開始,讓您理解TestFX的基礎與目標;接著,文章會說明如何從Maven專案開始使用TestFX,並對一個簡單的應用程式撰寫測試;我同樣會討論在使用TestFX前您必須先知道的限制。TestFX 4.0.x的版本目前是alpha階段,因此,這篇文章涵蓋3.1.2穩定版本。我假定您已使用過JavaFX並對其運作有相當程度的理解,包含對FXML很熟悉。

背景

TestFX建築在廣為人知的單元測試框架JUnit之上,就如同JUnit一樣,它相當容易學習與使用,使用TestFX,可以用與寫JUnit測試相似的方式撰寫測試。有幾個元件開發者需要添加,例如TestFX使用Hamcrest matchers在JUnit之上做測試驗證(test assertions)。與JUnit相比,使用Hamcrest matchers的好處是您可以寫出較貼近一般描述的驗證,且當驗證失敗時能取得較有用的錯誤息,提升測試程式的品質,同時也減低因需額外選項進行驗證所帶來的複雜度。

從使用Maven開始

將TestFX加到Maven專案中就如同將JUnit加入一樣簡單,在您的pom.xml中加入如下的片段:

<dependency>
    <groupId>org.loadui</groupId>
    <artifactId>testFx</artifactId>
    <version>3.1.2</version>
</dependency>

初始化一個簡單的測試案例

當您想寫TestFX測試,您的測試類別(test class)須繼承org.loadui.testfx.GuiTest,接著須覆寫getRootNode函式以提供待測畫面(view),如下列的程式片段,函式回傳一個FXML的畫面,但也可以回傳用程式建立(Swing風格)的待測畫面。

public class SampleTest extends GuiTest {

    @Override
    protected Parent getRootNode() {
        Parent parent = null;
        try {
            parent = FXMLLoader.load(getClass().getResource("sample.fxml"));
            return parent;
        } catch (IOException ex) {
            // ...

        }
        return parent;
    }
    // ...

}

透過繼承GuiTest,您同時獲得一些模擬使用者互動的UI測試函式,例如,用來找按鈕或文字輸入框等UI元件的函式,以及點擊按鈕、捲動捲軸等操作的函式。

待測範例應用程式

範例應用程式是一個基於GNOME gcalctool的計算機(Figure 1),它看起來像gcalctool,行為也像gcalctool,但它是用JavaFX開發而不是用gcalctool的原生語言Vala。

Figure 1. 範例應用程式的畫面

和其他的實作相較,這計算機會在輸入框中顯示使用者所有的輸入,當等於(=)按鈕被點擊時,結果才被計算。其他計算機只顯示最後輸入的數值,當等於按鈕被點擊時,只顯示結果,中間輸入的步驟不會被顯示。

這程式畫面是用Gluon Scene Builder 8.0.0建立的FXML檔案,所有按鈕套用CSS線性漸層風格,有趣的部分是對應的controller有三個函式:handleButtonActionhandleRemoveButtonActionhandleCalculationAction,前二個函式非常簡單,最後一個是當=按鈕被點擊時計算結果的函式,我將專注於測試這個函式。List 1是該函式根據使用者的輸入計算的片段程式碼。

Listing 1.
@FXML
private void handleCalculationAction(ActionEvent e) {
    String displayText = display.getText();
    int textLength = displayText.length();
    String result = "";
    if (displayText.contains("+")) {
        int plusIndex = displayText.indexOf("+");
        Double a = Double.valueOf(displayText.substring(0, plusIndex));
        Double b = Double.valueOf(displayText.substring(plusIndex + 1, textLength));
        result = String.valueOf((a + b));
    } else if (displayText.contains("x")) {
        int multiplyIndex = displayText.indexOf("x");
        Double a = Double.valueOf(displayText.substring(0, multiplyIndex));
        Double b = Double.valueOf(displayText.substring(multiplyIndex + 1, textLength));
        result = String.valueOf((a * b));
        display.setText(result);
    }
    // ...

}

這函式進行乘、除、加等運算,在函式的結尾,計算的結果會被設定成畫面上方文字輸入框中顯示的文字,這函式在下一節會被用來介紹如何用TestFX撰寫這範例程式的測試。為了教學的簡潔,省略某些功能(例如:減、二次方、小括弧、小數點等),若想完整功能的計算機,可以在這一期的Java Magazine中下載。

為範例程式寫測試

開始寫一個測試吧!模擬使用者想計算1加2的和:使用者依序點擊1、+及2的按鈕,接著按下=按鈕,計算機會在UI上方的文字輸入框顯示結果3。為了使用自動化測試來測試這情境,如前所述先在Maven中加入相依性,並完成測試類別的初始化(Listing 2)。

Listing 2.
public class CalculatorControllerTest extends GuiTest {

    @Override
    protected Parent getRootNode() {
        Parent p = null;
        try {
            p = FXMLLoader.load(getClass().getResource("gcalctoolFX.fxml"));
        } catch (IOException ex) {
            Logger.getLogger(CalculatorControllerTest.class.getName()).log(Level.SEVERE, null, ex);
        }
        return parent;
    }

下一步是從gcalctoolFX.fxml初始化我們想在這測試類別中測試的畫面(Listing 2),初始化是藉由載入對應的畫面,並將載入的畫面作為測試類別中getRootNode函式的回傳值來完成。完成初始化步驟後,可以開始撰寫測試案例的測試程式碼,一個TestFX測試案例(函式)必須遵循JUnit的公約(或稱慣例),包含使用@Test annotation,必須是public函式、回傳型別為void,Listing 3提供一個簡單的測試。

Listing 3.
@Test
public void testAddition() {
    Button one = find("#one");
    Button plus = find("#plus");
    Button two = find("#two");
    Button equalSign = find("#equal");
    click(one);
    click(plus);
    click(two);
    verifyThat("#display", hasText("1+2"));
    click(equalSign);
    verifyThat("#display", hasText("3.0"));
}

如您在Listing 3中所見,這測試和一般的JUnit測試一樣單純,在測試開始執行前,會透過Listing 2中的getRootNode函式建構待測試畫面,你唯一需要做的是使用從繼承類別GuiTest得來的find函式逐字尋找要點擊的按鈕。在使用FXML建構畫面的案例中,搜尋的文字即UI元件的辨識碼(fx:id),為了在controller類別中連結UI元件(bindings)與處理事件,必須設定辨識碼。辨識碼可以在Scene Builder中設定或直接編輯FXML檔案,若以撰寫程式的方式建構畫面,可以用setId()函式設定測試所需UI元件的辨識碼。

在用find()函式找到所需按鈕後,這些按鈕須以1、+、2、=的順序被點擊,透過呼叫click()函式的方式來點擊按鈕。在點擊=按鈕前,計算機的文字輸入框應該顯示“1+2”,點擊後,顯示的內容應該變成“3.0”作為加法運算的結果。可以用verifyThat()函式驗證顯示的文字是否正確,搭配hasText()函式指定預期顯示的文字內容。提醒一點,TestFX相容JaCoCo與其他涵蓋率計算工具,以紀錄多少行程式碼被您的TestFX測試所涵蓋,因此您可以像使用JUnit測試的方式來追蹤測試涵蓋率。

測試JavaFX應用程式,TestFX是相當不錯的測試框架,簡單、直覺,讓初學者可以快速上手,此外,提供簡潔的API讓測試易於理解,當測試失敗時,能更容易找出錯誤。

未通過的測試

單元測試的另一個重點,特別是當您在測試使用者介面時,是當測試未通過時,能提供什麼資訊。未通過的測試必須是可以被解譯、理解且容易重現,因此,重要的是,透過清楚的驗證才可以讓發生錯誤時明顯地看出什麼東西錯了。在TestFX中,Hamcrest matchers相當有用,因為它們能提供較易讀的驗證。從Listing 4可以看到TestFX測試的簡潔與Hamcrest matchers驗證的使用方式。

Listing 4.
@Test
public void testMultiplication() {
    Button two = find("#two");
    Button times = find("#times");
    Button three = find("#three");
    Button equalSign = find("#equal");
    click(two);
    click(times);
    click(three);
    verifyThat("#display", hasText("2x3"));
    click(equalSign);
    verifyThat("#display", hasText("6.0"));
}

Listing 4中的測試期望結果為6,透過修改controller的乘法運算來模擬臭蟲:將兩個運算元相乘後再加1,這個臭蟲用來說明TestFX如何處理錯誤,由於這個故意埋入的臭蟲,導致測試未通過,對應的堆疊軌跡顯示如下:

testMultiplication(...CalculatorControllerTest)
    Time elapsed: 2.434 sec <<< FAILURE!
java.lang.AssertionError:
Expected: Node should have label "6.0"
     but: Label was "7.0" Screenshot saved as /home/…/screenshot1436949687849.png
 at ...testfx.Assertions
             .verifyThat(Assertions.java:38)
 at ...testfx.Assertions
             .verifyThat(Assertions.java:26)
 at ...
...
Caused by: java.lang.AssertionError:
Expected: Node should have label "6.0"
     but: Label was "7.0"
  at org.hamcrest.MatcherAssert
        .assertThat(MatcherAssert.java:20)
  at org.loadui.testfx.Assertions
        .verifyThat(Assertions.java:33)
... 35 more

這(簡略的)堆疊軌跡可以提供我們清楚的資訊:哪一行程式驗證失敗了與失敗的條件,同時提供對應的螢幕截圖儲存位置。

限制

儘管TestFX是測試JavaFX應用程式相當有用的專案,但它仍然有些您需要注意的限制。第一個重大的限制是很難對TestFX撰寫的測試進行除錯(debugging),TestFX不允許在測試執行中移動滑鼠,由於TestFX無法回應中斷點,在除錯模式執行測試的過程中,如果您想在NetBeans畫面中觀察變數的值,會因為移動滑鼠導致測試中止。

還有一個主要的限制是當使用CSS修改元件外觀,就如同我對範例計算機的按鈕所做的一樣,套用風格後的元件有二個辨識碼:controller ID與CSS ID。這個版本的TestFX會搜尋id而不是fx:id,因此,它會使用CSS ID而不是controller ID,因為TestFX不會找到對應的按鈕而導致錯誤,繞過這問題的方法是每一個元件使用不同的CSS ID,這意味必須個別為每個按鈕套用相同的漸層風格。

另一個限制是截圖為全螢幕截圖。TestFX擷取整個螢幕畫面包含其他運作中的視窗而不僅僅是待測試程式視窗,大多數情況下,其它視窗對還原未通過的測試是不重要的,全螢幕截圖反而提供太多資訊,而且可能洩漏測試者或開發者的隱私。除此之外,截圖這功能無法開啟或關閉:每次測試未通過都會截圖,即便有時候您不需要。

還有,最近TestFX的開發已經漸緩,TestFX早在2014年就進入4.0的pre-release開發。更多的主動開發者加入有助於確保這相當有用的軟體能有後續版本。

結論

測試JavaFX應用程式,TestFX是相當不錯的測試框架,簡單、直覺,讓初學者可以快速上手,此外,提供簡潔的API讓測試易於理解,當測試失敗時,能更容易找出錯誤。然而,使用目前版本的TestFX時要記住:它仍存在一些限制。

LEARN MORE
Download the calculator
Video lecture on TestFX, with additional usage details
Gluon Scene Builder 8.0.0

譯者的告白
幾次信件的往返,取得Java Magazine編輯的同意,可以把喜歡的文章翻譯成中文後放在自己的部落格,這篇文章是首次嘗試,希望翻譯的品質別砸了自己與人家的招牌!我會陸續翻譯覺得有趣的文章。

這次翻譯發現:中英文在斷句與分段上其實不太一樣。若保留原有段落,有些段落會超級短,所以後來有進行些微的段落調整。另外,英文每一句的結尾都是句點,但翻成中文時不全是句點,有時候反而應該用逗點,這是我第一次翻這麼長的英翻中(過去考試大多是中翻英,念原文書時只需知道意思就好,也沒有特別想該用句點還是逗點),所以逗句都需要思考一下。最後是英文某些句子的被動形式與子句在翻成中文時,若完整照翻反而讀起來不通順,所以某些被動式會被轉成主動式,子句也會調整,盡可能讓中文讀起來通順。若有人覺得翻譯錯誤或是錯字,歡迎告知我。

← CodeFights近日感想 JCommander:解析命令列有更好的方法 →