Unit Testing the UI

With the Model-View-Presenter pattern, Windows Forms 2.0, and automatic data binding, you can build frameworks for unit testing the user interface.


July 02, 2007
URL:http://www.drdobbs.com/tools/unit-testing-the-ui/200001974

Jordan is responsible for front-end development at Liquidnet, an electronic equities trading venue focusing on buy-side institutions. He can be contacted at [email protected].


By using the Model-View-Presenter pattern, Windows Forms 2.0, and automatic data binding, you can create a framework for unit testing the user interface. The pattern enforces a separation of concerns that keeps the UI thin, focusing only on visualization of data and binding to a presentation model. In this article, I present a basic calculator application that leverages this architecture and demonstrates the ease of unit testing from this framework. (The complete source code for the calculator is available electronically; see "Resource Center," page 5.)

For the purposes of this article, you can view the Model-View-Presenter (MVP) pattern as a means to decouple presentation logic from the actual view itself. The goal should be a simple 1-1 mapping between properties in a presenter class and properties in a view class. For example, for each text box display, there should be a corresponding property in the presenter class. If the output requires advanced calculation or formatting to occur prior to display, the model and presenter classes should take care of the data prior to updating the view. Likewise, simple edits should immediately notify the model and presenter of an update in order to drive a new display. In many cases, as well as our simple example, the model and presenter classes can merge into a heavy model class. Because there is little code required to convert the model into presentable data, the model can handle the dual responsibilities of data storage and presentation logic.

The Calculator Application

Figure 1 illustrates a basic calculator form bound to a calculator presentation model.

[Click image to view at full size]

Figure 1: Calculator form with calculator model.

Each data element for display and edit from the view is described by a property, and each button-click action is represented by a parameterless method in the calculator model. By following the MVP pattern, you ensure that the view is focused on visualizing data as well as posting and receiving updates to and from the model.

Listing One includes the code for two of the calculator model's properties. In the listing, the setter for a property notifies consumers once a change has been committed. The Calculator class implements the INotifyPropertyChanged interface from .NET 2.0 in order to support firing events for property changes. In the case of Operand2, CanDivide is updated once Operand2 is updated, so two events are fired.

public class Calculator : INotifyPropertyChanged
{
    public decimal Operand2
    {
        get { return _operand2; }
        set
        {
            if (value == _operand2)
                return;
            _operand2 = value;
            OnPropertyChanged("Operand2");
            OnPropertyChanged("CanDivide");
        }
    }
    public bool CanDivide
    {
        get { return (this.Operand2 != 0); }
    }
    ...
}
Listing One

For these two model properties, there are analogous properties in the calculator form; see Listing Two. These properties in the view demonstrate the concern of visualizing this state of the model.

 
public partial class CalculatorForm : Form
{    
    public decimal Operand2
    {
        get { return Convert.ToDecimal(_operand2TextBox.Text); }
        set { _operand2TextBox.Text = value.ToString(); }
    }
    public bool CanDivide
    {
        get { return _divideButton.Enabled; }
        set { _divideButton.Enabled = value; }
    }
 ...
}   

Listing Two

Automatic Data Binding

Once you've created a full set of properties in the model and view, you can apply the data binding in the CalculatorForm. In Listing Three, you can bind the Text property of the text box to the Operand2 property of the model, and the Enabled property of the button to the CanDivide property of the model. These bindings enforce that when the Text in the view changes, the model is updated. When the CanDivide property changes, the Divide button is enabled/disabled.

public partial class CalculatorForm : Form
{
    public CalculatorForm()
    {
        InitializeComponent();
        InitializeBindings();
        this.Calculator = new Calculator();
    }
    private void InitializeBindings()
    {      
        _divideButton.DataBindings.Add(
        "Enabled", _calculatorBindingSource, 
            "CanDivide", false,
             DataSourceUpdateMode.OnPropertyChanged);
        _operand2TextBox.DataBindings.Add(
            "Text", _calculatorBindingSource,
            "Operand2", false, DataSourceUpdateMode.OnPropertyChanged);
    }
 ...
}
Listing Three

Unit Testing the User Interface

Using MVP and automatic data binding, unit tests for the view become straightforward. Because the view is now responsible for only visualization of values and responding to binding events, you can cover most of the functionality of the view with two unit tests for each display property:

In order to run a unit test for the view, it is necessary to launch the Form or Control, and inspect its state by inspecting the child controls within the Form or Control. Because child controls within a parent are typically represented by private member variables, you can leverage a Tester class that extracts the controls from the parent. In Listing Four, the GUITester class uses reflection to traverse the parent's hierarchy to retrieve the specified control by the control's Name property.

public static class GUITester
{
    public static T FindControl<T>(Control container, string name)
          where T : Control
    {
        if (container is T && container.Name == name)
            return container as T;
        if (container.Controls.Count == 0)
            return null;
        foreach (Control child in container.Controls)
        {
            Control control = FindControl<T>(child, name);
            if (control != null)
                return control as T;
        }
        return null;
    }
    #endregion
}

Listing Four

Once you can retrieve the child controls, it is straightforward to create a unit test to validate this visualization of a property. All you need to do is launch the form, update the property, and validate the state of the control; see Listing Five.

[TestFixture]
public class CalculatorFormTest
{
    [Test]
    public void TestVisualizingOperand2()
    {
        CalculatorForm target = new CalculatorForm();
        target.Show();
        TextBox operand2TextBox =
           GUITester.FindControl<TextBox>(target, "_operand2TextBox");
        _target.Operand2 = 100;
        Assert.AreEqual(target.Operand2.ToString(),
            _operand2TextBox.Text);
        target.Close();
    }
}

Listing Five

Now that you have test coverage for the visualization, you need to ensure that binding works correctly. This can be achieved with a simple unit test validating that the model and view are kept in sync, as in Listing Six.

 
[TestFixture]
public class CalculatorFormTest
{    
    [Test]
    public void TestBindingOperand2()
    {
        CalculatorForm target = new CalculatorForm();
        Calculator model = target.Calculator();
        target.Show();
        // Update the UI and validate the model
        target.Operand2 = 7;
        Assert.AreEqual(target.Operand2, model.Operand2);
        // Update the model and validate the UI
        model.Operand2 = 9;
        Assert.AreEqual(model.Operand2, target.Operand2);
        target.Close();
    }
}
Listing Six

As long as you create unit tests of this type for each display property, you can add a significant amount of coverage with little unit test code.

Conclusion

By leveraging MVP and automatic data binding, you can create simple unit tests that cover most of the responsibilities of the view. While it is true that some view operations, such as drag-and-drop, are difficult to test, binding and visualization are at the heart of what is happening in a view.

In addition, you can view the wrapper properties used in the example as superfluous. Although it is not necessary to wrap each text box with its own display property, the example serves to demonstrate the simplicity of creating the one-to-one mapping between the presentation property and the view property. How you apply the rule to each new application is up to you.

As long as you adhere to the spirit of MVP, you will find a straightforward approach to unit testing the view.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.