Write tests for JavaFX applications with TestFX

Overview

Describes how to test JavaFX code using TestFX, a test library for JavaFX.


What is TestFX?

A library for implementing tests in JavaFX. You can use this library to implement UI tests as well as unit tests. The license is EUPL v1.1. This time I will try using 4 series. As of January 2017, when this article was written, 4.0.5-alpha is the latest.

Why use the library?

When I implement and run a test that includes a JavaFX application or JavaFX class using regular JUnit, I get `` `java.lang.ExceptionInInitializerError``` as shown below.

StackTrace


java.lang.ExceptionInInitializerError
	at sun.reflect.GeneratedSerializationConstructorAccessor2.newInstance(Unknown Source)
	at java.lang.reflect.Constructor.newInstance(Unknown Source)
	at org.objenesis.instantiator.sun.SunReflectionFactoryInstantiator.newInstance(SunReflectionFactoryInstantiator.java:40)
	at org.objenesis.ObjenesisBase.newInstance(ObjenesisBase.java:59)
	at org.mockito.internal.creation.jmock.ClassImposterizer.createProxy(ClassImposterizer.java:128)

... Omitted ...

	at org.mockito.Mockito.mock(Mockito.java:1120)
	at jp.toastkid.loto6.Loto6ControllerTest.setUpTarget(Loto6ControllerTest.java:38)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

... Omitted ...

	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Caused by: java.lang.IllegalStateException: Toolkit not initialized
	at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:273)

... Omitted ...

	at javafx.scene.control.Control.<clinit>(Control.java:87)
	... 37 more

Caused by: java.lang.IllegalStateException:Toolkit not initialized.[How do you unit test a JavaFX controller with JUnit](http://stackoverflow.com/questions/11385604/how-do-you-unit-test-a-javafx-controller-with-junit)It seems that it can be implemented by using the method described in, but it is a bit complicated ... So I decided to use the existing library.



## Environment and others
 This implementation and confirmation was carried out in the following environment.

 | Java SE | 1.8.0_102
 |:---|:---
 | OS | Windows 10
 | Eclipse | 4.5
 | JFoenix | 1.0.0
 | JUnit   | 4.11
 | Mockito | 1.9.5
 | TestFX  | 4.0.5-alpha

----

# Introducing the library
 For Gradle, all you need to do is add a line of testCompile dependencies to your dependencies.


#### **`build.gradle fix`**
```groovy

    testCompile group: 'org.testfx', name: 'testfx-junit', version: '4.0.5-alpha'

Alternatively, this format is fine.

build.gradle fix


    testCompile 'org.testfx:testfx-junit:4.0.5-alpha'

If you are using a build tool other than Gradle, please check mvnrepository.com.

TestFX Dependent Library

Guava etc. are included as dependencies.

dependencies


org.testfx:testfx-junit:4.0.5-alpha
     \--- org.testfx:testfx-core:4.0.5-alpha
          +--- com.google.guava:guava:20.0
          +--- org.hamcrest:hamcrest-core:1.3
          \--- com.google.code.findbugs:annotations:3.0.1u2
               +--- net.jcip:jcip-annotations:1.0
               \--- com.google.code.findbugs:jsr305:3.0.1

Test implementation

The test class inherits from the org.testfx.framework.junit.ApplicationTest class. It seems that up to 3 series was named `GuiTest```. In the 4th series, it has been renamed to ApplicationTest. This class must implement the `public void start (Stage stage) throws Exception``` method. In TestFX, the main style is to write tests for the Controller class.

This time, I will implement the test in the JavaFX application that runs scripts that I created earlier.

testfx_ss1.png

I have a screen like this. The code of Groovy entered by the user is changed to ScriptEngine. api / javax / script / ScriptEngine.html), and when you click the Button that says run, it has the function of displaying the execution result of the script. The left side of the screen is the CodeArea (TextArea) that accepts code input from the user, and the left side is the CodeArea (TextArea) that displays the execution result.

Implementation of start (Stage stage) method

Initialize and display the GUI in this method. It is executed before the method with @Before. This time, I will describe the initialization process before displaying the screen according to the implementation of the class that calls Controller.

Test&nbsp;class


@Override
public void start(final Stage stage) throws Exception {
    try {
        final FXMLLoader loader
            = new FXMLLoader(getClass().getClassLoader().getResource("scenes/Main.fxml"));
        final VBox loaded = (VBox) loader.load();
        controller = (Controller) loader.getController();
        final Scene scene = new Scene(loaded);
        stage.setScene(scene);
    } catch (final IOException e) {
        LOGGER.error("Scene Reading Error", e);
    }
    stage.show();
}

As mentioned above, we are loading FXML, getting the Controller, initializing the Scene and Stage, and finally displaying the stage.

Empty test method

We also have an empty test method to run JUnit's Runner.

Empty test method


@Test
public void test() {
    // NOP.
}

Run

When you run the JUnit test, you will see the application screen as shown below. Since no other test method has been implemented yet, it will only appear for a moment and then disappear immediately.

testfx_ss1.png

I will briefly explain the application for which I will write tests this time. It has a function to execute the Groovy code entered by the user in ScriptEngine and click the Button labeled run to display the execution result of the script. The left side of the screen is the CodeArea (TextArea) that accepts code input from the user, and the left side is the CodeArea (TextArea) that displays the execution result.

Add test method

Next, let's add the code for the UI test.

Reception of input

Try entering Groovy's Hello World.

Enter the script in the Text Area on the left


@Test
public void test() throws InterruptedException {
    final CodeArea input = (CodeArea) lookup("#scripterInput").query();
    final String text = "println 'Hello world.'";
    Platform.runLater(() -> input.replaceText(text));
}

Node Lookup

Nodes included in the displayed Scene can be looked up by ID. The ID can be defined in FXML or specified by setId in the Node class. An example definition in FXML is shown below.

Definition example in FXML


<CodeArea fx:id="scripterInput" prefHeight="450.0" prefWidth="500.0">

scripterinputLook up the node with the id named codearea, cast it to the implementation class codearea, and use it in subsequent tests.

Lookup->cast


final CodeArea input = (CodeArea) lookup("#scripterInput").query();

On JUnit test classes, setting JavaFX Control to a value will fail if you don't use Platform.runLater.

Platform.runLater(() -> input.replaceText(text));

Run

When you run the test, the screen will be displayed with the script entered as shown below.

testfx_ss2.png

Executing UI events

The code to get the Button that says run with Lookup and execute the set event can be written in one line below.

Lookup->query->fireEvent


Platform.runLater(() -> lookup("#runButton").query().fireEvent(new ActionEvent()));

Since the run Button is specified in FXML to execute a method called runScript of the Controller class, that method is executed, and the CodeArea (TextArea) on the right side of the screen is the result of executing the script `Hello world.` Is displayed.

testfx_ss3.png

Checking the value

All you have to do is check the value with the assert method.

Final test code


@Test
public void test() throws InterruptedException {
    final CodeArea input = (CodeArea) lookup("#scripterInput").query();
    final String text = "println 'Hello world.'";
    Platform.runLater(() -> {
        input.replaceText(text);
        assertEquals(text, input.getText());
        lookup("#runButton").query().fireEvent(new ActionEvent());
        assertEquals(
            "Hello world.",
            ((CodeArea) lookup("#scripterOutput").query()).getText().trim()
        );
    });
}

important point

Method execution is asynchronous inside and outside Platform.runLater, so if you want to check the value of Node caused by a change to Node made by Platform.runLater, you need to write it in Platform.runLater.

As an example of failure, the code below always fails because the check with assertEquals is performed before ```input.replaceText (text) `` `in Platform.runLater.

Case where inspection fails


Platform.runLater(() -> input.replaceText(text));
assertEquals(text, input.getText());

Testing with Mock

You can also write the same code using Mockito's Whitebox without using the TestFX Lookup (Note: the Controller must have its own Node object).

@Test
public void test_mockito() throws InterruptedException {
    final CodeArea input = (CodeArea) Whitebox.getInternalState(controller, "scripterInput");
    final String text = "println 'Hello world.'";
    Platform.runLater(() -> {
        input.replaceText(text);
        assertEquals(text, input.getText());
        ((Button) Whitebox.getInternalState(controller, "runButton")).fireEvent(new ActionEvent());
        assertEquals(
            "Hello world.",
            ((CodeArea) Whitebox.getInternalState(controller, "scripterOutput")).getText().trim()
        );
    });
}

The advantage of using Mockito is that you can run tests without displaying the screen. This method is recommended for those who are annoyed by the screen being displayed every time.


Summary

So far, I have briefly explained how to write tests for JavaFX applications using TestFX. You might think it's annoying to see the screen every time you run a test. It's annoying. If you want to test without displaying the screen, it is better to use Mock to build the test. Even then, TestFX makes it easier to write tests because you don't have to deal with the thread-related parts of JavaFX yourself. I didn't introduce it this time, but if you want to write unit tests for your own JavaFX components, you can use this TestFX.


reference

article

There is already a lot of information about TestFX in English and Japanese.

English

-TestFX Official Documentation

Japanese

There is a lot of information on the 3rd system.

-First step to start JavaFX automatic test tool TestFX -Test JavaFX UI in JUnit format -Testing JavaFX application with TestFX

code

-This correction difference (GitHub) -Overall code of JavaFX application implementing test (GitHub)

Recommended Posts

Write tests for JavaFX applications with TestFX
Write tests with Minitest
[Swift / For beginners] Write smartly with type inference
HelloFX with JavaFX
Write RestController tests quickly with Spring Boot + Spock
Generate dummy data for various tests with Faker (java)
Prepare the environment for java11 and javaFx with Ubuntu 18.4
Merry Christmas with JavaFX !!
Write solidly with PhpStorm
[Java] Sample project for developing web applications with Spring Boot