Warning: Declaration of Bootstrap_Walker_Nav_Menu::start_lvl(&$output, $depth) should be compatible with Walker_Nav_Menu::start_lvl(&$output, $depth = 0, $args = Array) in /homepages/10/d412081534/htdocs/clickandbuilds/WordPress/wordpress/wp-content/themes/stanleywp/functions/function-extras.php on line 61
Architecture | buildpath.de

JavaFX – Decouple the View and its behavior to create a testable UI

The behavior of an UI is a complex state machine which should be tested. This can be done not only by automated UI-tests, but also with Unit Tests. It’s not easy to write Unit Tests for UIs,  because you need to decouple the state of the UI from its declaration.

One approach is the MVVM pattern. MVVM introduces a ViewModel layer that contains the state and behavior of an UI. The View only represents the ViewModel and reacts on its changes.

Shortfacts MVVM:

  • View does not know the Model
  • Model does not know anybody ;-)
  • ViewModel does not know the View (one difference to MVP)
  • ViewModel knows Model

 

Let’s try it. We want to build a login component which has two text fields (name/password) and a login button. The button should only be active when there is some text in the text fields. You can find the example on GitHub.

Step 1 - Write tests to specify the ViewModel

Because the ViewModel represents the UI state, we create the ViewModel first.Testdriven. Therefore we write an unit test to specify the behavior. The ViewModel should provide three properties which can be accessed

  • userNameProperty() – represents the current user input for the username
  • passwordProperty() - represents the current user input for the password
  • loginPossibleProperty() - represents whether a login is possible based on conditions (username and password not empty)

 

public class LoginViewModelTest {
	@Test
	public void disableLoginButton() throws Exception {
		LoginViewModel loginViewModel = new LoginViewModel();
		// No username and password were set
		Assert.assertFalse(loginViewModel.isLoginPossibleProperty().get());

		loginViewModel.userNameProperty().set("Stefanie Albrecht");
		// username was set, but no password
		Assert.assertFalse(loginViewModel.isLoginPossibleProperty().get());

		loginViewModel.passwordProperty().set("<3");
		// username and password were set
		Assert.assertTrue(loginViewModel.isLoginPossibleProperty().get());
	}
}

Step 2 - Implement the ViewModel, so the tests can pass

Yea – we have a test, but the test won’t compile and pass, if we don’t provide a proper implementation. So lets implement the ViewModel.

The userName and the password properties are representing the current input of upcoming text fields, so they are of the type StringProperty. The loginPossible property signals whether the user provided some input for the username and password field. It should be true, if both other properties have values.

public class LoginViewModel {

	private final StringProperty userName = new SimpleStringProperty();
	private final StringProperty password = new SimpleStringProperty();
	private final ReadOnlyBooleanWrapper loginPossible = new ReadOnlyBooleanWrapper();

	public LoginViewModel() {
                //Logic to check, whether the login is possible or not
		loginPossible.bind(userName.isNotEmpty().and(password.isNotEmpty()));
	}

	public StringProperty userNameProperty() {
		return userName;
	}

	public StringProperty passwordProperty() {
		return password;
	}

	public ReadOnlyBooleanProperty isLoginPossibleProperty() {
		return loginPossible.getReadOnlyProperty();
	}
}

Step 3 - Implement the View and bind it to the ViewModel

We can implement the View after the unit test for the ViewModel passed. I suggest to create a FXML-file by using the Scene Builder first. Now we create the JavaFX-Controller which is also part of the View layer in MVVM. You can view both files in the next code box by clicking on the tabs of the box. Let’s connect the View and the ViewModel by data binding, which ensures the propagation of changes in the properties. As we want the login button to be deactivated when there is no input in the text fields, we have to bind the disableProperty() of the login button to the isLoginPossibleProperty() in the ViewModel.

Login with Scene Builder

public class LoginView {

	@FXML
	private TextField userNameTextField;

	@FXML
	private PasswordField passwordTextField;

	@FXML
	private Button loginButton;

	@FXML
	void initialize() {
                //Create the ViewModel - In production this should be done by dependency injection
		LoginViewModel loginViewModel = new LoginViewModel();
                //Connect the ViewModel
		userNameTextField.textProperty().bindBidirectional(loginViewModel.userNameProperty());
		passwordTextField.textProperty().bindBidirectional(loginViewModel.passwordProperty());
		loginButton.disableProperty().bind(loginViewModel.isLoginPossibleProperty().not());
	}
}
<AnchorPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="de.saxsys.mvvm.LoginView">
 <children>
  <VBox alignment="CENTER_RIGHT" spacing="10.0">
   <children>
    <TextField fx:id="userNameTextField" alignment="BOTTOM_RIGHT" promptText="Username" />
    <PasswordField fx:id="passwordTextField" alignment="CENTER_RIGHT" promptText="Password" />
    <Button fx:id="loginButton" mnemonicParsing="false" onAction="#onLoginButtonPressed" text="Login" textAlignment="CENTER" />
   </children>
   <padding>
    <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
   </padding>
  </VBox>
 </children>
 <opaqueInsets>
 <Insets />
 </opaqueInsets>
</AnchorPane>

If the user enters a username or password the changes will be propagated by data binding from the View to the ViewModel. The ViewModel reevaluates the defined binding of the isLoginPossibleProperty(). A possible change of the isLoginPossibleProperty()  will be propagated to the disabledProperty() of the loginButton .

As you can see, we removed every logic from the view. The view is stupid and the behavior testable.

The example can be found on GitHub.