torutkのブログ

ソフトウェア・エンジニアのブログ

JavaFXの自動テストツールTestFXを始める第1歩

JavaFX Advent Calendar 2013の16日目のエントリーです。
前日のエントリーはYasuyuki Fukaiさんの3D初心者がJavaFXの3Dを使い初めてみた。 : 人生、気合いと具合 - blog、翌日のエントリーはpeko_kunさんです。

はじめに

GUIの部分をユニットテストしようとすると、通常は人がGUIを操作して結果を判定するので、ロジックのユニットテストのように自動化することが困難です。
JavaのSwingでは、FEST-SwingというGUI自動化テストツールがありました。JavaFXでは何かないかと探していたところ、

というのがひっかかりました。JemmyFXは、OpenJFXプロジェクトのテストツールでもありますが、以前試してみたときはうまく使えませんでした。
TestFXは、JavaOneサンフランシスコ2013のセッション「Ten Man-Years of JavaFX: Real-World Project Experiences(CON2670)」で紹介*1されたツールで、JemmyFXより簡単という宣伝があり、また最近JavaFX 8対応したとのツイットが流れています。
MarvinFXは情報が少なく、またリポジトリ更新が8ヶ前で止まっているようです。

そこで、今回はJavaFX 8対応をしたTestFX(TestFX 8)を試してみます。使用した環境は、

  • JDK 8 b120
  • NetBeansは、7.4
    • NetBeans 7.4自身はJDK 7u45で実行し、作成するアプリケーションのプロジェクト設定でJavaプラットフォームをJDK 8に指定

TestFX 8の入手とビルド

TestFX 8のプロジェクト(ブランチ)ページのURLは次となります。
https://github.com/SmartBear/TestFX/tree/java8

TestFXのリポジトリGitHubで管理されており、プロジェクトのURLは次になります。
https://github.com/SmartBear/TestFX

JavaFX 8用のTestFXはブランチjava8で管理されています。そこで、次のURLを指定してgit cloneし、ブランチをデフォルトのmasterからjava8に切り替えます。
https://github.com/SmartBear/TestFX.git

TestFXのソース一式にはmavenでビルドする設定が含まれています。NetBeans 7.4からmavenプロジェクトをそのまま(インポートして変換といった作業なしに)使うことができるようになったので、NetBeans上でTestFX 8のディレクトリ(mavenプロジェクト)を開き、ビルドを実施しました*2

ビルドが成功すると、targetディレクトリの下にtestFx-3.0.0-beta1.jarが生成されます。

次にTestFXが依存するライブラリがいくつかあるので入手します。TestFXのpom.xmlを開いて、必要なライブラリを確認します。要素の中の個々のに定義されています。

    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>14.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-all</artifactId>
            <version>1.3</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
            <exclusions>
                <exclusion>
                    <groupId>org.hamcrest</groupId>
                    <artifactId>hamcrest-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

NetBeansの場合、JUnitはテストを作成すると自動で依存に含まれるので、guavaとhamcrest-allを入手します。といってもmavenでビルドしたときにユーザーのホームディレクトリ下に必要なライブラリがダウンロードされ保存されているので、そこから取り出します。
Windows 7でユーザーtorutkの場合、次のディレクトリにあります。

C:\Users\torutk\.m2\repository\com\google\guava\guava\14.0.1\guava-14.0.1.jar
C:\Users\torutk\.m2\repository\org\hamcrest\hamcrest-all\1.3\hamcrest-all-1.3.jar

今回は、次のディレクトリにTestFXを使うのに必要なjarファイルを集めました。

C:\java\TestFX
  +-- testFx-3.0.0-beta1.jar
  +-- guava-14.0.1.jar
  +-- hamcrest-all-1.3.jar

この3つのJARファイルを、NetBeansのプロジェクト設定でテスト・ライブラリに追加します。


  • プロジェクト名がJavaFXApplication4になっていますが、NetBeansが生成したデフォルトのままいじっていないためです。横着が見えてしまいました。

最初のテストへの道

まず、GUIテストの対象となるプログラムを用意します。ここでは、NetBeansJavaFX FXMLアプリケーションを新規作成して生成されるデフォルトのプログラムを使用します。

NetBeansJavaFX FXMLアプリケーション種類を選択して自動生成されるプログラムの実行直後の画面を次に示します。

ボタンをクリックした後の画面を次に示します。

NetBeansJavaFX FXMLアプリケーションプロジェクトを新規作成したときに生成されるソースコードは次の3つのファイルです。

  • JavaFXApplication.java
  • FXMLDocument.fxml
  • FXMLDocumentController.java

それぞれのソースファイルの内容は次となります(コメント、import文等を省略)。

public class JavaFXApplication6 extends Application {
    
    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));      
        Scene scene = new Scene(root);        
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    } 
}
<AnchorPane id="AnchorPane" prefHeight="200" prefWidth="320" xmlns:fx="http://javafx.com/fxml"
            fx:controller="javafxapplication6.FXMLDocumentController">
    <children>
        <Button layoutX="126" layoutY="90" text="Click Me!" onAction="#handleButtonAction" fx:id="button" />
        <Label layoutX="126" layoutY="120" minHeight="16" minWidth="69" fx:id="label" />
    </children>
</AnchorPane>
public class FXMLDocumentController implements Initializable {
    @FXML
    private Label label;
    
    @FXML
    private void handleButtonAction(ActionEvent event) {
        System.out.println("You clicked me!");
        label.setText("Hello World!");
    }
    
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // TODO
    }       
}

TestFXのテストコードは、JUnitのテストケースとして生成します。
NetBeansでは、Javaのソースファイル(今回はJavaFXApplication4.java)を選択し右クリックでポップアップするメニューから[ツール]>[テストを作成]で生成します。

生成したら、TestFXのGUIテストを実行するため、GuiTestクラスを継承させます。

import org.loadui.testfx.GuiTest;

public class JavaFXApplication4Test extends GuiTest {
    :

GuiTestクラスは抽象クラスで、抽象メソッドNode getRootNode()を実装する必要があります。このメソッドはテスト対象となるJavaFXのノードを返す処理を記述します。

public class JavaFXApplication4Test extends GuiTest {
    :
    @Override
    protected Parent getRootNode() {
        // テスト対象のノードを作成して返却する処理
    }
    :

ここで、NetBeans で生成されるデフォルトのクラス(というか、JavaFXのプログラムの単純な記述方法)では、Application派生クラスのstartメソッド内でシーングラフを作成しstageにセットしているので、テストケース側でノード(シーングラフ)を取得することができません。
そこで、ノード(シーングラフ)を作成するstartメソッド内のコードをApplication派生クラスでデフォルトアクセスのメソッドとして切り出します。

言葉で書くと面倒そうですが、やっていることは次のコードです。

public class JavaFXApplication4 extends Application {
    
    @Override
    public void start(Stage stage) throws Exception {
        Parent root = getRoot();
        
        Scene scene = new Scene(root);
        
        stage.setScene(scene);
        stage.show();
    }

    Parent getRoot() throws IOException {
        Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));
        return root;
    }
    :

これで、テストケース側でノードを取得することができるようになりました。

public class JavaFXApplication4Test extends GuiTest {
    :
    @Override
    protected Parent getRootNode() {
        JavaFXApplication4 app = new JavaFXApplication4();
        try {
            return app.getRoot();
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }
    :

GuiTestのgetRootNodeでは、Application派生クラスをnewして、先ほど実装したgetRootメソッドを呼び、それを返却しています。

なお、FXMLをロードするメソッドがIOExceptionをスローするのでtry-catchを必要としますが、getRootNodeは例外をスローしないシグニチャのため、Java SE 8から新たに加わったUncheckedIOExceptionを使ってチェック例外を非チェック例外に変換しています*3

では、やっと本題のテストメソッド、ボタンを押したらラベルに所定の文字列が表示されるテストを記述します。

    @Test
    public void ボタンを押すとラベルにメッセージが表示されること() {
        click("Click Me!"); // (1)
        assertThat("#label", hasText("Hello World!")); //(2)
    }

(1)は、テキスト属性に"Click Me!"を持つノードを見つけてマウスクリックをするコードです。
(2)は、idに"label"とついたノードを見つけて、テキスト属性が"Hello World!"であるかを検査するメソッドです。idは、FXMLではfx:idに指定した値になります。

テストの実行です。JUnitのテスト実行でテストを実行すると、このアプリケーションのノードが表示された画面が表示され、自動でマウスクリックされラベルにメッセージが表示され、テストが終了すると画面が消えます。

あっけないので、本当にテストが動いているのか確認するため、assertThat文のメッセージを変更してテストを失敗させてみます。

    @Test
    public void ボタンを押すとラベルにメッセージが表示されること() {
        click("Click Me!"); // (1)
        assertThat("#label", hasText("World!")); //(2)
    }

これでテストを実行すると、JUnitテストが失敗したと表示されました。
うん、テストはちゃんと動いているようです。

なお、(1)をidで指定することもできます。

        click("#button"); /// (1')

ドキュメント

TestFXのドキュメントはあまりなく、以下のURLに簡単な例が載っているくらいです。
https://github.com/SmartBear/TestFX/wiki/Documentation

このドキュメントに、具体例(実際の例)を見たければ、loadui(TestFXでテストを実施している本格的なアプリケーション)のリポジトリを見よ、とあります。

さいごに

TestFXは、なかなか使えそうなツールです。なにより記述がシンプルです。

最初、TestFX 8でない方のリポジトリをとってきて、TestFXのビルドができなかったり、最初の肝心なところ(getRootNode)がなにをすればいいか分からず詰まっていたりしましたが、JavaFX Advent Calendarに書くという締め切りありのプレッシャーからなんとか動かすことができました。

*1:青江さんのJavaOne参加レポート記事に言及あります。

*2:コマンドで実行する場合、mvnとと叩いて実行するだけではエラーになりました。ターゲットを指定する必要がありますが、普段mavenを使っていないのでターゲットって何?となりNetBeansでビルドしてしまいました。

*3:単にJava SE 8で加わった新機能を使ってみたかっただけです。