javafx-gradle-plugin

After tweeting my blog posts on Twitter, I got a reply from the cofounder of Gluon no less (Eugene Ryzhikov) who suggested I take a look at the javafx-gradle-plugin. I’m glad I did – it has cleaned up my gradle file nicely so that it looks less abnormal.

I think I did it right.

Old Gradle Dependencies

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
compile name: "javafx.fxml" // WEIRD
compile name: "javafx.controls" // WEIRD
compile name: "javafx.base" // WEIRD
compile name: "javafx.graphics" // WEIRD
compile "com.google.guava:guava:27.1-jre"

testCompile "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
testCompile "org.assertj:assertj-core:3.11.1"
testCompile "org.testfx:testfx-core:4.0.15-alpha"
testCompile "org.testfx:testfx-junit:4.0.15-alpha"
}

New Gradle Dependencies

javafx {
modules = [ 'javafx.controls', 'javafx.fxml' ]
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
compile "com.google.guava:guava:27.1-jre"
// WEIRDNESS GONE
testCompile "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
testCompile "org.assertj:assertj-core:3.11.1"
testCompile "org.testfx:testfx-core:4.0.15-alpha"
testCompile "org.testfx:testfx-junit:4.0.15-alpha"
}

The full gradle file is here.

TestFx

The most well-known library for unit and integration testing JavaFX applications is TestFX. I have written some tests inside the application to run Junit-driven tests using TestFX – https://github.com/SergeMerzliakov/javafx-app-1.

TestFX works much like any other JUnit oriented testing library and to set it up you need to do 3 things:

  1. Add the testfx libraries as dependencies to build.gradle
  2. Make your Test classes inherit from org.testfx.framework.junit.ApplicationTest
  3. Implement the start() method to create the app to test
  4. Ensure all to-be-tested controls have a unique JavaFX Id

Point 4 above is optional but I think it is good practice.

SETUP

Step 1 – Add TestFx to build.gradle


dependencies {
    ....
    testCompile 'org.testfx:testfx-core:4.0.15-alpha'
    testCompile 'org.testfx:testfx-junit:4.0.15-alpha’
    ....
}

Nothing more to add here. We’ve all done this before.

Step 2 – Set Test classes to inherit from ApplicationTest

This adds TestFX magic juju to your test classes.

class MyClassTest : ApplicationTest() { 
  ...
  ...
}

Once again, nothing revolutionary so far. Sure, we are using up our inheritance ‘slot’, and maybe an annotation based approach would have been nicer, but such is life.

Step 3 – Implement the start() method

ApplicationTest has a single method you need to override, which is almost the same as the start method used to start a JavaFX Application. The example below creates a loader so that we can keep a reference to the FXML’s controller class, which is useful for testing.

...
private lateinit var controller: Controller

override fun start(stage: Stage?) {
  // We instantiate a loader directly so we can access the controller for testing
  val loader = FXMLLoader(javaClass.getResource(“/dir1/dir2/application.fxml"))
  val root = loader.load<VBox>()
  stage?.scene = Scene(root)

  // keep a reference to the controller for our tests
  controller = loader.getController()
  stage?.show()
}
...

Step 4 – Ensure Controls All Have Unique JavaFX Ids

You can do this is a few different ways:

  • SceneBuilder
  • In code
  • Hand editing the FXML file

Setting JavaFX Id inSceneBuilder

Just a screen shot for this one. Just have to know which text box to fill in.

JavaFX ID is the same as a CSS ID Selector. You know, like #greenButtonWithSharkLogo

Setting JavaFX Id in Code

@FXML
fun initialize() {
    ...
    tableView.id“tableView"
}

Setting JavaFX Id in FXML by Hand

<Button id="addItemButton" mnemonicParsing="false" onAction="#addItem" text="Add To List"> 
    <FlowPane.margin>
       <Insets right="10.0" />
    </FlowPane.margin>
</Button>

WRITING TESTS

This part is very much same as for writing any JUnit tests, but uses the TestFX robot helper functions to simulate a user.

The conceptual steps are:

  1. Find the control to test in the node graph 
  2. Perform some action on the control
  3. Check the results on the model and/or view

Step 1 – Find the Control to Test in the Node Graph

You use the org.testfx.api.FxRobotInterface.lookup() method to find the control you need. There are a few ways of doing this:

  • Using the text of a labelled node:
val cancelButton = lookup(“Cancel”).query<Button>()
  • Using the CSS class of a node:
val allButtons =  lookup(“.button”).queryAll<Button>()
  • Using the JavaFx id of a node:
val pinkButton = lookup(“#thirdPinkButtonOnTheLeft").query<Button>()
  • Using a whole CSS selector:
val helpButton = lookup(".small-button #helpButton").query<Button>()

I prefer the methods using the JavaFx id, as they can be made unique and less prone to CSS changes.

Step 2 – Perform Some Action on the Control

There are lots of these in org.testfx.api.FxRobotInterface. The most common being clickOn(), doubleClickOn(), write() and type().

At this point you can perform some actions, such as:

  • Pushing a button
clickOn(“#updateButton”)
  • Entering text in a field (once it is selected or has focus)
clickOn("#SomeTextField") 
write(“This is the text in the currently selected field”)
  • Hitting enter or tab key:
type(KeyCode.TAB) 
type(KeyCode.ENTER)

Step 3 – Check the Results

You can check the model, the view or both.

If the data model is an ObservableList<T>, checking both model and view containers is not really necessary, as the view gets updated automatically whenever you alter the contents of the model . This is AWESOME by the way! One line of code, and no more addThisListener() or addThatListener()……

If your model is a just a standard collection of some kind,  and you manually manage model-view synchronisation, then testing both model and views after UI operations makes sense. Any such data model tests probably belong in another test, and not lumped in with UI tests.

Checking the Model

// check model after removing an item from the list view
assertThat(controller.friends).hasSize(originalModelCount - 1)

Checking the View

// check ListView items reduced by one.
assertThat(friendView.items).hasSize(originalViewCount - 1)

A Complete Example

ItemTabIntegrationTest.kt

/** 
* Use Junit TestFX to integration test the application UI, so
* not unit tests per se.
* BetterApplicationTest is a subclass of TestFx class ApplicationTest (discussed below)
*/
class ItemTabIntegrationTest : BetterApplicationTest() {

 // Node Ids defined in the FXML file. Same as a CSS Id selector
  // Use these to uniquely identify JavaFX controls for testing
  companion object {
    const val LIST_DEMO_TAB = "#listDemoTab"
    const val ITEM_LIST_VIEW = "#itemListView"
    const val ENTER_ITEM_FIELD = "#enterItemField"
    const val ADD_ITEM_BUTTON = "#addItemButton"
 }

  private lateinit var controller: Controller

 override fun start(stage: Stage?) {
    // we instantiate a loader directly so we can access the controller for testing
    val loader = FXMLLoader(javaClass.getResource("/app1/app1.fxml"))
    val root = loader.load<VBox>()
    controller = loader.getController() // keep a reference for our tests
    stage?.scene = Scene(root)
    stage?.show()
 }

  @Test
  fun shouldAddItemToList() {
    val initialCount = controller.listModel.size


     //ensure correct Tab has focus
    clickOn(LIST_DEMO_TAB)


    //create new item for list - should appear at the end
    val newItem = "Zoo"
    clickOn(ENTER_ITEM_FIELD)
    write(newItem)
    type(KeyCode.TAB)
    clickOn(ADD_ITEM_BUTTON)

    // check model updated
    val modelCount = controller.listModel.size
    assertThat(modelCount).isEqualTo(initialCount + 1)

    // check view updated
    val viewCount = controller.itemListView.items.size

  // this method is in declared in the base class
    val zooListItem = getListViewRow(ITEM_LIST_VIEW, viewCount - 1)
    assertThat(zooListItem.text).isEqualTo(newItem)  }
}

BetterApplicationTest.kt

I have created a subclass of org.testfx.framework.junit.ApplicationTest and added some methods to manipulate listView and tableView data. All tests in app1 and app2 in the repo inherit from this. A better way may be to create a ViewTestUtils object with these methods in it, but you would need to pass in an instance of the ApplicationTest. 

Neither option excited me, so I went with the simplest option – subclassing. I am keen to fix this as I use these helper methods a lot, and the subclassing approach has limited traction across a real project with hundreds of tests.

open class BetterApplicationTest : ApplicationTest() { 
   /**
    * Helper function to get a row from a TableView
    *
    *  Creating a generic version of this function ==> fun <T:Any> getTableViewRow(viewId: String, row: Int): T
    *
    *  is very tricky and I did not quite succeed. Ah, so close...
    *
    *  see: https://stackoverflow.com/questions/43477903/instantiating-a-generic-type-in-kotlin
    */
   fun getTableViewRow(viewId: String, row: Int): SomeProperty {
      val tableView = lookup(viewId).query<TableView<SomeProperty>>()
      val cell1: TableCell<SomeProperty, String> = from(tableView).lookup(".table-cell").nth(row * 2).query()
      val cell2: TableCell<SomeProperty, String> = from(tableView).lookup(".table-cell").nth(row * 2 + 1).query()

      return SomeProperty(cell1.text, cell2.text)
   }

   /**
   * Helper function to get a row from a ListView.
    * Type T is the type of the ListView data model.
    */
   fun getListViewRow(viewId: String, row: Int): ListCell<String> {
      val listView = lookup(viewId).query<ListView<String>>()
      return from(listView).lookup(".list-cell").nth(row).query()
   }

   /**
   * Helper function to get a row starting the given text from a ListView.
    * Type T is the type of the ListView data model.
    *
    *  This method is a bit dodgy, but illustrates how to access listView row data
    *
    */
   fun <T> getListViewRowByFirstName(viewId: String, textToFind: String): ListCell<T>? {
      val listView = lookup(viewId).query<ListView<T>>()
      val cells = from(listView).lookup(".list-cell").queryAll<ListCell<T>>()
      // assumes type T has a toString method starting with first name!
      return cells.find { it.item.toString().toLowerCase().startsWith(textToFind.toLowerCase()) }
   }
}

DRAG AND DROP

Getting TestFX to test drag and drop operations is surprisingly easy. After wasting my youth on lots of fun and games with SWT and Swing, I mentally readied myself for an epic battle of wits and stamina, only to succeed very quickly. What a letdown.

So a snippet from one of Drag and Drop tests looks like this:

@Test 
fun shouldDragSingleFriendBetweenTwoListViews() {
   .....
   // drag a single friend from friendListView to PartyListView
   dragFriendToParty("Andrea")
   ......
}

private fun dragFriendToParty(firstName: String) {
   // find friend by first name via dodgy utility method
   val friend = getListViewRowByFirstName<Person>(FRIEND_LIST_VIEW, firstName)

 // drag and drop automation is just this line!
 drag(friend).dropTo(PARTY_LIST_VIEW)
}

That’s it. Just a single lines in the dragFriendToParty() method. Here is the whole unit test class in GitHub.

SUMMARY

Whilst it’s a bit strange at first, once you get your head around the TestFX way of doing things, it quickly becomes easy to write tests for your user interface, and will become an indispensable part of your automated test arsenal. TestFX can be used for unit testing individual components as well, even though the examples here focus on testing entire applications.

TestFX Resources

TestFX Robot functions: https://github.com/TestFX/TestFX/blob/master/docs/manual/component-robot.md

TestFX queries: https://github.com/TestFX/TestFX/wiki/Queries

Application Number 1

I started this blog after spending several years of my private time building Kotlin and JavaFX desktop applications for fun and very little profit. My day job has nothing to do with either. Along the way, I have picked up hard-won experience with Kotlin and JavaFX.  I often struggled with getting answers for my issues, and having collected lots of private repos in bitbucket, thought… “Why not blog about my experiences?”

Nothing sharpens your skills like creating demos and explaining things, if only to avoid writing something stupid. 

Anyway, enough crap about me. On with the show.

I plan to blog about issues that I came across developing Kotlin desktop applications such as Drag and Drop, Custom Controls, Multiple Controllers and so on. Each post will discuss a specific, working application on GitHub.

This first post is mostly about showing how to get started with a basic Kotlin/JavaFX/Gradle setup.

I have created a GitHub repo with fully working example:

https://github.com/SergeMerzliakov/javafx-app-1

Here is a screen shot. Nothing fancy, just a basic demo.

Up and Running in 30 Seconds

If you have Java 11 and OpenJDK 12:

git clone git@github.com:SergeMerzliakov/javafx-app-1.git

<edit build.gradle. Update JFX_INSTALL to point to JFX 12>

gradlew clean build

gradlew runApp1

JavaFX is now a Separate Library

Duh. It’s 2019 and all of us with more than 30 seconds of Googling behind us will know this. JavaFX is now OpenFX and its downloaded as a plain old zip file. So you can install it anywhere. This has implications for your gradle config.

My build.gradle repositories block looks like this now.

repositories {
mavenCentral()
flatDir {
dirs "${JFX_INSTALL}/lib"
}
}

A variable called JFX_INSTALL now defines where it’s installed. This will come in handy later. The install contains a lib directory with all the jars you need.

I don’t know if this is the best way to do it, but it satisfies rule 1 – Keeping Things Simple.

Given that as of today, we have Java 11 and 12, with OpenJFX 11 and 12, what are the allowed permutations? Well I know that:

  • Java 11 + OpenJFX 11 ==> Works
  • Java 11 + OpenJFX 12 ==> Works
  • Java 12 + OpenJFX 12 ==> Works
  • Java 12 + OpenJFX 11 ==> Not sure ++

++ The OpenJFX website tells you to use Java 11 and that’s about it. I quote:

If you have newer or older versions of Java installed along with JDK 11, you need to make sure that the JAVA_HOME environment variable points to JDK 11

Some basic tests with Java 11 and switching between OpenJFX 11 and 12 has shown zero difference in behaviour so far.

Gradle Oddities

The dependencies section references the OpenJFX install location rather than the Maven repository details. That’s because the Maven artefact JFX jars are all empty. WTF? No obvious reason after some research. I ploughed on, and did this:

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
compile name: 'javafx.fxml'
compile name: 'javafx.controls'
compile name: 'javafx.base'
compile name: 'javafx.graphics'
}

Inelegant, but I am nothing if not goal oriented. Stints as a pre-sales consultant really drilled that into me.

I suspect that this may be due to the use of Java Modules, which I have zero experience with. No one at my day job has ever used them either. So I will blithely coast over this and worry about it ‘real soon now’.

Running The Application

Since we need to use modules, starting an OpenJFX 11+ app requires some extra VM parameters, basically telling the JVM where you installed OpenJFX and which modules you require loaded. Weird. Why can’t this be worked out some other way? Surely as part of the compile/link/resolve dependencies process?

Below is the gradle task for running App1. Hopefully the first of many.

task runApp1(type: JavaExec) {
group = "Application"
description = "Runs Application 1"
classpath sourceSets.main.runtimeClasspath
main = 'org.epistatic.app1.Main'
doFirst {
jvmArgs = [
'--module-path', "${JFX_INSTALL}/lib",
'--add-modules', 'javafx.fxml,javafx.controls'
]
}
}

Summary

Well that’s it for the first post. Nothing revolutionary here, but hopefully the code repository works out of the box. If you cannot get the demo running, let me know so I can fix the bastard.