4 March 2020

TDD a Vaadin UI

As Code Cop I am working to get teams into adopting TDD. I use a mixture of classic training, guided workshops, Coding Dojos, code reviews and coaching to make this happen. One of my clients is working with Vaadin and keeps using it as an excuse to not use TDD. Not good. After my recent experiments with test driving user interfaces I "hijacked" one of our mob programming sessions and we worked on the Login Form Kata.

Vaadin
The client uses Vaadin 8 and the web has several ideas how to test (drive) it:
  • Use the Model View Presenter pattern, see Vaadin Advanced Application Architectures.
  • Gradually separate the UI from the logic, thinking of MVP more as a process than a pattern, starting with separate methods, which are accessible to tests so that the test can invoke them. For more details see Is MVP a Best Practice?
  • Create factories for all UI components so that tests can mock the UI. See this answer on StackOverflow for more details.
  • Running integrated tests in a test bed to simulate UI.​getCurrent() or VaadinSession.​getCurrent(), e.g. using the Karibu-Testing library.
LoginPresenter
We used MVP as described in my previous article. The group decided against a view model, because there was no specific UI model. Next was the presenter. The first two tests made us implement the requirement for a successful login: User name and password given, button "Log in" clicked, back end reports success, then the form is closed.
import org.mockito.Mockito;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class LoginPresenterTest {

  public static final String USERNAME = "Peter";
  public static final String PASSWORD = "Slovakia";

  private AuthenticationService authenticationService;
  private NavigatorView navigatorView;
  private LoginView loginView;
  private ExecutorService executorService;

  private LoginPresenter loginPresenter;

  @BeforeEach
  public void init() {
    authenticationService = Mockito.mock(AuthenticationService.class);
    navigatorView = Mockito.mock(NavigatorView.class);
    loginView = Mockito.mock(LoginView.class);
    executorService = Executors.newSingleThreadExecutor();

    loginPresenter = new LoginPresenter(authenticationService, navigatorView,
                                        loginView, executorService);
  }

  @Test
  public void should_call_backend_on_button_click() throws InterruptedException {
    Mockito.when(authenticationService.authenticate(USERNAME, PASSWORD))
      .thenReturn(new AuthenticationResult(true, "we do not care!"));

    loginPresenter.login(USERNAME, PASSWORD);
    waitForAuthentification();

    Mockito.verify(authenticationService, Mockito.times(1))
      .authenticate(USERNAME, PASSWORD);
  }

  @Test
  public void should_navigate_on_login_success() throws InterruptedException {
    Mockito.when(authenticationService.authenticate(USERNAME, PASSWORD))
      .thenReturn(new AuthenticationResult(true, "we do not care!"));

    loginPresenter.login(USERNAME, PASSWORD);
    waitForAuthentification();

    Mockito.verify(navigatorView, Mockito.times(1))
      .navigateToDashBoard();
  }

  private void waitForAuthentification() throws InterruptedException {
    executorService.shutdown();
    executorService.awaitTermination(1, TimeUnit.SECONDS);
  }
Using an ExecutorService made the asynchronous execution pretty simple. I like how they wrote two tests for the different aspects of successful login. Using smaller steps helped to get some logic sooner. It turned out that we needed another component for navigation, so we made up the NavigatorView. Then we went for the requirements User name and password given, button "Log in" clicked, back end reports an error, show message in error line, form stays open and While the back end is working, the "Log in" button stays disabled.
import org.mockito.InOrder;
import static org.mockito.ArgumentMatchers.any;

...

  @Test
  public void should_display_error_on_login_error() throws InterruptedException {
    String errorMessage = "error message";
    Mockito.when(authenticationService.authenticate(USERNAME, PASSWORD))
      .thenReturn(new AuthenticationResult(false, errorMessage));

    loginPresenter.login(USERNAME, PASSWORD);
    waitForAuthentification();

    Mockito.verify(navigatorView, Mockito.never())
      .navigateToDashBoard();
    Mockito.verify(loginView, Mockito.times(1))
      .displayErrorMessage(errorMessage);
  }

  @Test
  public void should_register_to_view() {
    Mockito.verify(loginView, Mockito.times(1))
      .addListener(any(LoginListener.class));
  }

  @Test
  public void should_disable_button_while_backend_is_working() throws InterruptedException {
    Mockito.when(authenticationService.authenticate(USERNAME, PASSWORD))
      .thenReturn(new AuthenticationResult(false, "we do not care!"));

    loginPresenter.login(USERNAME, PASSWORD);

    InOrder inOrder = Mockito.inOrder(loginView);
    inOrder.verify(loginView).setLoginButtonEnabled(false);
    waitForAuthentification();
    inOrder.verify(loginView).setLoginButtonEnabled(true);
  }
}
The finished presenter class looked almost like the one for Swing.
import java.util.concurrent.ExecutorService;

public class LoginPresenter implements LoginListener {

  private final AuthenticationService authenticationService;
  private final NavigatorView navigatorView;
  private final LoginView loginView;
  private final ExecutorService executorService;

  public LoginPresenter(AuthenticationService authenticationService,
                        NavigatorView navigatorView, LoginView loginView,
                        ExecutorService executorService) {
    this.authenticationService = authenticationService;
    this.navigatorView = navigatorView;
    this.loginView = loginView;
    this.executorService = executorService;

    loginView.addListener(this);
  }

  @Override
  public void login(String username, String password) {
    loginView.setLoginButtonEnabled(false);
    executorService.submit(() -> invokeLoginService(username, password));
  }

  private void invokeLoginService(String username, String password) {
    AuthenticationResult authenticate =
      authenticationService.authenticate(username, password);
    loginView.setLoginButtonEnabled(true);
    if (authenticate.success) {
      navigatorView.navigateToDashBoard();
    } else {
      loginView.displayErrorMessage(authenticate.message);
    }
  }
}
And the presenter would hide behind the LoginListener interface.
public interface LoginListener {

  void login(String username, String password);

}
Test Bed
As for the Swing version, using a test bed helped testing the view. There is a useable Vaadin testing tool, the Karibu-Testing library. It worked out of the box, no special treatment necessary. We wrote tests that the view forwards actions to the LoginListener, i.e. the LoginPresenter, and that the view can display an error message.
import com.github.mvysny.kaributesting.v8.MockVaadin;
import com.vaadin.ui.Button;
import com.vaadin.ui.Label;
import com.vaadin.ui.TextField;
import org.mockito.Mockito;

import static com.github.mvysny.kaributesting.v8.LocatorJ.*;

class LoginViewTest {

  private final LoginViewImpl content = new LoginViewImpl();

  @BeforeEach
  void beforeEach() {
    MockVaadin.setup(() -> new HelloUI(content));
  }

  @AfterEach
  void afterEach() {
    MockVaadin.tearDown();
  }

  @Test
  void should_pass_on_button_click() {
    LoginListener loginListener = Mockito.mock(LoginListener.class);
    content.addListener(loginListener);

    // simulate a text entry as if entered by the user
    _setValue(
      _get(TextField.class, spec -> spec.withCaption("Phone, email or username")),
      "peter");

    _setValue(
      _get(TextField.class, spec -> spec.withCaption("Password")),
      "Slovakia");

    // simulate a button click as if clicked by the user
    _click(_get(Button.class, spec -> spec.withCaption("Log in")));

    Mockito.verify(loginListener).login("peter", "Slovakia");
  }

  @Test
  void should_display_error_message() {
    content.displayErrorMessage("myError message");

    // simulate a text entry as if entered by the user
    Label label = _get(Label.class, spec -> spec.withValue("myError message"));

    Assertions.assertEquals(label.getStyleName(), "warning");
  }
}
Satisfying these two tests gave us the following view:
import com.vaadin.ui.*;

public class LoginViewImpl extends VerticalLayout implements LoginView {

  private final TextField username = new TextField("Phone, email or username");
  private final TextField password = new PasswordField("Password");
  private final Button loginButton = new Button("Log in");
  private final Label errorLabel = new Label();

  public LoginViewImpl() {
    addComponent(username);
    addComponent(password);
    addComponent(loginButton);
    addComponent(errorLabel);
    errorLabel.setStyleName("warning");
  }

  @Override
  public void addListener(LoginListener loginListener) {
    loginButton.addClickListener(clickEvent -> {
      loginListener.login(username.getValue(), password.getValue());
    });
  }

  @Override
  public void displayErrorMessage(String errorMessage) {
    errorLabel.setValue(errorMessage);
  }

  // ...
}
That was as far as we went during the sessions. We did not finish the whole login window, next tests would drive the logic of setLoginButtonEnabled(boolean).

Conclusion
Besides small differences, the code looks much like the Swing version. The process worked the same, most tests do not deal with the UI. As I do not know many UI frameworks my conclusion is limited. Maybe - hopefully - probably this approach works for state based user interfaces as long as there is a reasonable test bed. I can see how it could work for C# WinForms. On the other hand, Vaadin and WinForms are very similar to Java's Swing. I need to look at other kind of user interfaces.

No comments: