Showing posts with label UI testing. Show all posts
Showing posts with label UI testing. Show all posts

Wednesday, March 31, 2010

Passive Views and View Interfaces

User interfaces are inherently difficult to test since logic buried in the GUI can't really be tested without bringing up the entire system, and this is not really practical for unit (or even integration) tests. It's common to say "Extract the logic outside the GUI" but in practice, I don't see it done as much as I think it should be.
First, let's distinguish between a passive view and and active view. An active view tends to register itself with the model, listen for changes, and update itself. The following code would be typical in an active view:

public class MyView {

public void init() {
model.addPropertyChangeListener(this);
}

public void propertyChanged(PropertyChangeEvent evt) {
String propName = evt.getPropertyName();
if ( propName.equals("prop1") ) {
updateViewWithProp1Change();
}
else if ( propName.equals("prop2")) {
updateViewWithProp2Change();
}
// ... etc ...
}
}

Typically the updateViewWithPropXXXChange methods would read the new value of the property and determine what to display on the view. To give a concrete example, imagine a view that updates a temperature display.

public void updateTemperatureDisplay(double newTempFarenheit) {
if ( mode.equals(FARENHEIT) ) {
tempLabel.setValue(Double.toString(newTempFarenheit));
}
else if ( mode.equals(CELSIUS) ) {
tempLabel.setValue(Double.toString(toCelsius(newTempFarenheit)));
}
}

In this example, the view does the conversion to Celsius. This logic is now difficult to test without showing the temperature display. This view is active. It's fast to write because the logic is contained in here, but difficult to test.
A passive view on the other hand can be thought of as a "dumb" view. It displays what it is told.

public interface MyPassiveView {
void setTemperatureText(String value);
}

public class MyPassiveVewImpl implements MyPassiveView {
public void setTemperatureText(String value) {
tempLabel.setText(value);
}
}
public class MyController {
private MyPassiveView view;

public void updateTemperatureDisplay(double newTempFarenheit) {
String temperatureText = "";
if ( mode.equals(FARENHEIT) ) {
temperatureText = Double.toString(newTempFarenheit);
}
else if ( mode.equals(CELSIUS) ) {
temperatureText = Double.toString(toCelsius(newTempFarenheit)));
}
else { // handle error }
view.setTemperatureText(temperatureText);
}

Now all the view does is set the label text. This is more work but the controller can be independently tested from the view. Notice in this example also the view implements an interface. View interfaces greatly simplify testability and make many of these patterns possible. I think this is frequently overlooked by developers and the view ends up being a concrete class.

There are various patterns that fall along this line, notably model-view-controller, model-view-presenter, and supervising controller all with varying degrees of passiveness. These can all be found on Martin Fowler's website (http://martinfowler.com/), and many other results can be found by a simple search.

Tuesday, September 8, 2009

Automated UI Testing

I've often been asked how to automate testing for a UI. There isn't an easy solution to this problem, and automated UI testing is very difficult. An application's UI is frequently changing, sometimes just minor layout changes, and sometimes major reworks. Regardless, the change can be enough to throw off a lot of automated UI testing tools. So does that mean we shouldn't test the UI?

Every situation is different, but these are a few tips that will help make automated UI testing easier:

  1. Make the UI as thin as possible. Don't put business logic in UI components, even though it can be tempting (patterns such as Model-View-Controller help with this). An example:
    Difficult to test:

    JButton button = new JButton("Check if even");
    button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e) {
    int num = Integer.parseInt(numberField.getText());
    if ( num % 2 == 0 ) {
    System.out.println("num " + num + " is even");
    }
    }
    });

    This is difficult to test because now the whole UI needs to be brought up just to test this function, and this is not conducive to automated testing. This can be refactored as follows:

    JButton button = new JButton("Check if even");
    button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e) {
    int num = Integer.parseInt(numberField.getText());
    if ( NumberUtils.isEven(num)) {
    System.out.println("num " + num + " is even");
    }
    }
    });

    public static class NumberUtils {
    public static boolean isEven(int num) {
    return num %2 == 0;
    }
    }

    While this may not look like a big deal, the change is signficant. The NumberUtils class can have a simple unit test written for it and now that code is being tested outside the UI. It's easy to see that as functions become more complex, it becomes even more important to move them outside the UI logic.

  2. Wait until the last responsible moments to write UI tests. Don't write automated UI tests while the UI is still in a great state of flux. You'll spend more time maintaining them than you will find useful. This will vary from project to project, but I find this doesn't generally happen until later on in the project.

  3. Maintain automated UI tests like you maintain code. They should be run as part of a regression test (even if it's a nightly test as opposed to a continuous build since they may take longer to run). They generally can't be simple record/playback tests. While this is a good place to start, they really should be refactored by a developer to make sure that when the UI changes and tests break, minimal changes will be needed to the UI tests. For example, if multiple tests are looking for a particular table column, make that a static string somewhere (or even reference it from the code under test if you can and the string itself is not under test). This way if the string changes, you don't need to update the string in multiple places.
You may find that you don't have the time or resources to write automated UI tests. It's the hardest thing to test and usually gets cut from testing first. If you're not going to have automated tests for it (and even if you are), be sure that UI code only includes UI related functionality, not the algorithms being invoked by the UI.