Unit Testing User Interfaces (Updated!)


Usually one of the major difficulties a developer faces when writing unit tests is how to write test code for User Interfaces. This is particularly important when we're doing Test-Driven Development.

The Visual Studio .NET IDE makes it difficult to unit test Windows Forms applications, because if we want to keep the test code decoupled from the UI code (which, in this case, is our domain code), we should keep the test code in a separate assembly and reference the assembly that contains all the forms. The IDE doesn't allow us to reference an assembly that's compiled to an .exe. Although this is only a limitation of the IDE, there is a very effective way to structure the solution so that we can develop UI code test-driven and keep the test code in an isolated assembly.

In fact, we only try to reference the Windows Application assembly, because it's where the IDE's code generator places the forms in the first place. However, we can just as easily setup a Class Library assembly and move all the forms to this assembly. All we need to do is to set up a reference from the Windows Application assembly to the Class Library assembly.

I usually setup the project structure of the solution in Visual Studio like this:

  1. Class library for the user interface
  2. Windows Application with the application launcher (Main() method)
  3. Class library for the unit tests

Some things in the previous list are worth noting. First, both project 2) and project 3) should set a reference to project 1), which is where all forms should be developed.

Second, project 2) will be the default startup project and will only have the Main() method that runs the startup form from project 1).

To develop the unit tests I use NUnitForms, which is an extension to NUnit that allows me to create an instance of a form and manipulate the controls in it.

Here's a (really) simplistic example of what a unit test might look like:

[Test]
public void TestOnButtonPressed()
{
      MyUserInterface controller = new MyUserInterface();
      controller.Show();

      TextBoxTester textBox = new TextBoxTester("txtName");
      textBox["Text"] = "John Doe";

      ButtonTester button = new ButtonTester("bttnHello");
      button.Click();

      LabelTester label = new LabelTester("lblName");
      Assertion.AssertEquals("Hello, John Doe!", label["Text"]);
}

The test code creates an instance of the form (possible because we can reference the Class Library that has the form) as well as some objects (from NUnitForms) that I can use to control each of the form's controls (the button, the label and the textbox). It assigns the value "John Doe" to the "Text" property of the textbox control, fires the button's click event and checks if the label's "Text" property is the expected salutation string.

If I'm writing this code test-first, the previous code gets me to a red bar in NUnit, so I have to code the handler for the onClick event for the button.

private void bttnHello_Click(object sender, System.EventArgs e)
{
      lblName.Text = "Hello, " + txtName.Text + "!";
}

A quick compile and I run the test suite in NUnit and I get a green bar this time.

Of course this is a completely simplistic example, but since I can create an instance of the form and interact with its controls from my unit tests, I can implement whichever behavior is required for the user interface.

Update!
[31-03-2005]

You can download a working sample here (94Kb, zip).
Just be sure to update all the references and reference paths in the solution to the locations where you have NUnitForms installed.

Comments (7)

  1. sog1 says:

    Can you list complete code for the tester class including imports, references etc.?

    The NunitForms documentation is very lacking.

    Thanks

  2. José Almeida says:

    Sure.

    First reference the nunit.framework and NUnitForms assemblies (nunit.framework.dll and NUnitForms.dll).

    Then you just need to add the following uses:

    using NUnit.Framework;

    using NUnit.Extensions.Forms;

    and the namespace of your class library that has all the forms.

    After that it’s just as I described above. NUnitForms doesn’t have wrapper objects for all the controls (yet), but the ones that are available are the most common and should be enough for most situations.

    Anyway, here’s a complete listing for a sample test class that uses NUnitForms:

    using System;

    using NUnit.Framework;

    using NUnit.Extensions.Forms;

    using DemoWinFormsApp;

    namespace NUnitFormsTester

    {

    [TestFixture]

    public class NUnitFormsTest

    {

    MyForm form;

    public NUnitFormsTest()

    {

    }

    [SetUp]

    public void SetUp()

    {

    form = new MyForm();

    form.Show();

    }

    [Test]

    public void Test()

    {

    TextBoxTester MyTextBox = new TextBoxTester("MyTextBox");

    ButtonTester MyButton = new ButtonTester("MyButton");

    LabelTester MyLabel = new LabelTester("MyLabel");

    MyTextBox.Enter("NUnitForms Test");

    MyButton.Click();

    Assertion.AssertEquals(MyLabel.Text, "NUnitForms Test");

    }

    }

    }

    hope it helps!

    Cheers!

  3. Hans M. Rupp says:

    Hallo I am trying out NUnitForms and I cannot get it working. I always get a NoSuchControlException:

    NUnitFormsTest.GUITest.Test1 : NUnit.Extensions.Forms.NoSuchControlException : TextBox

    I cannot figure out what’s wrong 🙁 Looking at the documentation I thought that the controls are accessed by their name.

    The program to be tested:

    —————————————————————

    using System;

    using System.Drawing;

    using System.Collections;

    using System.ComponentModel;

    using System.Windows.Forms;

    using System.Data;

    namespace NUnitFormsTest

    {

    public class Form1 : System.Windows.Forms.Form

    {

    private System.Windows.Forms.TextBox TextBox;

    private System.ComponentModel.Container components = null;

    public Form1()

    {

    InitializeComponent();

    }

    protected override void Dispose( bool disposing )

    {

    if( disposing )

    {

    if (components != null)

    {

    components.Dispose();

    }

    }

    base.Dispose( disposing );

    }

    #region Windows Form Designer generated code

    private void InitializeComponent()

    {

    this.TextBox = new System.Windows.Forms.TextBox();

    this.SuspendLayout();

    //

    // TextBox

    //

    this.TextBox.Location = new System.Drawing.Point(24, 40);

    this.TextBox.Name = "TextBox";

    this.TextBox.Size = new System.Drawing.Size(240, 20);

    this.TextBox.TabIndex = 0;

    this.TextBox.Text = "";

    //

    // Form1

    //

    this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);

    this.ClientSize = new System.Drawing.Size(292, 273);

    this.Controls.AddRange(new System.Windows.Forms.Control[] {

    this.TextBox});

    this.Name = "Form1";

    this.Text = "Form1";

    this.ResumeLayout(false);

    }

    #endregion

    [STAThread]

    static void Main()

    {

    Application.Run(new Form1());

    }

    }

    }

    ———————————————————————————-

    The test:

    using System;

    using NUnit.Framework;

    using NUnit.Extensions.Forms;

    namespace NUnitFormsTest

    {

    [TestFixture]

    public class GUITest : Assertion {

    private Form1 form;

    public GUITest() {

    }

    [SetUp]

    public void SetUp() {

    form = new Form1();

    form.Show();

    }

    [Test]

    public void Test1() {

    TextBoxTester textBox = new TextBoxTester("TextBox");

    textBox.Enter("Test");

    AssertEquals("Test", textBox.Text);

    }

    }

    }

    Many thanks,

    Hans

  4. Han,

    I have a similar problems with NUNIT. With no solution, yet. In my situation, I have a UI button click that eventually makes an asynchronous SOAP request/response. Hence the SOAP request/response are done an a different thread of execution. In order for the response to repopulate the control requires the response to be copied back to the user’s thread. I think this is done by Window’s messaging. In order for a form to listen (hook or subclass) into the windows’ messaging it must "start" the application by executing "System.Windows.Forms.Application.Run()".

    The "Application.Run" when executed is blocked. (Because it is now listening to window’s messages). I do not have any threads left to run the Nunit test. I could start a new thread within the nunit test to run the button click, but the information will not be copied back to the proper thread or I have reentrancy problems.

    I think there needs to be a change in the NUNIT framework. NUNIT should support another mode. This mode after loading the test assembly into an application domain, should start a thread and execute the "System.Windows.Forms.Application.Run()". When a test is executed, NUnit should add the delegate to message queue (see the BeginInvoke about queuing on the thread that the control’s underlying handle was created). I think this should execute a button click the same way a mouse click would.

    Robert Livermore B.Sc, MCP, MCSD, MCSE+I, MCDBA

    Senior Developer

    Cetaris

    http://www.cetaris.com

  5. wildhope says:

    Ping Back来自:blog.csdn.net

Skip to main content