JavaFX関連クラスについてJUnitテストを実施するための準備
背景
JavaFXのクラスを使った関数を実装した際に、その関数も単体テスト(JUnit)を行う必要があります。そのテスト実行の際に事前準備が必要だったので、そのことについて記載します。前提
今回、以下のようなクラスを考えます。JavaFXパッケージのクラスを使ったユーティリティクラスです。import javafx.scene.image.Image; import javafx.scene.paint.Color; /** * 画像に関するユーティリティクラス */ public class ImageUtils { /** * 指定されたImegeオブジェクトについて、Color配列に変換します。 * * @param in Imageオブジェクト * @return Color配列 * * @throws NullPointerException 引数がnullの場合。 */ public static Color[][] toArrays(final Image in) { if (in == null) { throw new NullPointerException("in must not be null"); } final int width = (int) in.getWidth(); final int height = (int) in.getHeight(); Color[][] out = new Color[height][width]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { out[y][x] = in.getPixelReader().getColor(x, y); } } return out; } }
問題点
上記の関数について、以下のような単体テストを書きます。import javafx.scene.image.Image; import javafx.scene.paint.Color; public class ImageUtilsTest { @Test public void test_toArrays() { // prepare Image image = new Image("black.png"); // action Color[][] result = ImageUtils.toArrays(image); // check final int width = (int) image.getWidth(); final int height = (int) image.getHeight(); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { assertEquals(image.getPixelReader().getColor(x, y), result[y][x]); } } } }
テスト実行します。
java.lang.RuntimeException: Internal graphics not initialized yet at com.sun.glass.ui.Screen.getScreens(Screen.java:75) at com.sun.javafx.tk.quantum.QuantumToolkit.getScreens(QuantumToolkit.java:637) at com.sun.javafx.tk.quantum.QuantumToolkit.getMaxPixelScale(QuantumToolkit.java:652) at com.sun.javafx.tk.quantum.QuantumToolkit.loadImage(QuantumToolkit.java:660) at javafx.scene.image.Image.loadImage(Image.java:1041) at javafx.scene.image.Image.initialize(Image.java:785) at javafx.scene.image.Image.<init>(Image.java:599) at jp.co.namihira.java8.ImageUtilsTest.test_toArrays(ImageUtilsTest.java:19) (以下、略)
例外が発生しテスト実行できなかった(;´Д`)Imageクラス関連でloadしようとして、初期化ができていないようです。
java - How to load image from static method - Stack Overflow
を参照したら、
once you are sure that the JavaFX toolkit has been appropriately initialized for your application (for example once your application's init or start methods have been invoked or your JFXPanel created).
と書いてあったので、JavaFX関連のクラスを使う際には必ずJavaFXアプリとして初期化(Applicationクラスのinit()またはstart())する必要があるそうです。
解決策
以下の記事に書かれているやり方で解決しました。IT Tips and Memory Joggers!: JavaFX JUnit Testing
JUnit実行時に別スレッドを立てて、そのスレッドでJavaFXの処理化を行うようです。
テストクラス
テストランナー(後述)を指定し、テスト実行するようにします。import static org.junit.Assert.*; import javafx.scene.image.Image; import javafx.scene.paint.Color; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(JavaFxJUnit4ClassRunner.class) public class ImageUtilsTest { @Test public void test_toArrays() { // prepare Image image = new Image("black.png"); // action Color[][] result = ImageUtils.toArrays(image); // check final int width = (int) image.getWidth(); final int height = (int) image.getHeight(); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { assertEquals(image.getPixelReader().getColor(x, y), result[y][x]); } } } }
JavaFxJUnit4ClassRunner.java
各単体テスト実行の前に、JavaFX初期化のためのスレッドを作って実行する。import java.util.concurrent.CountDownLatch; import javafx.application.Platform; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; public class JavaFxJUnit4ClassRunner extends BlockJUnit4ClassRunner { /** * コンストラクタ */ public JavaFxJUnit4ClassRunner(final Class<?> clazz) throws InitializationError { super(clazz); // メインスレッドでJavaFXアプリをスタートする。 JavaFxJUnit4Application.startJavaFx(); } /** * 各テストケースを実行する */ @Override protected void runChild(final FrameworkMethod method, final RunNotifier notifier) { // テスト実行スレッドの管理オブジェクトを作成する。 final CountDownLatch latch = new CountDownLatch(1); // JavaFX管理上のスレッドでテストケースを実行する。 Platform.runLater(() -> { // テストケースを実行する super.runChild(method, notifier); // スレッド管理オブジェクトをデクリメントする latch.countDown(); }); // テストケースが終わるまで待つ。 try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } }
JavaFxJUnit4Application.java
初期化のために実行されるJavaFXアプリケーションimport java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; import javafx.application.Application; import javafx.stage.Stage; public class JavaFxJUnit4Application extends Application { // JUnitの複数スレッド対策用Lockオブジェクト private static final ReentrantLock LOCK = new ReentrantLock(); // JavaFXアプリがスタートしたか(JavaFXの初期化が完了したか)のフラグ private static AtomicBoolean started = new AtomicBoolean(); public static void startJavaFx(){ try { LOCK.lock(); if (!started.get()) { // JavaFX初期化用のスレッドワーカー作成 final ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(() -> JavaFxJUnit4Application.launch()); // JavaFX初期化完了まで待つ。 while (!started.get()) { Thread.yield(); } } } finally { LOCK.unlock(); } } protected static void launch() { Application.launch(); } @Override public void start(final Stage stage) { started.set(Boolean.TRUE); } }
まとめ
このテストランナーを取り込めば、JavaFX関連のクラスについても通常(?)のJavaクラスのように単体テストが実行できます。※クラスやメソッドを使う際には事前条件はできる限り少なくして欲しい(;´Д`)教訓。
参考
IT Tips and Memory Joggers!: JavaFX JUnit Testingjava - How to load image from static method - Stack Overflow