概要
JavaFX 用のテストライブラリ TestFX を使って JavaFX のコードをテストする方法について述べます。
TestFX とは
JavaFX でテストを実装するためのライブラリです。このライブラリを使うと、ユニットテストだけでなく UI テストも実装できます。ライセンスは EUPL v1.1 です。
今回は4系を使ってみることにします。この記事を書いている2017年01月時点では 4.0.5-alpha
が最新です。
なぜライブラリを使うのか?
通常の JUnit を使って JavaFX アプリケーションや JavaFX のクラスを含むテストを実装して実行すると、下記のように java.lang.ExceptionInInitializerError
が出てしまいます。
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 です。
testCompile group: 'org.testfx', name: 'testfx-junit', version: '4.0.5-alpha'
あるいは、こちらの形式でも大丈夫です。
testCompile 'org.testfx:testfx-junit:4.0.5-alpha'
Gradle 以外のビルドツールをお使いの場合は mvnrepository.com でご確認ください。
TestFX の依存ライブラリ
Guava 等が依存として含まれています。
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 アプリケーション にテストを実装してみます。
このような画面を持っています。ユーザの入力した Groovy のコードを ScriptEngine で実行し、run と書いてある Button をクリックすると、スクリプトの実行結果を表示する、という機能を持っています。画面の左側がユーザからコードの入力を受け付ける CodeArea(TextArea) で、左側がその実行結果を表示する CodeArea(TextArea) です。
start(Stage stage)メソッドの実装
このメソッド内で GUI の初期化と表示の実行をします。 @Before をつけたメソッドよりも先に実行されます。今回は Controller を呼び出す側のクラスの実装に則って、画面表示前の初期化処理を記述してみます。
@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 のテストを実行すると、下記のようにアプリケーションの画面が表示されます。まだ他のテストメソッドが実装されていないため、一瞬だけ表示され、すぐに消えます。
今回テストを書いていくアプリケーションについて軽く説明します。ユーザの入力した Groovy のコードを ScriptEngine で実行し、run と書いてある Button をクリックすると、スクリプトの実行結果を表示する、という機能を持っています。画面の左側がユーザからコードの入力を受け付ける CodeArea(TextArea) で、左側がその実行結果を表示する CodeArea(TextArea) です。
テストメソッドの追加
続いて、 UI テストのコードを追加していきましょう。
入力の受付
Groovy の Hello World を入力してみます。
@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 での定義例を示します。
<CodeArea fx:id="scripterInput" prefHeight="450.0" prefWidth="500.0">
scripterInput
という名前の ID を持つ Node を Lookup し、実装クラスである CodeArea にキャストして、以後のテストで利用します。
final CodeArea input = (CodeArea) lookup("#scripterInput").query();
JUnit のテストクラス上では、 JavaFX の Control に値をセットする際、 Platform.runLater
を使わないと失敗します。
Platform.runLater(() -> input.replaceText(text));
実行
テストを実行すると、下記の通りスクリプトが入力された状態で画面が表示されます。
UI イベントの実行
run と書いてある Button を Lookup で取得し、設定されているイベントを実行させる、というコードは下記の1行で書けます。
Platform.runLater(() -> lookup("#runButton").query().fireEvent(new ActionEvent()));
run Button には Controller クラスの runScript というメソッドを実行させるように FXML で指定されているので、そのメソッドが実行され、画面右側の CodeArea(TextArea) にスクリプトの実行結果である Hello world.
が表示されます。
値の検査
あとは 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系の情報が多めです。