This solution is based on a JUnit4 Runner. The new runner class extends the BlockJUnit4ClassRunner which is the default runner for JUnit tests. If you also need the spring support then just extend the SpringJUnit4ClassRunner instead. Both runners can co-exist if necessary because they both use the JavaFxJUnit4Application class to guarantee a single JavaFx instance.
JavaFxJUnit4ClassRunner
Runs all the unit tests in the JavaFX Thread.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;
/**
* This basic class runner ensures that JavaFx is running and then wraps all the runChild() calls
* in a Platform.runLater(). runChild() is called for each test that is run. By wrapping each call
* in the Platform.runLater() this ensures that the request is executed on the JavaFx thread.
*/
public class JavaFxJUnit4ClassRunner extends BlockJUnit4ClassRunner
{
/**
* Constructs a new JavaFxJUnit4ClassRunner with the given parameters.
*
* @param clazz The class that is to be run with this Runner
* @throws InitializationError Thrown by the BlockJUnit4ClassRunner in the super()
*/
public JavaFxJUnit4ClassRunner(final Class<?> clazz) throws InitializationError
{
super(clazz);
JavaFxJUnit4Application.startJavaFx();
}
/**
* {@inheritDoc}
*/
@Override
protected void runChild(final FrameworkMethod method, final RunNotifier notifier)
{
// Create a latch which is only removed after the super runChild() method
// has been implemented.
final CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(new Runnable()
{
@Override
public void run()
{
// Call super to actually do the work
JavaFxJUnit4ClassRunner.super.runChild(method, notifier);
// Decrement the latch which will now proceed.
latch.countDown();
}
});
try
{
latch.await();
}
catch (InterruptedException e)
{
// Waiting for the latch was interruped
e.printStackTrace();
}
}
}
JavaFxJUnit4Application
Manages starting JavaFX. Uses a static flag to make sure that FX is only started once.Manages starting JavaFX. Uses a static flag to make sure that FX is only started once and a LOCK to make sure that multiple calls will pause if the JavaFX thread is in the process of being started. This improves on the previous version because it doesn't involve a sleep.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.stage.Stage;
/**
* This is the application which starts JavaFx. It is controlled through the startJavaFx() method.
*/
public class JavaFxJUnit4Application extends Application
{
/** The lock that guarantees that only one JavaFX thread will be started. */
private static final ReentrantLock LOCK = new ReentrantLock();
/** Started flag. */
private static AtomicBoolean started = new AtomicBoolean();
/**
* Start JavaFx.
*/
public static void startJavaFx()
{
try
{
// Lock or wait. This gives another call to this method time to finish
// and release the lock before another one has a go
LOCK.lock();
if (!started.get())
{
// start the JavaFX application
final ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new Runnable()
{
@Override
public void run()
{
JavaFxJUnit4Application.launch();
}
});
while (!started.get())
{
Thread.yield();
}
}
}
finally
{
LOCK.unlock();
}
}
/**
* Launch.
*/
protected static void launch()
{
Application.launch();
}
/**
* An empty start method.
*
* @param stage The stage
*/
@Override
public void start(final Stage stage)
{
started.set(Boolean.TRUE);
}
}
Sample Test
A sample test class which instantiates a Scene object. This will fail if it isn't on the JavaFX thread. It can be shown to fail if the @RunWith annotation is removed.
/**
* This is a sample test class for java fx tests.
*/
@RunWith(JavaFxJUnit4ClassRunner.class)
public class ApplicationTestBase
{
/**
* Daft normal test.
*/
@Test
public void testNormal()
{
assertTrue(true);
}
/**
* Test which would normally fail without running on the JavaFX thread.
*/
@Test
public void testNeedsJavaFX()
{
Scene scene = new Scene(new Group());
assertTrue(true);
}
}
JavaFxJUnit4Application - Previous version
Manages starting JavaFX. Uses a static flag to make sure that FX is only started once.import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.stage.Stage;
/**
* This is the application which starts JavaFx. It is controlled through the startJavaFx() method.
*/
public class JavaFxJUnit4Application extends Application
{
/**
* Flag stating if javafx has started. Static so that it
* is shared across all instances.
*/
private static boolean started;
/**
* Start JavaFx.
*/
static void startJavaFx()
{
if (started)
{
return;
}
started = true;
/**
* The executor which starts JavaFx.
*/
final ExecutorService executor = Executors.newSingleThreadExecutor();
// Start the java fx application
executor.execute(new Runnable()
{
@Override
public void run()
{
JavaFxJUnit4Application.launch();
}
});
// Pause briefly to give FX a chance to start
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
/**
* Launch.
*/
static void launch()
{
Application.launch();
}
/**
* An empty start method.
*
* @param stage The stage
*/
@Override
public final void start(final Stage stage)
{
// Empty
}
}
Superb info, thanks so much for the near copy & paste details on how to write JFX unit tests, exactly what I wanted!!
ReplyDeleteThanks for this great post, I modified the runner and deployed it on GitHub. Feed free to join the mini project :-)
ReplyDeletehttp://blog.buildpath.de/javafx-testrunner/
It Works Great! Thanks
ReplyDeleteWorks Great! Thanks
ReplyDelete