Posts‎ > ‎

Creating a simple JavaFX 8 app and testing it with Automaton

posted Jul 28, 2014, 2:23 PM by Renato Athaydes   [ updated Jul 29, 2014, 2:01 PM ]
I have finally given Automaton 1.1 the last touches and released it, making it publicly available at Bintray's JCenter. I believe that with the newest features and bug fixes (see the release notes for a full list) Automaton has really become an awesome framework to allow anyone to thoroughly test their UIs (JavaFX, Swing or mix of both) in a very similar manner to some great UI-testing frameworks such as Selenium and Geb.

In this blog post, I would like to show how to create and test a JavaFX application using pure Java code and, alternatively, using FXML (a UI-layout markup language similar to HTML) and the Scene Builder, a free designer tool which greatly eases the process.
The application will be tested using some incredibly easy to write Automaton Scripts (AScripts, as I like to call them). If you still need a hand,  you will see how you can benefit from code-highlighting/auto-completion for AScripts in IntelliJ.
Finally, I will mention some advanced Automaton features such as custom selectors.

All source code shown in this post can be found in this GitHub project.

So, let's get started!

Creating a simple JavaFX 8 application using pure Java code


In JavaFX (version 2.x and 8) there are 2 ways one can design an UI: using pure Java code or using FXML. The former is easy to get started, but the latter allows the UI layout to be written declaratively and separately from application logic, making it a better fit for complex applications.

As our application will be simple, let's get started with some pure Java code:

public class SimpleFxApp extends Application {

    Label status;
    TextField nameField;
    TextField emailField;
    TextArea commentsField;

    @Override
    public void start( Stage primaryStage ) throws Exception {
        status = new Label();
        status.setId( "status-label" );
        VBox panel = new VBox( 20 );
        panel.getChildren().addAll(
                row( "Name", nameField = new TextField() ),
                row( "Email", emailField = new TextField() ),
                row( "Comments", commentsField = new TextArea() ),
                row( "", buttons() ),
                status
        );
        HBox root = new HBox( 10 );
        root.getChildren().addAll( panel );
        HBox.setMargin( panel, new Insets( 10 ) );
        Scene scene = new Scene( root, 500, 400 );
        primaryStage.setScene( scene );
        primaryStage.centerOnScreen();
        primaryStage.show();
    }

    public Node row( String labelText, Node field ) {
        HBox row = new HBox( 10 );
        Label label = new Label( labelText );
        label.setMinWidth( 120 );
        row.getChildren().addAll( label, field );
        return row;
    }

    public Node buttons() {
        HBox buttons = new HBox( 10 );
        Button cancel = new Button( "Cancel" );
        cancel.setOnAction( ( e ) -> status.setText( "You cancelled" ) );
        Button ok = new Button( "OK" );
        ok.setOnAction( ( e ) -> status.setText( computeStatus() ) );
        buttons.getChildren().addAll(
                cancel,
                ok
        );
        return buttons;
    }

    private String computeStatus() {
        return "Name: " + nameField.getText() + ", Email: " + emailField.getText() +
                ", Comments: " + commentsField.getText();
    }

    public static void main( String[] args ) {
        Application.launch( SimpleFxApp.class );
    }

}

The resulting UI shown in Windows 7:

screenshot


Designing the same UI with the Scene Builder

The Java code above will probably be very familiar to Swing users. FXML, on the other hand, is more like writing HTML (you can even style JavaFX nodes using css).

If you decide to use FXML, you can also benefit from Oracle's free designer tool, JavaFX Scene Builder, which really is a great feature of JavaFX.

It works great most of the time, and when it does not do exactly what you want, you can easily modify the FXML file manually and next time you open it in Scene Builder, it will keep your changes intact.

Here is what the Scene Builder looks like:

scene builder


To create Nodes, you just need to drag elements from the toolbar on the left into the central area where the application canvas is (or into the Node tree). The styling and layout can be configured on the right-side panel.
At the lower-left corner, you can see your application's Node tree, and you may even move Nodes around or copy and paste parts of it.

Another really cool feature is that the Scene Builder helps you style your Node using JavaFX's version of CSS, as shown in the screen below:

styling


With the label for Name selected, as I typed into the Style text field, I got a list of available style names, which really helps when you are still learning.

Once you're done designing your view, you can save the FXML file and inspect its contents. I achieved a UI very similar to the one created with the Java-code above with the following FXML document:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<HBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="382.0"
      prefWidth="600.0" spacing="10.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <VBox prefHeight="340.0" prefWidth="393.0" spacing="20.0">
         <children>
            <HBox prefHeight="39.0" prefWidth="393.0" spacing="10.0">
               <children>
                  <Label style="-fx-min-width: 200;" text="Name:" />
                  <TextField />
               </children>
               <opaqueInsets>
                  <Insets top="10.0" />
               </opaqueInsets>
            </HBox>
            <HBox prefHeight="39.0" prefWidth="393.0" spacing="10.0">
               <children>
                  <Label style="-fx-min-width: 200;" text="Email" />
                  <TextField />
               </children>
            </HBox>
            <HBox prefHeight="124.0" spacing="10.0">
               <children>
                  <Label style="-fx-min-width: 200;" text="Comments:" />
                  <TextArea prefHeight="250.0" prefWidth="400.0" />
               </children>
            </HBox>
            <HBox prefHeight="36.0" prefWidth="393.0" spacing="10.0">
               <children>
                  <Button mnemonicParsing="false" text="Cancel" />
                  <Button mnemonicParsing="false" text="OK" />
               </children>
               <padding>
                  <Insets left="210.0" />
               </padding>
            </HBox>
            <Label id="status-label" prefHeight="17.0" prefWidth="399.0" />
         </children>
         <opaqueInsets>
            <Insets />
         </opaqueInsets>
      </VBox>
   </children>
   <opaqueInsets>
      <Insets left="10.0" />
   </opaqueInsets>
   <padding>
      <Insets left="10.0" top="10.0" />
   </padding>
</HBox>

Connecting the FXML view with a Java controller

Once you have the FXML file, though, you still need to manually "connect" it to the Java back-end.

In the case of this UI, we needed to add actions to the buttons and get a reference to the status-label and TextFields so that we could set the text on the status label. But before we can write the Java "controller", we must add some metadata to the FXML document.

Pretty basic. First thing to do is tell JavaFX what is the name of the controller class (which we will look at later) through the fx:controller attribute.

<HBox fx:controller="com.athaydes.automaton.tests.fxml.AppPanel">

All Nodes you want to have a reference for in the Java code must have an fx:id field with the same name as the Java controller's field it represents. So, in the FXML file we add:
<TextField fx:id="nameField" />
<...>
<TextField fx:id="emailField" />
<...>
<TextArea fx:id="commentsField" />
<...>
<Label id="status-label" fx:id="status" />
<...>

For button actions, we can simply link the button onAction attribute to the method we want to run.
<Button onAction="#onCancel" text="Cancel" />
<Button onAction="#onOK" text="OK" />

Notice the similarity between this and HTML/JavaScript. It's ironic to think that desktop UI frameworks, the grandparents of current web technology, are now trying to catch up.

If you like a little extra help when entering this metadata, I highly recommend using IntelliJ to do it. Besides being able to bring up the Scene Builder directly from the context menu when selecting the FXML file, IntelliJ can suggest values for fx:id based on the controller  class and even onAction methods. It can also format the document nicely, auto-complete tag names (using JavaFX class names), organize imports, refactor the names of things in both the Java code and the FXML documents, and many other incredibly useful features.

NetBeans also has great support for JavaFX (being itself also developed by Oracle), however I cannot tell how it compares to IntelliJ for JavaFX development. All I can say is that one thing that is currently not missing is great tooling for JavaFX.

And finally, we can create the Java controller:
public class AppPanel extends HBox {

    @FXML Label status;
    @FXML TextField nameField;
    @FXML TextField emailField;
    @FXML TextArea commentsField;

    @FXML public void onOK() {
        status.setText(computeStatus());
    }

    @FXML public void onCancel() {
        status.setText("You cancelled");
    }

    private String computeStatus() {
        return "Name: " + nameField.getText() + ", Email: " + emailField.getText() +
                ", Comments: " + commentsField.getText();
    }

}

Notice that because the FXML document has a HBox tag as its root, the controller it connects to must itself extend HBox.
Also, we need to mark fields that should be "injected" by the FXML document loader with the @FXML annotation.
Annotating the action methods, though, is optional. I like to do it to make it clearer that those methods are supposed to be called from the FXML view.

To load the FXML document, we use JavaFX's FXMLLoader as shown below:
public class FXMLFxApp extends Application {


    @Override
    public void start( Stage primaryStage ) throws Exception {
        HBox root = FXMLLoader.load(this.getClass().getResource("/fxml/SimpleFxApp.fxml"));
        Scene scene = new Scene( root, 500, 400 );
        primaryStage.setScene( scene );
        primaryStage.centerOnScreen();
        primaryStage.show();
    }

    public static void main( String[] args ) {
        Application.launch( FXMLFxApp.class );
    }

}


For another example of a FXML view, have a look at the Automaton JavaFX demo (screenshot here)

From the perspective of testing, whether you write your application in pure Java or FXML does not matter at all. So let's continue with how we can test this little application.

Testing the UI with Automaton


Now, as a tester, we probably want to verify the behaviour of our simple application, which is just:

1 - when the user clicks on OK, the status label shows a summary of all form fields.
2 - when the user clicks on Cancel, the status label shows the text "You cancelled".

Contrived, without a doubt, but should be enough to show how Automaton can automate simple tests, and hopefully will motivate readers to try Automaton on their own applications and hopefully contribute with more advanced use-cases, which Automaton should be able to handle just as well.

So, for case 1 above, our test will look like this:

txtFields = fxer.getAll( 'type:TextField' )
assert txtFields && txtFields.size() >= 2
clickOn txtFields[0]
enterText 'Renato Athaydes'
clickOn txtFields[1]
enterText 'renato@email.com'
clickOn 'type:TextArea'
type 'A comment from an Automaton script'
clickOn 'text:OK'

assertThat fxer['status-label'], hasText(
        'Name: Renato Athaydes, Email: renato@email.com, Comments: A comment from an Automaton script' )

println 'Test PASSED!'
  
In the first line, we ask the JavaFX driver (fxer) to get us all TextFields in the form. We do this because our UI designer (me in this case) carelessly failed to give every field an ID, which is unfortunately what happens in most real applications.
Notice the prefix type: in 'type:TextField'. When we add this prefix, we invoke one of Automaton's built-in selectors, type:, which allows us to select any Component (in Swing) or Node (in JavaFX) by its type, or class name. You may use the qualified name if more precision is required.

As we are using the default driver, which starts searching for Nodes from the root Node, this would be dangerous to do in real-life because the first TextField could be in some completely unrelated part of the UI. But soon I will show how to limit the search space to only the actual Node under test, so please trust me this is ok for now.

We then make sure we found at lest the 2 fields we expected (line 2) using Groovy's assert, then on line 3 we ask Automaton to click on the first field (arrays are 0-indexed).
Now that the name field is focused, we enter the user name, in this case my name: Renato Athaydes.

On line 5 we click on the second text field, then enter the email address (notice that the enterText method supports any String at all, regardless of the keyboard layout or region settings).

Finally, we click on the only TextArea and enter a comment.
Here, we used the type method, which actually simulates the user typing on the keyboard, and therefore is dependent on the keyboard layout to work (so prefer enterText to enter any special characters such as @ or $).

To click on the OK button, we use one of Automaton's most powerful selectors: text:. This will work to select basically anything that has a text: Button, TextField, Label, TextArea and more!

You can see that after entering text in all fields, we assert that the label with ID status-label (the only one I had the common-sense of adding an ID to) has the text we expected.
Two things deserve an explanation in this line:

The first one is that the notation fxer[ 'selector' ] (translated to fxer.getAt( "selector" ); in Java) means, roughly, get the Node matching selector (if no Node is found, a GuiItemNotFoundException is thrown. To check for existence, use
!fxer.getAll( 'selector' ).empty). When no prefix is given in the selector, as is the case here, Automaton will use the default selector for the driver, which in the case of the fxer, is the ID-selector. In JavaFX, you can select by ID using the # prefix, such as in #status-label. In fact, using this format would also work, but the # can be omitted because it is the default selector.

The other one is that assertThat is a Hamcrest assertion (which you would be familiar with if you're used to writing JUnit tests). You can use that in Automaton scripts together with Automaton's own Hamcrest matchers, such as hasText used here.

Now, we can easily write the second test:

clickOn 'text:Cancel'

assertThat fxer['status-label'], hasText('You cancelled')

println 'Test PASSED!'

Can't get any simpler, but that's all there is to it.

Code completion/highlighting with IntelliJ

One really cool feature of Automaton is the code-completion and highlighting for AScripts available in IntelliJ IDEA simply by adding Automaton as a dependency of your project (any file matching *AScript.groovy will be covered). Click here for a screenshot.

Running the tests

Using the Automaton java-agent via the command-line

This is probably the simplest way to run your test scripts (eg. myAScript.groovy) if you do not want to rely on an IDE to run tests for you. You simply launch your jar (eg. my-app.jar) using the standard java command and Automaton's Java Agent (which needs to be given the path to the AScript to execute):

java -javaagent:Automaton-1.x-all-deps.jar=myAScript.groovy -jar my-app.jar
If you have several AScript files to execute, you can simply give the Java agent the path to a directory containing all your files (only .groovy files are executed) as explained in the documentation.

Using JUnit (or any other test framework)


First of all, make sure to add a dependency to Automaton in your project. For example, in Gradle (with the JCenter repository enabled):
testCompile "com.athaydes.automaton:Automaton:1.1.0"
In the example below, we assume we saved our scripts under src/test/groovy/firstAScript.groovy and src/test/groovy/secondAScript.groovy.

Then, create the following test class (written in Java here):

public class SimpleFxAppTest {


    static class ScriptOutputCapturer {
        List<String> strings = new ArrayList<>();

        public void write(String s) {
            strings.add(s);
        }

        public String lastLine() {
            return strings.get(strings.size() - 2); // last line is just new-line
        }
    }

    @Before
    public void setup() {
        FXApp.startApp(new SimpleFxApp());
    }

    @Test
    public void firstTest() {
        runScript("src/test/groovy/firstAScript.groovy");
    }

    @Test
    public void secondTest() {
        runScript("src/test/groovy/secondAScript.groovy");
    }

    public void runScript(String path) {
        ScriptOutputCapturer writer = new ScriptOutputCapturer();
        AutomatonScriptRunner.getInstance().run(path, writer);

        System.out.println("Writer: " + writer.strings);

        assertThat(writer.strings.isEmpty(), is(false));
        assertThat(writer.lastLine(), is("Test PASSED!"));
    }

}

As you can see, now each AScript has become its own JUnit test! Using this approach, you could use any testing framework you wish.

The only mechanism Automaton currently offers for the Java code to know whether or not the test passed is by checking its output (that's why class StringOutputCapturer is needed). However, I plan to improve on this in future Automaton versions (and would appreciate any feedback on this).

Using Automaton's advanced features

Selecting the right thing

It is very important that when selecting Nodes (or JComponents in Swing), you restrict the search space if there is any chance of your selector matching more than one Node.
To do this is very simple in Automaton. First, you need some means to get the main Node of interest, which you could hopefully do by using the ID-selector (or some advanced selector as described below).
You can then create a new driver (FXer in JavaFX, Swinger in Swing, SwingerFXer in mixed applications) from that Node as shown below:

Node compUnderTest = fxer.getAt( "#component-under-test" );
FXer newDriver = FXer.getUserWith( compUnderTest );
Using newDriver, Automaton will ONLY look at Nodes inside the hierarchy of compUnderTest, which makes your tests much safer!

Creating your own selectors

Automaton has some more advanced features. For example, you can create your own selectors, as the following code sample shows:

Map<String, AutomatonSelector<Node>> customSelectors = new LinkedHashMap<>();

customSelectors.put( "$", new SimpleFxSelector() {
  @Override
  public boolean followPopups() {
    return false;
  }

  @Override
  public boolean matches( String selector, Node node ) {
    return node.getStyle().contains( selector );
  }
} );
customSelectors.putAll( FXer.getDEFAULT_SELECTORS() );

fxer.setSelectors( customSelectors );

Node blueNode = fxer.getAt( "$blue" );
With the above custom selector, you now have a style selector (in just a few lines of code)!

Notice that you can even change the default selector used by the driver (the first selector in the map is used as the default - that's why we need to use a LinkedHashMap which keeps entries in insertion order).

Complex selectors

You can also use the Complex selectors matchingAny or matchingAll to combine other selectors.
swinger.clickOn( matchingAll( "type:TextField", "txt-field-1" ) )
       .moveTo( matchingAny( "text:Dont care about case", "text:Dont Care About Case" ) )


Writing test code in Java

Even though the examples shown in this blog post were all written using the AScript DSL (which is a Groovy DSL), Automaton can also be used in pure Java code to achieve the same results through a fluent API:

swinger.clickOn( "button-name" ).pause( 250 )
       .drag( "text:Drag me to the inbox" )
       .onto( "type:" + InboxWidget.class.getName() ).waitForFxEvents();

Complete project with source and Automaton tests


A complete Gradle project, containing all the source shown in this blog post, is available on GitHub! Please feel free to try Automaton and if you find issues, don't hesitate to contact me or create issues on the issue tracker!


Comments