Unit Testing Data Bindings

Explanation

The Caliburn.Testability assembly contains various classes related to unit testing a UI. You can use the BindingValidator class to validate data bindings on DependencyObjects, DataTemplates and Styles. The easiest way to access this functionality is through the Validator factory.

Note: This feature is only available to WPF due to limitations in the Silverlight API.

Prerequisites

In order to correctly test WPF user interfaces, you should insure that your tests are running in an STA thread. Check the documentation for your unit testing framework to insure that your tests are configured to run properly. If your testing framework does not support this feature, take a look at this blog.

Usage

The static Validator class has several methods for the most common scenarios related to unit testing of data bindings. See below for examples of the most common usage patterns.

General UI Validation
[TestFixture]
public class The_UI_for_adding_a_new_customer
{
    [Test]
    public void is_data_bound_to_a_CustomerModel()
    {
        var validator = Validator.For<AddNewCustomerForm, CustomerModel>();
        var result = validator.Validate();
 
        Assert.That(result.HasErrors, Is.False, result.ErrorSummary);
    }
}

The Validator class has several 'For' methods. In this case we are using the overload that takes a type parameter for the UI and a type parameter for the bound data type. Simply call the Validate method and check the results to insure that all your binding expressions are correct. The ErrorSummary contains a textual representation of any errors that are found. The framework will walk the entire UI hierarchy checking data bindings on all dependency properties. It will check all elements for inline DataTemplates (such as ItemsControl.ItemTemplate) and, if found, check the template. All inline styles will be checked. Triggers on both DataTemplate and Style will also be checked.

Insure a Specific Property Was Bound
[TestFixture]
public class The_UI_for_adding_a_new_customer
{
    [Test]
    public void is_data_bound_to_the_CustomerModel_FirstName()
    {
        var validator = Validator.For<AddNewCustomerForm, CustomerModel>();
        var result = validator.Validate();
 
        Assert.That(result.WasBoundTo(x => x.FirstName));
    }
}

By using the WasBoundTo method, a test can use lambda expressions to do strongly typed validation of specific properties. There is also a WasNotBoundTo method for convenience purposes.

Validate a Resource
[TestFixture]
public class The_UI_for_adding_a_new_customer
{
    [Test]
    public void is_data_bound_to_a_CustomerModel()
    {
        var validator = Validator.For<MyResources, CustomerModel>("addNewCustomerTemplate");
        var result = validator.Validate();
 
        Assert.That(result.HasErrors, Is.False, result.ErrorSummary);
    }
}

In the above example, the validator creates the ResourceDictionary called MyResources and then locates the resource with key "addNewCustomerTemplate." It then determines whether the resource is a DataTemplate, Style or DependencyObject and runs the appropriate validation.

Note: For many UI testing scenarios, you may need to customize the way the validator traverses the UI. This can be accomplished by changing properties on the validator's Settings. For details of the defaults and meaning of these properties see Element Enumerator Settings.

Assertion Syntax

In addition to the syntax shown above (using NUnit), the ValidationResult<T> class, return by calling the Validate() method of a validator, has a number of Assert* methods that can be leveraged for less verbose unit testing. The methods are as follows and are self-explanatory:
  • AssertNoErrors
  • AssertWasBound
  • AssertWasNotBound

If the assertion of any of these methods fails, a ValidationException will be thrown containing the details of the failure in its Message property.

Resource Dependencies

If the control you are testing is dependent on application level resources, you need make sure that these are in place, or you will not be able to test the UI. To do this, you must call the InitializeComponent method of your application. There is a catch though. You can only create one instance of your application per AppDomain. I recommend creating a test base class that does this in its static constuctor. This will ensure that the application is instantiated and InitializeComponent is called exactly once before any tests run. Below is a sample of what this might look like:

Base Class and Test Sample
using Caliburn.Testability;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
using TestUI;
 
namespace Tests
{
    public class TestBase
    {
        static TestBase()
        {
            var app = new MyApplication();
            app.InitializeComponent();
        }
    }
 
    [TestFixture]
    public class TestCase : TestBase
    {
        [Test]
        public void Test()
        {
            var validator = Validator.For<MyUserControl, MyModel>();
            var result = validator.Validate();
 
            Assert.That(result.HasErrors, Is.False, result.ErrorSummary);
        }
 
        [Test]
        public void Test2()
        {
            var validator = Validator.For<MyUserControl, MyModel>();
            var result = validator.Validate();
 
            Assert.That(result.HasErrors, Is.False, result.ErrorSummary);
        }
 
        [Test]
        public void Test3()
        {
            var validator = Validator.For<MyUserControl, MyModel>();
            var result = validator.Validate();
 
            Assert.That(result.HasErrors, Is.False, result.ErrorSummary);
        }
    }
}

There is another mechanism which can be used for handling scenarios with shared resources. It is a bit more intrusive, but may be the only way to get things working in some specific cases. This second technique involves providing constructor overloads for controls that use shared resources. The overloads are specifically designed to aid in unit testing. See below:

UI Code
using System.Windows.Controls;
 
namespace TestUI
{
    public partial class MyUserControl : UserControl
    {
        public MyUserControl()
            : this(false) { }
 
        public MyUserControl(bool isUnitTesting)
        {
            if (isUnitTesting)
            {
                var resource = new CommonResources();
                resource.InitializeComponent();
 
                Resources.MergedDictionaries.Add(resource);
            }
 
            InitializeComponent();
        }
    }
}
Test Code
using Caliburn.Testability;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
using TestUI;
 
namespace Tests
{
    [TestFixture]
    public class TestCase
    {
        [Test]
        public void Test()
        {
            var validator = Validator.For<MyUserControl, MyModel>(new MyUserControl(true));
            var result = validator.Validate();
 
            Assert.That(result.HasErrors, Is.False, result.ErrorSummary);
        }
    }
}

Last edited Oct 23, 2009 at 6:55 PM by EisenbergEffect, version 13