JavaFX / FXML Injection Unit Testing

I have some UI components that use FXML files to define a view (layout), then the controller (input handling) is defined in the component code.  In Scala, it looks something like this:

class OptionsUi extends BorderPane {
private val fxml = new FXMLLoader(getClass().getResource("Options.fxml"))
fxml.setRoot(this)
fxml.setController(this)
fxml.load()
@FXML protected var okButton: Button = _
// ...
}

The class “OptionsUi” serves as the controller, and the file “Options.fxml” is the view.  One problem is that the controls defined in the FXML file need to match up with the Scala class as variables annotated with @FXML.  For example, if the FXML file has a Button with the id “okButton”, then the controller class needs the variable okButton listed above.  The FXML API injects the okButton on fxml.load() using reflection.  I have to initialize the variable to something, so I use Scala’s ubiquitous underscore, which in this context means “I don’t care”, interpreted by Scala as a null reference.  (I use “protected” instead of “private”, because private variables generate different Java byte code that had confusing reflection-related interactions with JavaFX and FXML.)

The bad news: If I make a mistake, perhaps adding/removing/renaming a variable in one place and forgetting to do it in the other, it causes a NullPointerException when the variable is accessed.  Most of the time, NPEs are just a bad dream for Scala users, a memory of an earlier & uglier past, but there’s no way around it to use the FXML API in this way.

However, I could write an assertion that checks the okButton is injected with a non-null value.  One option is to write such an assertion for every @FXML variable in my controllers.  But we can go one step further:


test("UIs based on FXML have all @FXML members set from .fxml file") {
javafx.application.Application.launch(classOf[FxmlTestApplication])
}

class FxmlTestApplication extends javafx.application.Application {
def createUis(implicit context: ApplicationContext): List[_] = List(new MainMenu, new OptionsUi)

override def start(primaryStage: Stage) {
try {
val context = new ApplicationContext()
val uis = createUis(context)
for (
ui <- uis;
f <- ui.getclass.getDeclaredFields;
if (f.getDeclaredAnnotations.exists { _.isInstanceOf[FXML] })
) yield {
f.setAccessible(true)
val value = f.get(ui)
assert(value != null, s"${ui.getClass.getName} did not inject @FXML field ${f.getName}")
}
} finally {
// Workaround; can't avoid this
println("Please ignore exception printed by JavaFX: IllegalStateException: Attempt to call defer when toolkit not running")
Platform.exit()
}
}
}

Essentially, my unit test creates some components, looks for any @FXML fields, and checks that they were injected with non-null values.  I left the “ApplicationContext” in the example to show that you do have to create the UI components for this method, which means instantiating or mocking their dependencies.  Also, my test explicitly lists all the UI components in the application—with a littler effort you could get it to scan the ClassLoader for relevant/annotated components.  Finally, there is an exception printed by JavaFX on Platform.exit(), but it appears to be harmless and does not prevent the test from passing or failing appropriately.

Anyway, this provides the nice features of unit testing: more confidence in refactoring, catching typos much earlier, etc. with respect to the @FXML-injected variables.  It caught a mistake I made literally minutes after writing the test.

Back to an NPE-free Scala world!

One thought on “JavaFX / FXML Injection Unit Testing

  1. karadoc says:

    This was really useful, thanks. Couldn’t figure out why JavaFX (8) was injecting some of my fields but not others (table columns seemed to work with private, but not TableView for some bizarre reason).

    Unit testing that is a great idea. IntelliJ renames the FXML ids and Java fields for you (nice!) but pretty obviously not when using Scala.

Comments are closed.