Most modern user interface technologies are state based. Classic frameworks like Swing, JavaFX, Eclipse RCP, Windows Forms, Vaadin and many more consist of heavy weight UI widgets that manage all their state and interact with the application using event handlers. Such frameworks are harder to test than plain code. I have not found much information on how to do that, one resource that got me started is chapter eight from
Lasse Koskela's Test Driven. I am using many ideas from that book in here.
Model View Presenter (MVP) Pattern
Because these UI technologies are heavy weight, hard to test and often slow to run, one needs to decouple from them as much as possible.
The goal is to minimize all UI dependencies. As few classes as possible should be "contaminated" with UI stuff, and the code that needs to do UI should contain as little logic as possible. Common ways to separate concerns between the interface and the underlying logic are MVC, MVP, MVVM or similar architectural patterns. I am using the
MVP pattern. Its goal is - obviously -
to facilitate automated unit testing and improve the separation of concerns in presentation logic. That means MVP is used to have a very thin, dumb UI, which is called the view. The model contains the UI data or UI model which might contain information about enabled fields, active buttons and so on. The presenter is a
mediator and wires the model and the view.
Using Java and Swing
On my research on how to test drive user interfaces I started with Java and Swing. The
Wikipedia page lists several UI frameworks which use the MVP patterns themselves, Swing being one of them. That means that Swing uses MVP internally. Larger components like
JComboBox
or
JTable
have their own model classes
ComboBoxModel
and
TableModel
. This is useful because the models do not depend on the user interface part of Swing and can be TDD'ed in the usual way. It does not mean that Swing can be test driven easily. As example I will run my
Login Form Kata. I am going to develop the basic login screen step by step using TDD.
First Test: LoginModel should contain user lookup and password
I start with the model. The model contains the state of the user interface. There is no problem using TDD for that. My first test is
class LoginModelTest {
LoginModel model = new LoginModel();
@Test
void shouldContainUserLookupAndPassword() {
model.setLookup("user@server.com");
model.setPassword("secret123");
assertEquals("user@server.com", model.getLookup());
assertEquals("secret123", model.getPassword());
}
}
After
seeing it red, I add
public class LoginModel {
private String lookup;
private String password;
// getters and setters for lookup and password
}
Green. Usually I am not testing for getters and setters, but today I have to start somewhere. Depending on the used version of MVP, the model could update the view itself. I choose to keep the model very simple, following the
Passive View aka
Humble Dialog Box variant of MVP. This seems to be the usual way. In this example of login form, there is no state that goes back from the model into the view yet, e.g. if the login button should be disabled, so it does not make any difference.
Test: LoginPresenter should pass lookup and password to its model
Next is line is the
LoginPresenter
. The presenter is handling all user input. It is important, that the presenter has no dependency on Swing, too, so there is no problem TDDing that. The presenter will be notified of user input into the lookup or password field and will store the values in the model.
class LoginPresenterTest {
LoginModel model = new LoginModel();
LoginPresenter presenter = new LoginPresenter(model);
@Test
void shouldPassLookupAndPasswordToModel() {
presenter.lookupChanged("user");
presenter.passwordChanged("pass");
assertEquals("user", model.getLookup());
assertEquals("pass", model.getPassword());
}
}
public class LoginPresenter {
private final LoginModel model;
public LoginPresenter(LoginModel model) {
this.model = model;
}
public void lookupChanged(String newLookup) {
model.setLookup(newLookup);
}
public void passwordChanged(String newPassword) {
model.setPassword(newPassword);
}
}
Empty LoginView
Finally I create the
LoginView
interface. The view wraps the UI technology completely. It starts as an empty interface. The methods to come will be driven by the needs of the presenter.
public interface LoginView { }
Test: LoginPresenter should close the view on successful login
It is time to go for some real logic. When the login button is clicked, the authentication back end will be called and if the call was successful, the view should be closed. The test uses Mockito to mock the view and verify it has been called.
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class LoginPresenterTest {
LoginModel model = new LoginModel();
LoginView view = mock(LoginView.class);
AuthenticationService auth = mock(AuthenticationService.class);
LoginPresenter presenter = new LoginPresenter(model, view, auth);
@Test
void shouldCloseViewOnSuccessLogin() {
model.setLookup("user");
model.setPassword("secret");
when(auth.authenticate("user", "secret")).
thenAnswer(invocation -> {
return new AuthenticationResult(true, null);
});
presenter.loginButtonClicked();
verify(view).close();
}
}
This adds a
close()
method to the
LoginView
. Because I own the view, I can use domain names and the actual UI technology is not visible from the outside.
Test: LoginPresenter should display an error on failed login
Another test drives the display of an error message if authentication fails. This creates a
showError(String message)
method in the view.
@Test
void shouldDisplayErrorOnFailedLogin() {
model.setLookup("user2");
model.setPassword("secret2");
when(auth.authenticate("user2", "secret2")).
thenAnswer(invocation -> {
return new AuthenticationResult(false, "Login failed.");
});
presenter.loginButtonClicked();
verify(view).showError("Login failed.");
}
And so the
loginButtonClicked
method is complete.
public LoginPresenter(LoginModel model, LoginView view,
AuthenticationService authenticationService) {
this.model = model;
this.view = view;
this.authenticationService = authenticationService;
}
@Override
public void loginButtonClicked() {
AuthenticationResult result =
authenticationService.authenticate(model.getLookup(), model.getPassword());
if (result.success) {
view.close();
} else {
view.showError(result.message);
}
}
Test: LoginPresenter should call Authentication service asynchronously
As stated in the
requirements, all calls to the back end need to be asynchronous. Synchronous code would block the Swing event thread and render the UI unresponsive. So I change the tests to force the calls to be asynchronous. I need two
java.util.concurrent.CountDownLatch
s and have to wait on them to start and finish asynchronous processing.
Test Bed
The last piece is the view - the implementation of the view to be precise. To test the real view it has to be started and sent some events. This is what I wanted to avoid. It is slow and brittle. By following the MVP pattern described above, the view will have as little code as possible. There will only be a few tests executing it. Sometimes it is necessary to call certain methods on UI classes for the UI to work at all, e.g.
show
. It is not pretty. Best would be to use a test bed or harness to run these UI components in an isolated way, e.g. to have a dedicated window for the component under test and to create the application in a way that allows to exercise UI features independently.
Lasse Koskela mentions
Abbot to test stand alone AWT or Swing components. It provides helper code for finding and interacting with UI elements. See how Abbot works in one of
its tutorials. It is pretty old and its test support is for JUnit 3, but it gets the job done. It displays a frame while running the tests and there are flickering tests from time to time. This is not a problem of Abbot itself, but is the nature of full UI tests. As I said, these are brittle.
Test: SwingLoginView has a login button with text
A basic start is to assert the existence of UI elements. Abbot offers several ways to find elements, using its
getFinder().find(...)
method. An easy way to locate elements is to use ids or names, e.g. with
loginButton.setName("LoginButton")
.
import javax.swing.JButton;
import javax.swing.JPanel;
import abbot.finder.ComponentSearchException;
import abbot.finder.matchers.NameMatcher;
import junit.extensions.abbot.ComponentTestFixture;
public class SwingLoginViewTest extends ComponentTestFixture {
LoginView view = new SwingLoginPanel();
public SwingLoginViewTest(String name) {
super(name);
}
public void testHasLoginButtonWithText() throws ComponentSearchException {
showFrame((JPanel) view); // Abbot shows the view
JButton loginButton = findLoginButton();
assertEquals("Log in", loginButton.getText());
}
private JButton findLoginButton() throws ComponentSearchException {
return (JButton) getFinder().find(new NameMatcher("LoginButton"));
}
}
This tests forces me to create the initial login panel.
import javax.swing.JButton;
import javax.swing.JPanel;
public class SwingLoginPanel extends JPanel implements LoginView {
private final JButton loginButton = new JButton("Log in");
public SwingLoginPanel() {
createLoginButton();
}
private void createLoginButton() {
loginButton.setName("LoginButton");
add(loginButton);
}
@Override
public void close() {
}
// other empty LoginView methods
}
Observer Pattern
In MVP, the view delegates user input somewhere else and does nothing but rendering. We using the
Observer Pattern to get notifications from the UI. The view takes the role of subject and the presenter is observing it. Observer Pattern subjects need to manage the list of observers - in Java usually named listeners - and allow code to register and sometimes deregister them.
Test: SwingLoginView should send button click to presenterimport static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import abbot.tester.JButtonTester;
...
public void testSendButtonClickToPresenter() throws ComponentSearchException {
LoginListener listener = mock(LoginListener.class);
view.registerLoginListener(listener);
showFrame((JPanel) view);
JButton loginButton = findLoginButton();
JButtonTester tester = new JButtonTester(); // from Abbot
tester.actionClick(loginButton);
verify(listener).loginButtonClicked();
}
As I said, the presenter will be the actual observer, hiding behind the
LoginListener
interface.
public interface LoginListener {
void loginButtonClicked();
}
Its implementation is straight forward.
@Override
public void registerLoginListener(LoginListener listener) {
loginButton.addActionListener(ae -> listener.loginButtonClicked());
}
Test: SwingLoginView has input fields for lookup and password
Like for the login button, I assert the existence of lookup and password fields and that they are wired to the listener. To verify that, the test code enters some text into the fields and checks that the listener mock has been called.
public void testHasLookupField() throws ComponentSearchException {
LoginListener listener = mock(LoginListener.class);
view.registerLoginListener(listener);
showFrame((JPanel) view);
JTextField lookupField = findLookupField();
assertEquals(20, lookupField.getColumns());
// verify that it is wired as well
JTextFieldTester tester = new JTextFieldTester(); // from Abbot
tester.actionEnterText(lookupField, "user");
verify(listener).lookupChanged("user");
}
private JTextField findLookupField() throws ComponentSearchException {
return (JTextField) getFinder().find(new NameMatcher("LookupField"));
}
Yes, this is not a good unit test as it tests two things - visible by its two checks,
assert
and
verify
. I should split that into two independent tests. Unfortunately these tests are slow, so I want to minimize their number. Together with a similar test for the
JPasswordField passwordField
the view is taking shape.
private final JTextField lookupField = new JTextField(20);
private final JPasswordField passwordField = new JPasswordField(20);
private void createLookupField() {
lookupField.setName("LookupField");
add(lookupField);
}
private void createPasswordField() {
passwordField.setName("PasswordField");
add(passwordField);
}
@Override
public void registerLoginListener(LoginListener listener) {
lookupField.getDocument().
addDocumentListener(new AllDocumentListener() {
@Override
protected void fire() {
listener.lookupChanged(lookupField.getText());
}
});
passwordField.getDocument().
addDocumentListener(new AllDocumentListener() {
@Override
protected void fire() {
listener.passwordChanged(new String(passwordField.getPassword()));
}
});
loginButton.addActionListener(ae -> listener.loginButtonClicked());
}
To be notified of each input character, I use Swing's
DocumentListener
. (
AllDocumentListener
is my own base class to handle the many methods of it.) This gives the final listener for the observer:
public interface LoginListener {
void lookupChanged(String lookup);
void passwordChanged(String password);
void loginButtonClicked();
}
Test: SwingLoginView should display errors
This is the final test for the view.
public void testErrorDisplay() throws ComponentSearchException, InterruptedException {
showFrame((JPanel) view);
JLabel errorField = findErrorField();
assertEquals("", errorField.getText());
view.showError("Alert!");
Thread.sleep(10); // wait for asynchronous update
assertEquals("Alert!", errorField.getText());
assertEquals(Color.RED, errorField.getForeground());
}
private JLabel findErrorField() throws ComponentSearchException {
return (JLabel) getFinder().find(new NameMatcher("ErrorField"));
}
And the final
SwingLoginView
:
import java.awt.Color;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
...
private static final Color ERROR_COLOR = new Color(255, 0, 0);
private final JLabel errorField = new JLabel();
private void createErrorField() {
errorField.setName("ErrorField");
errorField.setForeground(ERROR_COLOR);
add(errorField);
}
@Override
public void showError(String message) {
SwingUtilities.invokeLater(() -> errorField.setText(message));
}
SwingUtilities.invokeLater()
is needed because
showError
is called asynchronously. While there is agreement in the TDD community to not assert on layout or styling, I am asserting that the foreground of the error field changed to an colour indicating the error, because it is important to indicate errors also in colour.
Test: LoginPresenter should register itself to the view
To complete the Observer Pattern, the presenter needs to register itself to the view. I started with
verify(view).registerLoginListener(Mockito.any(LoginListener.class));
but that is not what I want. I really want that the methods on the listener trigger the required functionality in the back end or model.
import org.mockito.ArgumentCaptor;
...
@Test
void shouldRegisterItselfToView() throws InterruptedException {
when(auth.authenticate(any(String.class), any(String.class))).
thenReturn(new AuthenticationResult(true, null));
ArgumentCaptor<LoginListener> argument = ArgumentCaptor.forClass(LoginListener.class);
verify(view).registerLoginListener(argument.capture());
LoginListener listener = argument.getValue();
listener.lookupChanged("user");
assertEquals("user", model.getLookup());
listener.passwordChanged("pass");
assertEquals("pass", model.getPassword());
listener.loginButtonClicked();
Thread.sleep(10);
verify(auth).authenticate("user", "pass");
}
When the presenter registers itself to the view, I capture the listener using Mockito's
ArgumentCaptor
. Then I call the listener and check the wanted behaviour. The sleep time is required because authentication is run asynchronously. Finally the presenter is complete.
public class LoginPresenter implements LoginListener {
// ...
public LoginPresenter(LoginModel model, LoginView view,
AuthenticationService authenticationService) {
this.model = model;
this.view = view;
this.authenticationService = authenticationService;
view.registerLoginListener(this);
}
// ...
}
and the final view (interface) is
public interface LoginView {
void close();
void showError(String message);
void registerLoginListener(LoginListener listener);
}
The
actual commits are here.
Conclusion
This was my first try and it worked well. I conclude that it
is possible to TDD UIs at least using Java and Swing. I have working examples and the testing tools are fair. The final code has the usual TDD benefits - more separation between concerns (i.e. logic and UI technology) and a better design using domain methods. I tested the UI elements briefly. I did not go into test driving styling, colours or positions. In Swing components have all these properties accessible, e.g. visibility, colours, positions and more. So I could have asserted them. That is nice because I could go as far as I wanted to. I am undecided if I should test colours and other styling related things. As I said, there seems to be consensus to not assert on layout or styling and I did not. In general it is still unclear (for me) how much automated testing is needed for an UI in such situations. While I did not write tests for everything, I already have a feeling that there are "lots of test but no application".