GUI unit tests using JavaFX components should be carefully handled.
In fact, JavaFX uses a single-threaded rendering design, which means every GUI operations must be done in the JavaFX Application Thread. This is important since JUnit tests are not executed in the JavaFX Application Thread.
What does that mean?
When designing GUI-related unit tests, GUI operations must be executed explicitly in the JavaFX Application Thread, using Platform.runLater(Runnable pRunnable). However, JUnit assertions must not be executed in the JavaFX Application Thread.
Therefore, it is good practice to only use Platform.runLater() for GUI calls, and fit the rest of the logic in the test method, as one would usually do.
IMPORTANT NOTE:Platform.runLater() queues the Runnable in the event queue. This queue is responsible for what the Application Thread runs. Then, the code passed in the Runnable may not be run instantly. However, there is a workaround by calling this method right after the Platform.runLater() call:
public static void waitForRunLater() throws InterruptedException
{
Semaphore semaphore = new Semaphore(0);
Platform.runLater(() -> semaphore.release());
semaphore.acquire();
}
The semaphore.release() call will be following the Runnable in the event queue, then the execution of the test method will resume as soon as the Runnable has been run. This is crucial since omitting this detail can result in the test ending before the first Runnable was even run.
JavaFX Platform Initialization Issue
Before performing any Platform.runLater() call, the Platform itself should be initialized (otherwise the call will result in an IllegalStateException: Toolkit not initialized).
This can be done by two different ways:
Calling Platform.startup(Runnable pRunnable) first. Then using Platform.runLater() only. While this is ok for running a single test class, it can be a problem when running several test classes in a row, since Platform.start(Runnable pRunnable)can only be run once (otherwise results in an IllegalStateException: Toolkit already initialized).
Using the provided JavaFXLoader class by calling JavaFXLoader.load() once before running any test (this can be done in a static void method with the @BeforeAll JUnit annotation). This way is better than the other since it works whether a single test class is run or multiple ones.
JavaFX Platform Exiting Requirements
It is necessary to ensure that every GUI component is closed before the test run terminates. For instance, Stage objects can be closed with Stage#close(). Since the Platform implicitly exits at the end of the test run, an error can be print out to the console if some GUI components is not closed yet. This error does not seem to affect the quality of the test run, but it is good practice to avoid it.
Once again, GUI components can be closed after all tests have been run using a static void method with the @AfterAll JUnit annotation.
Possible solution to explore
Using the JUnit extension system, it would be possible to initialize the Platform before running the first test, and to explicitly close it (hence there would be no need to close every GUI component, since it would act as a force close) once all the tests from all test files have been run.
Then, every GUI-related test class would just need the @ExtendWith({Extension.class}) JUnit annotation to register as a JavaFX-dependent class requiring the JavaFX toolkit.
GUI unit tests using JavaFX components should be carefully handled.
In fact, JavaFX uses a single-threaded rendering design, which means every GUI operations must be done in the JavaFX Application Thread. This is important since JUnit tests are not executed in the JavaFX Application Thread.
What does that mean?
When designing GUI-related unit tests, GUI operations must be executed explicitly in the JavaFX Application Thread, using
Platform.runLater(Runnable pRunnable)
. However, JUnit assertions must not be executed in the JavaFX Application Thread.Therefore, it is good practice to only use
Platform.runLater()
for GUI calls, and fit the rest of the logic in the test method, as one would usually do.IMPORTANT NOTE:
Platform.runLater()
queues the Runnable in the event queue. This queue is responsible for what the Application Thread runs. Then, the code passed in the Runnable may not be run instantly. However, there is a workaround by calling this method right after thePlatform.runLater()
call:The
semaphore.release()
call will be following the Runnable in the event queue, then the execution of the test method will resume as soon as the Runnable has been run. This is crucial since omitting this detail can result in the test ending before the first Runnable was even run.JavaFX Platform Initialization Issue
Before performing any
Platform.runLater()
call, the Platform itself should be initialized (otherwise the call will result in anIllegalStateException: Toolkit not initialized
).This can be done by two different ways:
Platform.startup(Runnable pRunnable)
first. Then usingPlatform.runLater()
only. While this is ok for running a single test class, it can be a problem when running several test classes in a row, sincePlatform.start(Runnable pRunnable)
can only be run once (otherwise results in anIllegalStateException: Toolkit already initialized
).JavaFXLoader
class by callingJavaFXLoader.load()
once before running any test (this can be done in a static void method with the@BeforeAll
JUnit annotation). This way is better than the other since it works whether a single test class is run or multiple ones.JavaFX Platform Exiting Requirements
It is necessary to ensure that every GUI component is closed before the test run terminates. For instance,
Stage
objects can be closed withStage#close()
. Since the Platform implicitly exits at the end of the test run, an error can be print out to the console if some GUI components is not closed yet. This error does not seem to affect the quality of the test run, but it is good practice to avoid it.Once again, GUI components can be closed after all tests have been run using a static void method with the
@AfterAll
JUnit annotation.Possible solution to explore
Using the JUnit extension system, it would be possible to initialize the Platform before running the first test, and to explicitly close it (hence there would be no need to close every GUI component, since it would act as a force close) once all the tests from all test files have been run.
Then, every GUI-related test class would just need the
@ExtendWith({Extension.class})
JUnit annotation to register as a JavaFX-dependent class requiring the JavaFX toolkit.