prmr / JetUML

A desktop application for fast UML diagramming.
GNU General Public License v3.0
619 stars 121 forks source link

JavaFX GUI Unit Testing #513

Closed ArthusWQZ closed 7 months ago

ArthusWQZ commented 9 months ago

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:

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.

prmr commented 7 months ago

Partially implemented as part of #508