How to test JavaFX Services

In this post we want to take a look on how to write an integration test for a JavaFX Service.  Our showcase is a Service that performs a login and signals whether the login was successful or not. Please have a look on the code below to understand how the login works. As I said, this is an integration test for the combination of the Service and the Task in it. In addition you should write an Unit Test for the task in the Service .

public class LoginService extends Service {

    private final String password;
    private final String username;

    private ClientApi clientApi;

    @Inject
    public LoginService(Client Api, String username, String password) {
        this.username = username;
        this.password = password;
        this.clientApi = clientApi;
    }

    @Override
    protected Task createTask() {
        Task task = new Task() {
            @Override
            protected Boolean call() throws Exception {
                 // true/false for login state, Exception for Networkproblems
                 Boolean login;
                 try {
                      login = clientApi.login(username, password);
                 } catch (Exception e) {
                      //If error occures, set fail message and throw e to make the task fail
                      updateMessage("Error while logging in");
                      throw e;
                 }
                 if (!login) {
                      updateMessage("Invalid credentials");
                 }
                 return login;
            }
        };

        return task;
    }
}

As you can see, the service follows the Dependency Injection pattern, so we can mock the dependencies easily.


What do we want to test?

 

  • Successful login
    • stateProperty() is Worker.State.SUCCEEDED
    • valueProperty() is true
    • messageProperty() is “”

 

  • Failed login because of wrong credentials
    • stateProperty() is Worker.State.SUCCEEDED
    • valueProperty() is true
    • messageProperty() is “Invalid Credentials”

 

  • Failed login because of  network errors
    • stateProperty() is Worker.State.FAILED
    • valueProperty() is false
    • messageProperty() is “Error while logging in” 

 


Problems which occurs when you test a Service

  1. A service needs an initialized JavaFX Toolkit
  2. A services executes the tasks in separate thread which makes it hard to perform asserts

 

Solution for Problem 1

As I already mentioned we need to initialize the JavaFX Toolkit, otherwise we will get this Exception when we start the Service:

java.lang.IllegalStateException: Toolkit not initialized
	at com.sun.javafx.application.PlatformImpl.runLater(Unknown Source)
	at com.sun.javafx.application.PlatformImpl.runLater(Unknown Source)
	at javafx.application.Platform.runLater(Unknown Source)
	at javafx.concurrent.Service.runLater(Unknown Source)
	at javafx.concurrent.Service.start(Unknown Source)
	at de.saxsys.javafx.test.example.LoginServiceTest.failedLoginWithNetworkErrors(LoginServiceTest.java:79)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

I created a small JUnit TestRunner to solve this problem. The Runner manages the JavaFX Toolkit initialization. Please read the following post for more information:

http://blog.buildpath.de/javafx-testrunner/

We’ll see how to use the Runner in our following code boxes.

Solution for Problem 2

To handle the new thread which is spawned by the Service we use a CompletableFuture<T> that blocks the Test-Thread until the complete method of the future was called.

In the example you can see a CompletableFutures that will complete when the state of the Serviceis the state Worker.State.DONE.

If this happens, we transport the current values of the Service with a DTO through the CompletableFuture back to our test thread.

CompletableFuture<ServiceTestResults> serviceStateDoneFuture = new CompletableFuture<>();

service.stateProperty().addListener((observable, oldValue, newValue) -> {
     //We check, whether the new state is the state we are looking for to make our tests
     if (newValue == Worker.State.Done) {
            serviceStateReady.complete(
             new ServiceTestResults(service.getMessage(), service.getState(),service.getValue())
            );
     }
});

service.start();

// This call is blocking the thread until complete() was called and get() gives an result. You can set a timeout 
ServiceTestResults result = serviceStateDoneFuture.get(100, TimeUnit.MILLISECONDS); 

Assert.assertEquals("YEA WE CAN ASSERT",result.message)
// DTO to transport the results of the check
private class ServiceTestResults {
   
  String message;
  Worker.State state;
  Boolean serviceResult;

  ServiceTestResults(String message, State state, Boolean serviceResult) {
      this.message = message;
      this.state = state;
      this.serviceResult = serviceResult;
  }
}

Test the LoginService

Ok, now we can write the tests for our LoginService. With the combination of the Test Runner and CompletableFuture we can test all cases that we wanted. I hope the code explains itself – If you have further questions please comment under the post.

package de.saxsys.javafx.test.example;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

import javafx.concurrent.Worker;
import javafx.concurrent.Worker.State;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

import de.saxsys.javafx.test.JfxRunner;

//The JfxRunner ensures, that an JavaFX-Application is present
@RunWith(JfxRunner.class)
public class LoginServiceTest {

    private static final String EMPTY_STRING = "";

    @Test
    public void testStateBySuccessFulLogin() throws Exception {
        // Mock the CientAPI to have a successful login
        ClientApi mockedClientApi = (username, password) -> true;
        // Create the Service with the mocked ClientAPI
        LoginService service = new LoginService(mockedClientApi, EMPTY_STRING, EMPTY_STRING);

        // Create a completableFuture to test the background task of the
        // service
        CompletableFuture<ServiceTestResults> stateCompareableFuture = createStateCompareFuture(service,
                Worker.State.SUCCEEDED);

        // We are not in the UI Thread so we have to start the service in
        // runLater()
        service.start();

        // get the result of the CompareableFuture with a timeout when the
        // result does not appear
        ServiceTestResults result = stateCompareableFuture.get(100, TimeUnit.MILLISECONDS);

        // Tests
        Assert.assertEquals(Worker.State.SUCCEEDED, result.state);
        Assert.assertEquals(EMPTY_STRING, result.message);
        Assert.assertTrue(result.serviceResult);
    }

    @Test
    public void failedLoginWithWrongCredentials() throws Exception {
        // Mock the CientAPI to have a failed login because of invalid
        // credentials
        ClientApi mockedClientApi = (username, password) -> false;
        // Create the Service with the mocked ClientAPI
        LoginService service = new LoginService(mockedClientApi, EMPTY_STRING, EMPTY_STRING);
        // Create a completableFuture to test the background task of the
        // service
        CompletableFuture<ServiceTestResults> stateCompareFuture = createStateCompareFuture(service,
                Worker.State.SUCCEEDED);

        service.start();

        // get the result of the CompareableFuture with a timeout when the
        // result does not appear
        ServiceTestResults result = stateCompareFuture.get(100, TimeUnit.MILLISECONDS);

        // Tests
        Assert.assertEquals(Worker.State.SUCCEEDED, result.state);
        Assert.assertEquals("Invalid credentials", result.message);
        Assert.assertFalse(result.serviceResult);
    }

    @Test
    public void failedLoginWithNetworkErrors() throws Exception {
        // Mock the CientAPI to have a failed login with some errors
        ClientApi mockedClientApi = (username, password) -> {
            throw new RuntimeException();
        };
        // Create the Service with the mocked ClientAPI
        LoginService service = new LoginService(mockedClientApi, EMPTY_STRING, EMPTY_STRING);

        // Create a complteableFuture to test the background task of the
        // service
        CompletableFuture<ServiceTestResults> stateCompareFuture = createStateCompareFuture(service,
                Worker.State.FAILED);

        service.start();

        // get the result of the CompareableFuture with a timeout when the
        // result does not appear
        ServiceTestResults result = stateCompareFuture.get(100, TimeUnit.MILLISECONDS);

        // Tests
        Assert.assertEquals(Worker.State.FAILED, result.state);
        Assert.assertEquals("Error while logging in", result.message);
        Assert.assertNull(result.serviceResult);
    }

    // Helper method which creates a compareableFuture that tells us, when the
    // state of the service reached a state which we expect
    private CompletableFuture<ServiceTestResults> createStateCompareFuture(LoginService service,
            Worker.State stateToReach) {
        CompletableFuture<ServiceTestResults> serviceStateReady = new CompletableFuture<>();
        service.stateProperty().addListener(
                (observable, oldValue, newValue) -> {
                    if (newValue == stateToReach) {
                        serviceStateReady.complete(new ServiceTestResults(service.getMessage(), service.getState(),
                                service.getValue()));
                    }
                });
        return serviceStateReady;
    }

    // DTO to transport the results of the check
    private class ServiceTestResults {
        String message;
        Worker.State state;
        Boolean serviceResult;

        ServiceTestResults(String message, State state, Boolean serviceResult) {
            this.message = message;
            this.state = state;
            this.serviceResult = serviceResult;
        }
    }
}

 

Share Button

0 Comments

  1. Pingback: JavaFX links of the week, July 28 // JavaFX News, Demos and Insight // FX Experience

  2. Pingback: Saxonia Systems AG » asynchrone JavaFX-Logik testen

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>