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
- A service needs an initialized JavaFX Toolkit
- 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 Service
is 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;
}
}
}