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()
isWorker.State.SUCCEEDED
valueProperty()
istrue
messageProperty()
is “”
- Failed login because of wrong credentials
stateProperty()
isWorker.State.SUCCEEDED
valueProperty()
istrue
messageProperty()
is “Invalid Credentials”
- Failed login because of network errors
stateProperty()
isWorker.State.FAILED
valueProperty()
isfalse
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; } } }
Pingback: JavaFX links of the week, July 28 // JavaFX News, Demos and Insight // FX Experience
Pingback: Saxonia Systems AG » asynchrone JavaFX-Logik testen