LoginSignup
7
12

More than 5 years have passed since last update.

TestFX で JavaFX アプリケーションのテストを書く

Posted at

概要

JavaFX 用のテストライブラリ TestFX を使って JavaFX のコードをテストする方法について述べます。


TestFX とは

JavaFX でテストを実装するためのライブラリです。このライブラリを使うと、ユニットテストだけでなく UI テストも実装できます。ライセンスは EUPL v1.1 です。
今回は4系を使ってみることにします。この記事を書いている2017年01月時点では 4.0.5-alpha が最新です。

なぜライブラリを使うのか?

通常の JUnit を使って JavaFX アプリケーションや JavaFX のクラスを含むテストを実装して実行すると、下記のように java.lang.ExceptionInInitializerError が出てしまいます。

StackTrace
java.lang.ExceptionInInitializerError
    at sun.reflect.GeneratedSerializationConstructorAccessor2.newInstance(Unknown Source)
    at java.lang.reflect.Constructor.newInstance(Unknown Source)
    at org.objenesis.instantiator.sun.SunReflectionFactoryInstantiator.newInstance(SunReflectionFactoryInstantiator.java:40)
    at org.objenesis.ObjenesisBase.newInstance(ObjenesisBase.java:59)
    at org.mockito.internal.creation.jmock.ClassImposterizer.createProxy(ClassImposterizer.java:128)

…中略…

    at org.mockito.Mockito.mock(Mockito.java:1120)
    at jp.toastkid.loto6.Loto6ControllerTest.setUpTarget(Loto6ControllerTest.java:38)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

…中略…

    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Caused by: java.lang.IllegalStateException: Toolkit not initialized
    at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:273)

…中略…

    at javafx.scene.control.Control.<clinit>(Control.java:87)
    ... 37 more

Caused by: java.lang.IllegalStateException: Toolkit not initialized とのことです。 How do you unit test a JavaFX controller with JUnit に書いてあるやり方を使えば実装できるらしいですが、ちょっと複雑ですね……ということで、すでにあるライブラリを使うことにしました。

環境その他

今回の実装と確認は下記の環境で実施しました。

Java SE 1.8.0_102
OS Windows 10
Eclipse 4.5
JFoenix 1.0.0
JUnit 4.11
Mockito 1.9.5
TestFX 4.0.5-alpha

ライブラリの導入

Gradle の場合、dependencies に testCompile の依存を1行追加するだけで OK です。

build.gradleの修正
    testCompile group: 'org.testfx', name: 'testfx-junit', version: '4.0.5-alpha'

あるいは、こちらの形式でも大丈夫です。

build.gradleの修正
    testCompile 'org.testfx:testfx-junit:4.0.5-alpha'

Gradle 以外のビルドツールをお使いの場合は mvnrepository.com でご確認ください。

TestFX の依存ライブラリ

Guava 等が依存として含まれています。

dependencies
org.testfx:testfx-junit:4.0.5-alpha
     \--- org.testfx:testfx-core:4.0.5-alpha
          +--- com.google.guava:guava:20.0
          +--- org.hamcrest:hamcrest-core:1.3
          \--- com.google.code.findbugs:annotations:3.0.1u2
               +--- net.jcip:jcip-annotations:1.0
               \--- com.google.code.findbugs:jsr305:3.0.1

テストの実装

テストクラスで org.testfx.framework.junit.ApplicationTest クラスを継承します。3系までは GuiTest という名前だったそうです。4系では ApplicationTest という名前に変わっています。このクラスは public void start(Stage stage) throws Exception メソッドを実装する必要があります。
TestFX では Controller クラスに対してテストを書いていくスタイルが主になります。

今回は以前私が作成した、スクリプトを動かす JavaFX アプリケーション にテストを実装してみます。

testfx_ss1.png

このような画面を持っています。ユーザの入力した Groovy のコードを ScriptEngine で実行し、run と書いてある Button をクリックすると、スクリプトの実行結果を表示する、という機能を持っています。画面の左側がユーザからコードの入力を受け付ける CodeArea(TextArea) で、左側がその実行結果を表示する CodeArea(TextArea) です。

start(Stage stage)メソッドの実装

このメソッド内で GUI の初期化と表示の実行をします。 @Before をつけたメソッドよりも先に実行されます。今回は Controller を呼び出す側のクラスの実装に則って、画面表示前の初期化処理を記述してみます。

Test class
@Override
public void start(final Stage stage) throws Exception {
    try {
        final FXMLLoader loader
            = new FXMLLoader(getClass().getClassLoader().getResource("scenes/Main.fxml"));
        final VBox loaded = (VBox) loader.load();
        controller = (Controller) loader.getController();
        final Scene scene = new Scene(loaded);
        stage.setScene(scene);
    } catch (final IOException e) {
        LOGGER.error("Scene Reading Error", e);
    }
    stage.show();
}

上記の通り、FXML をロードして Controller を取得し、 Scene と Stage を初期化して、最後に stage を表示しています。

空のテストメソッド

JUnit の Runner を動かすために空のテストメソッドも用意しておきます。

空のテストメソッド
@Test
public void test() {
    // NOP.
}

実行

JUnit のテストを実行すると、下記のようにアプリケーションの画面が表示されます。まだ他のテストメソッドが実装されていないため、一瞬だけ表示され、すぐに消えます。

testfx_ss1.png

今回テストを書いていくアプリケーションについて軽く説明します。ユーザの入力した Groovy のコードを ScriptEngine で実行し、run と書いてある Button をクリックすると、スクリプトの実行結果を表示する、という機能を持っています。画面の左側がユーザからコードの入力を受け付ける CodeArea(TextArea) で、左側がその実行結果を表示する CodeArea(TextArea) です。

テストメソッドの追加

続いて、 UI テストのコードを追加していきましょう。

入力の受付

Groovy の Hello World を入力してみます。

左側のTextAreaにスクリプトを入力
@Test
public void test() throws InterruptedException {
    final CodeArea input = (CodeArea) lookup("#scripterInput").query();
    final String text = "println 'Hello world.'";
    Platform.runLater(() -> input.replaceText(text));
}

Node の Lookup

表示中の Scene に含まれている Node は ID を使って Lookup することが可能です。ID は FXML 中で定義したり、Node クラスの setId で指定することが可能です。下記に FXML での定義例を示します。

FXMLでの定義例
<CodeArea fx:id="scripterInput" prefHeight="450.0" prefWidth="500.0">

scripterInput という名前の ID を持つ Node を Lookup し、実装クラスである CodeArea にキャストして、以後のテストで利用します。

Lookup->cast
final CodeArea input = (CodeArea) lookup("#scripterInput").query();

JUnit のテストクラス上では、 JavaFX の Control に値をセットする際、 Platform.runLater を使わないと失敗します。

Platform.runLater(() -> input.replaceText(text));

実行

テストを実行すると、下記の通りスクリプトが入力された状態で画面が表示されます。

testfx_ss2.png

UI イベントの実行

run と書いてある Button を Lookup で取得し、設定されているイベントを実行させる、というコードは下記の1行で書けます。

Lookup->query->fireEvent
Platform.runLater(() -> lookup("#runButton").query().fireEvent(new ActionEvent()));

run Button には Controller クラスの runScript というメソッドを実行させるように FXML で指定されているので、そのメソッドが実行され、画面右側の CodeArea(TextArea) にスクリプトの実行結果である Hello world. が表示されます。

testfx_ss3.png

値の検査

あとは assert メソッドで値を検査するだけです。

最終的なテストコード
@Test
public void test() throws InterruptedException {
    final CodeArea input = (CodeArea) lookup("#scripterInput").query();
    final String text = "println 'Hello world.'";
    Platform.runLater(() -> {
        input.replaceText(text);
        assertEquals(text, input.getText());
        lookup("#runButton").query().fireEvent(new ActionEvent());
        assertEquals(
            "Hello world.",
            ((CodeArea) lookup("#scripterOutput").query()).getText().trim()
        );
    });
}

注意点

Platform.runLater の中と外ではメソッドの実行が非同期なので、 Platform.runLater で実施した Node に対する変更で生じた Node の値を検査したい場合は Platform.runLater の中で書く必要があります。

失敗例として、下記のコードでは assertEquals での検査が Platform.runLater 内の input.replaceText(text) よりも先に実行されるので必ず失敗します。

検査が失敗するケース
Platform.runLater(() -> input.replaceText(text));
assertEquals(text, input.getText());

Mock を使ったテスト

TestFX の Lookup を使わなくとも、 Mockito の Whitebox を使って同じコードを書くこともできます(注:Controller にそれぞれの Node オブジェクトを持たせる必要があります)。

@Test
public void test_mockito() throws InterruptedException {
    final CodeArea input = (CodeArea) Whitebox.getInternalState(controller, "scripterInput");
    final String text = "println 'Hello world.'";
    Platform.runLater(() -> {
        input.replaceText(text);
        assertEquals(text, input.getText());
        ((Button) Whitebox.getInternalState(controller, "runButton")).fireEvent(new ActionEvent());
        assertEquals(
            "Hello world.",
            ((CodeArea) Whitebox.getInternalState(controller, "scripterOutput")).getText().trim()
        );
    });
}

Mockito を使うと、画面を表示せずにテストを実行できるというメリットがあります。毎回画面が表示されて鬱陶しいという方にはこちらの方法をお勧めします。


まとめ

以上、 TestFX を用いて JavaFX アプリケーションのテストを書く方法について簡単に説明いたしました。テスト実行の度に画面が表示されると鬱陶しいのではないかと思われるかもしれません。鬱陶しいです。画面を表示せずにテストしたいのであれば Mock を使ってテストを組んでいく方がよいでしょう。その場合でも TestFX を使うと JavaFX のスレッド絡みの部分を自分で操作する必要がないので、テストは書きやすくなります。
今回は紹介しませんでしたが、自作した JavaFX のコンポーネントに単体テストを書きたい場合にも、この TestFX を使えば可能です。


参考

記事

TestFX については英語・日本語で多くの情報がすでにあります。

英語

日本語

3系の情報が多めです。

コード

7
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
12