Freigeben über


Unit Testing Control Designers

One of the inherent problems of control development is that it doesn't lend itself easily to unit testing. After all, you are developing a user interface, and it's necessary to validate that it looks as expected. While that's true, more complex control logic may still benefit from some unit testing.

Recently, I found myself in a situation where I was developing a Windows control with some rather complex design-time logic, and regression errors were beginning to pop op, so I decided to see if I couldn't unit test the control designer after all.

As it turns out, it's not that difficult, and the very loosely coupled service architecture of the .NET design-time framework makes it very easy to simulate design-time behavior outside of Visual Studio.

As usual, an example is the best way to illustrate the point, so consider this simple control:

 [Designer(typeof(MyControlDesigner))]
public partial class MyControl : FlowLayoutPanel
{
    public MyControl()

    {

        InitializeComponent();

    }
}

As you can see, the only thing this control does is derive from FlowLayoutPanel and provide its own designer. The idea behind this control is to provide design-time behavior that allows the user to select two or more child controls of the control, right-click on it and select Reverse order.

Here's the initial version of the designer class:

 public class MyControlDesigner : ScrollableControlDesigner
{
    private DesignerVerbCollection designerVerbs_;
 
    public MyControlDesigner()
        : base()
    {
        this.designerVerbs_ = new DesignerVerbCollection();
    }
 
    public override DesignerVerbCollection Verbs
    {
        get { return this.designerVerbs_; }
    }
 
    public override void Initialize(IComponent component)
    {
        base.Initialize(component);
 
        IMenuCommandService mcs =
            this.GetService(typeof(IMenuCommandService))
            as IMenuCommandService;
        if (mcs != null)
        {
            DesignerVerb verb =
                new DesignerVerb("Reverse order",
                this.OnReverseOrderInvoked);
            mcs.AddVerb(verb);
            this.designerVerbs_.Add(verb);
        }
    }
 
    private void OnReverseOrderInvoked
        (object sender, EventArgs e)
    {
        throw new NotImplementedException();
    }
}

The order reversing logic isn't yet implemented, but the relevant designer verb is wired up and ready to go. Now it's possible to use test-driven development while implementing the OnReverseOrderInvoked method.

I'll show you the complete unit test first, and then I'll walk you through it:

 [TestMethod]
public void ReverseTwo()
{
    StubSite site = new StubSite();

    site.SetService(typeof(IMenuCommandService),

        new StubMenuCommandService());
 
    StubSelectionService selectionService =

        new StubSelectionService();

    site.SetService(typeof(ISelectionService),

        selectionService);
 
    MyControl mc = new MyControl();

    mc.CreateControl();

    mc.Site = site;
 
    MyControlDesigner mcd = new MyControlDesigner();

    mcd.Initialize(mc);
 
    Button button1 = new Button();

    button1.Name = "button1";

    Button button2 = new Button();

    button2.Name = "button2";

    Button button3 = new Button();

    button3.Name = "button3";
 
    mc.Controls.Add(button1);

    mc.Controls.Add(button2);

    mc.Controls.Add(button3);
 
    List<Button> selectedButtons = new List<Button>(2);

    selectedButtons.Add(button2);

    selectedButtons.Add(button3);
 
    selectionService.SetSelectedComponents

        (selectedButtons);
 
    foreach (DesignerVerb verb in mcd.Verbs)

    {

        if ((verb.Text == "Reverse order")

            && (verb.Enabled))

        {

            verb.Invoke();

            break;

        }

    }
 
    Assert.AreEqual<Control>(button1, mc.Controls[0]);

    Assert.AreEqual<Control>(button3, mc.Controls[1]);

    Assert.AreEqual<Control>(button2, mc.Controls[2]);
}

Admittedly, this isn't the shortest unit test I've ever written, but it does perform a very relevant test, and a few of these is much faster than having to manually testing the control again and again.

Let's review the test method. The designer class uses its protected GetService method to obtain a reference to an IMenuCommandService implementation. This behavior is implemented in System.ComponentModel.Design.ComponentDesigner (from which MyControlDesigner ultimately derives) and uses the associated component's Site property to obtain the service. This means we have to site MyControl with an ISite implementation.

For this purpose, you can create a stub implementation of ISite, and that's what I've done here with StubSite:

 internal class StubSite : ISite
{
    private Dictionary<Type, object> services_;
 
    internal StubSite()
    {
        this.services_ = new Dictionary<Type, object>();
    }
 
    // ISite Members go here
 
    #region IServiceProvider Members
 
    public object GetService(Type serviceType)
    {
        if (this.services_.ContainsKey(serviceType))
        {
            return this.services_[serviceType];
        }
        return null;
    }
 
    #endregion
 
    internal void SetService(Type serviceType, object service)
    {
        this.services_[serviceType] = service;
    }
}

As you can see, I've omitted some members, but all of them just throws a NotImplementedException, since I don't need them for the test.

This stub site class can now be injected with services, which is what happens next: Stubs of IMenuCommandService and ISelectionService are injected into the stub site. These stubs are also implemented as internal classes in the test project, and are quite similar to StubSite in that they mostly ignore method calls or throw a NotImplementedException.

With all the services in place, we can now create the control and site it. The instantiated control is then used to initiatialize the designer, which retrieves the injected StubMenuCommandService and creates the designer verb.

The next steps just add some Button controls to MyControl and selects two of them (using the stub selection service).

At this point, we can loop through all the designer verbs of the designer and invoke the Reverse order verb if found. This should cause the sequence of child Buttons to altered, because the two selected Buttons should have been reversed so that button3 comes before button2.

Obviously, this test will currently throw a NotImplementedException when the Invoke method is called on the Verb, since that's the current implementation of the OnReverseOrderInvoked method in MyControlDesigner.

With a failing test, you are now ready to begin the test-driven development process of implementing the OnReverseOrderInvoked method to make the test succeed.

Some parting words: In a real-world scenario you'll need more tests to cover different aspects of the order reversing logic: The designer verb should only be enabled if two or more child controls are selected, maybe the verb should not be visible when other controls external to MyControl are selected, etc.