28. Oktober 2015
Alexander Casall
0

JavaFX testrunner 1.2 released

These days I had some talks at the JavaOne in San Francisco – one of it was about Pitfalls you might have to handle when you use JavaFX in bigger projects. One point of the talk was the testability of  JavaFX Services. With jfx-testrunner now allows you to test JavaFX Services easily. Mainly a colleague of mine already wrote a blog post regarding this topic.

As I said, we faced problems regarding the testability of JavaFX Services in our customer projects. Therefore I added a helper to my library jfx-testrunner (1.2) which is called ServiceWrapper. The API is pretty simple. Let’s have a look on it:

@RunWith(JfxRunner.class)
public class ServiceTestableTest {

  @Test
  public void testServiceMultipleTimesWithTestrunner() throws ExecutionException, InterruptedException,
      TimeoutException {
    TestService service = new TestService();
    ServiceWrapper wrapper = new ServiceWrapper(service);
    //blocking
    wrapper.startAndWait(6000);
    //Access all properties of the Service using the wrapper
    assertEquals("I'm an expensive result. 1", wrapper.getValue());
    //blocking
    wrapper.restartAndWait(6000);
    //Access the updated properties of the Service using the wrapper
    assertEquals("I'm an expensive result. 2", wrapper.getValue());
  }
}

But which problems solves the jfx-testrunner?

Ok, let’s start with the basics. How would you start to try to test a service? I bet, at the beginning you’ll try it like it is shown in the following code block.

1. You might start try to test your service like this:

@Test
public void testLongLastingOperationSimple() {
  TestService service = new TestService();
  service.start();
  assertEquals("I'm an expensive result. 1",service.getValue());
}

2. You need a bootstrapped toolkit, other wise you will get this exception on the Service#start call

java.lang.IllegalStateException: Toolkit not initialized
  at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:273)
  at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:268)
  at javafx.application.Platform.runLater(Platform.java:83)
  at de.saxsys.pitfalls.services.testable.ServiceTestableTest. testLongLastingOperationSimple(ServiceTestableTest.java:31)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at

3. Exception will be thrown, if you access the JavaFX properties like getValue() after the first service execution. But you need the access to the values to perform the asserts.

java.lang.IllegalStateException: Service must only be used from the FX Application Thread
  at javafx.concurrent.Service.checkThread(Service.java:906)
  at javafx.concurrent.Service.getValue(Service.java:204)
  at de.saxsys.pitfalls.services.testable.ServiceTestableTest. testLongLastingOperationSimple(ServiceTestableTest.java:42)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  at

4. The next problem is, that your unit test will finish before the service executes completely, so you need a solution to block the test thread until the service has finished.

To shorten the blog post, I give you the solution which enables you to test the service:

@RunWith(JfxRunner.class)
public class TestServiceTest {

  @Test
  public void testMyService() throws ExecutionException, InterruptedException,
      TimeoutException {
    TestService service = new TestService();
    CompletableFuture<String> future = new CompletableFuture<>();
    Platform.runLater(() -> {
      service.valueProperty().addListener((b, o, n) -> {
        if (n != null) {
          future.complete(n);
        }
      });
      service.restart();
    });
    assertEquals("I'm an expensive result. 1", future.get(5, TimeUnit.SECONDS));
    // service.getValue(); would throw an exception, cause you are not on Platform Thread
  }
}

As you can see I’m using a CompletableFuture to block the test thread and wait for the result of the future, which is delivered with the future.complete(n) call. To rerun your service in the test, you have to double the code:

@RunWith(JfxRunner.class)
public class ServiceTestableTest {

  @Test
  public void testServiceTwice() throws ExecutionException, InterruptedException,
      TimeoutException {
    TestService service = new TestService();
    CompletableFuture<String> future = new CompletableFuture<>();
    Platform.runLater(() -> {
      service.valueProperty().addListener((b, o, n) -> {
        if (n != null) {
          future.complete(n);
        }
        
      });
      service.restart();
    });
    assertEquals("I'm an expensive result. 1", future.get(5, TimeUnit.SECONDS));
    
    CompletableFuture<String> future2 = new CompletableFuture<>();
    Platform.runLater(() -> {
      service.valueProperty().addListener((b, o, n) -> {
        if (n != null) {
          future2.complete(n);
        }
        
      });
      service.restart();
    });
    assertEquals("I'm an expensive result. 2", future2.get(5, TimeUnit.SECONDS));
  }

Because this is a way too much boilerplate code to test a Service, I introduced the ServiceWrapper to jfx-testrunner. The test looks like this:

@RunWith(JfxRunner.class)
public class ServiceTestableTest {

  @Test
  public void testServiceMultipleTimesWithTestrunner() throws ExecutionException, InterruptedException,
      TimeoutException {
    TestService service = new TestService();
    ServiceWrapper wrapper = new ServiceWrapper(service);
    wrapper.startAndWait(6000);
    assertEquals("I'm an expensive result. 1", wrapper.getValue());
    wrapper.restartAndWait(6000);
    assertEquals("I'm an expensive result. 2", wrapper.getValue());
  }
}

You save a lot of boilerplate code using the small library. It has a very small footprint and no external dependencies.

Go for it.

Alexander Casall works as a software developer at Saxonia Systems AG in Dresden, Germany. His focus is on the implementation of modern multi-touch applications (JavaFX) and iOS development (www.buildpath.de). Alexander publishes articles in journals and is a speaker at conferences (JavaOne, JAX, W-JAX) and in various user groups.

Twitter Xing 

TeilenTweet about this on TwitterShare on Facebook0Share on Google+0Share on LinkedIn0