なみひらブログ

学んだことを日々記録する。~ since 2012/06/24 ~

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 Testing

java - How to load image from static method - Stack Overflow

Application (JavaFX 2.2)