JavaFX Unit Tests without TestFX

Whilst developing a recent project, I found that I needed to create FX nodes outside the context of an FX application (an experiment in code generation). This led me to consider how to create unit tests without a dependency on TestFX, as I only needed to create FX nodes without ever displaying them, but wanted to ensure JavaFX threading issues were taken care of. I came up with a solution, but it could also be used for normal JavaFX unit tests as well (with caveats).

I do not recommend my solution as a replacement for TestFX or similar frameworks – this approach suites my particular requirements, and may suite yours. I still use TestFX extensively and have no plans to stop doing so.

The Benefits

  • Simpler to test FX object creation, outside of an FX application (my use case)
  • Tests run faster
  • For applications, does not highjack your mouse when it runs (no need for headless-mode monocle setup)
  • Create a different application in every test

The Downsides

  • Slightly (?) clunkier code
  • For applications, you cannot watch the tests run and see what they are actually doing
  • May need to duplicate (a little or a lot of) TestFX helper functionality

Key Steps

  1. Create a plain old JUnit Test class
  2. Initialize the FX toolkit in a @BeforeClass/@BeforeAll static method
  3. Wrap FX creation code in FXBlock class
  4. Wrap FX tests assertions in FXBlock class

The FXBlock Class

This class just wraps calls to Platform.runLater and runs a block of FX code in the FXApplication thread and ALWAYS blocks until the FX Thread is done. Here is a minimal example:

FXBlock(Runnable {
  val stage = Stage()
  stage.scene = Scene(AnchorPane(Button("OK”)))
  stage.show()
}).run()

All FX setup code and unit test checks should run inside an FXBlock. This is the “clunky” bit.

A Complete Example using JUnit 4

The full class is here. There is lots of scope to tidy things up, but the code below works. There’s no magic anywhere, just the FXBlock class to simplify the logic.

class NoTestFXTest {

  companion object {
    @BeforeClass
    @JvmStatic
    fun startJavaFXRuntime() {
      Platform.startup {}
    }
  }

  @Test
  fun fxThreadTest() {
    // when
    val ap = AnchorPane()

    FXBlock(Runnable {
      val stage = Stage()
      ap.prefHeight = 200.0
      ap.prefWidth = 240.0

      val label = Label("Empty")
      ap.children.add(label)

      val button = Button("OK")
      button.onAction = EventHandler { label.text = "button clicked" }
      ap.children.add(button)

      stage.scene = Scene(ap)
      stage.show()
    }).run()

    // then
    FXBlock(Runnable {
      val button = ap.children.find { it is Button } as Button
      button.fire()

      val label = ap.children.find { it is Label } as Label
      assertThat(label.text).isEqualTo("button clicked")
    }).run()
  }
}

Conclusion

This approach requires recreating TestFX functionality, and so would not be the best approach for the vast majority of test scenarios. I will use it for my FX code generation project, as I don’t need to test complete JavaFX applications, just the creation of FX nodes and scene graphs.

That said, my previous experience with duplicating TestFX capabilities like “Find By Id”, “click node with Id” was pretty good – it did not take long and worked well. So if some time in the future, TestFX stops being maintained – I have a fallback plan.