473,324 Members | 2,511 Online
Bytes | Software Development & Data Engineering Community
Post Job

Home Posts Topics Members FAQ

Join Bytes to post your question to a community of 473,324 software developers and data experts.

Test Driven Development with Windows Forms

I've just released an article on using Test Driven Development with C# and
Windows Forms. GUI's are often difficult to test, so I thought it might be
of interest.

The article along with the example source code can be downloaded here:

http://www.blogitek.com/timhaughton/...0And%20TDD.zip
The article text is below. Not sure what it will do to the formatting when
it hits Usenet world. Downloading the zip might be easiest.

--
Regards,

Tim Haughton

Agitek
http://agitek.co.uk
http://blogitek.com/timhaughton

**********

After a conversation on the TestFirstUserInterfaces list on Yahoo regarding
TDD'ing forms that handle events and launch dialogues, I've gathered my
thoughts and put fingers to keyboard. By happy coincidence, the topic
coincided with some work I was doing, so I was able to spend some time
looking at the problem.

At Ron Jeffries' request, I thought I'd write a little 'thing' showing how
this might work in practice. For this little example, we're going to write
an application whose main form displays some text in a font the user
specifies. Just to make sure we're close enough to the original post, we'll
have a button that when clicked, launches a font selection dialogue.
Naturally we'll proceed test first.

House keeping: I'm writing this using VS2005, C#, NUnit and a modified
version of NMock, the modifications to which I'll include in the source code
to this example.

I'm not going to test drive the initial layout of the form as it's just not
interesting from the perspective of this example. So we'll start from here:

using System;

using System.Drawing;

using System.Windows.Forms;

namespace TestApp

{

static class Program

{

[STAThread]

static void Main()

{

Application.EnableVisualStyles();

Application.SetCompatibleTextRenderingDefault( false );

Application.Run( new MainForm() );

}

}

class MainForm :Form

{

private Button button;

private Label label;

public MainForm()

{

button = new Button();

label = new Label();

this.Width = 400;

this.Height = 250;

button.Location = new Point( 5, 10 );

button.Text = "Select Font";

button.Width = 75;

label.Text = "FooBar";

label.Location = new Point( 85, 10 );

label.Width = 300;

label.Height = 200;

Controls.Add( button );

Controls.Add( label );

}

}

}

OK, so the first thing we'd like to see is a font selection dialogue be
shown when the user when the button is pressed. Straight away we're into
murky water. Showing a dialogue during a test is not generally considered a
helpful thing. We need some way to prompt the user for some information, and
we also need a way to test.

What areas do we need to test?

1.. When the button is clicked, some action is taken by the form to
solicit the user input.
2.. A font selection dialogue is displayed.
3.. If the user accepts the change to the font, the change is applied to
the label.
4.. If the user rejects the change to the font, no change is applied to
the label.
This is where the User Interrogator steps in to lend a hand. We will
separate the notion of requesting information from the user, and the UI
mechanisms used to achieve this goal.

I'm going to create a new interface to help with this task, so, to the
tests:

[TestFixture]

public class MainFormSpec

{

[Test]

public void FormShouldCallGetFontFromUserWhenButtonIsClicked()

{

DynamicMock mock = new DynamicMock( typeof(IUserInterrogator) );

MainForm form = new MainForm(
(IUserInterrogator)(mock.MockInstance) );

mock.Expect( "GetFontFromUser" );

ControlTester ct = new ControlTester( form );

ct["Button"].Events["Click"].Fire();

mock.Verify();

}

}

I'm cheating a little here by using some useful lightweight Control testing
classes, which I've included in the example source code. Naturally this
doesn't compile, so we need to do a smidgeon of work to get a build:

public interface IUserInterrogator

{

}

A change to the MainForm's constructor:

public MainForm( IUserInterrogator interrogator )

{

button = new Button();

label = new Label();

...

And a tweak of the entry point method:

Application.Run( new MainForm( null ) );

We now have a build, and a red light. Time to go for green. All we're
interested in at this stage is showing that when the button is clicked, the
form calls the GetFontFromUser method on the interrogator object. So, a
little tweak to the form's constructor, hook up an event handler for the
button's Click event and have it call the GetFontFromUserMethod.

public MainForm( IUserInterrogator interrogator )

{

this.interrogator = interrogator;

button = new Button();

label = new Label();

this.Width = 400;

this.Height = 250;

button.Name = "Button";

button.Location = new Point( 5, 10 );

button.Text = "Select Font";

button.Width = 75;

button.Click += new EventHandler( button_Click );

...

and,

void button_Click(object sender, EventArgs e)

{

interrogator.GetFontFromUser();

}

Groovy! We have our first green light. This tells us that we're heading in
the right direction, but it's not terribly useful as the GetFontFromUser
doesn't actually return a font - let's put that right. We'll add a new test
to ensure that the form uses the returned Font correctly.

[Test]

public void FormShouldGetFontFromUserAndApplyToLabelWhenButton IsClicked()

{

DynamicMock mock = new DynamicMock( typeof( IUserInterrogator ) );

MainForm form = new MainForm( (IUserInterrogator)(mock.MockInstance) );

Font wingDings = new Font( "WingDings", 10F );

ControlTester ct = new ControlTester( form );

Label label = ct["Label"].control as Label;

Assert.AreNotEqual( wingDings, label.Font );

mock.ExpectAndReturn( "GetFontFromUser", wingDings, null );

ct["Button"].Events["Click"].Fire();

Assert.AreEqual( wingDings, label.Font );

}

So, we need to name our label "Label" to satisfy the ControlTester,

label.Name = "Label";

change the interface to return a font,

public interface IUserInterrogator

{

Font GetFontFromUser();

}

and wire up the event handler properly:

void button_Click(object sender, EventArgs e)

{

label.Font = interrogator.GetFontFromUser();

}

Green light. OK, but we've got some duplication here between the tests. Our
new test method is implicitly doing the job of the first test, so let's
remove the first test.

The next step is to provide a real implementation of IUserInterrogator for
the form to use. We've already tested the interaction between the
interrogator and the form by using a mock, so when we've TDD'd our concrete
interrogator, it should just plug straight in. Earlier on, we swam out of
murky water by delegating the displaying of the font selection dialogue to
the interrogator. It's finally caught up with us, so what are we going to
do?

Let's write a test.

[TestFixture]

public class UserInterrogatorSpec

{

[Test]

public void ShouldReturnCorrectFontWhenUserSelectsWingdingsAnd Accepts()

{

Form form = new Form();

UserInterrogator interrogator = new UserInterrogator( form );

Font f = interrogator.GetFontFromUser();

Assert.AreEqual( new Font( "Wingdings", 10F ), f );

}

}

That basically encapsulates what we want to do, but at the minute we don't
have a build, so let's write a stub.

public class UserInterrogator : IUserInterrogator

{

public UserInterrogator(Form owner)

{

}

public Font GetFontFromUser()

{

return null;

}

}

Now we've got a build, and a red light. In going for green, we'll write a
naïve implementation just to crystallise our understanding of the problem.

public class UserInterrogator : IUserInterrogator

{

private Form owner;

public UserInterrogator(Form owner)

{

this.owner = owner;

}

public Font GetFontFromUser()

{

FontDialog fd = new FontDialog();

fd.ShowDialog( owner );

return fd.Font;

}

}

Running the tests now results in a FontDialogue popping up. This is just no
good at all. We need to separate construction and operation. In order to do
this, we'll need to add a new interface. Looking at the implementation has
also brought to my attention the need for the current font to be sent to the
GetFontFromUser method, so lets make the necessary changes now.

[TestFixture]

public class MainFormSpec

{

[Test]

public void
FormShouldGetFontFromUserAndApplyToLabelWhenButton IsClicked()

{

DynamicMock mock = new DynamicMock( typeof( IUserInterrogator ) );

MainForm form = new MainForm(
(IUserInterrogator)(mock.MockInstance) );

Font wingDings = new Font( "WingDings", 10F );

ControlTester ct = new ControlTester( form );

Label label = ct["Label"].control as Label;

Assert.AreNotEqual( wingDings, label.Font );

mock.ExpectAndReturn( "GetFontFromUser", wingDings, new object[] {
label.Font } );

ct["Button"].Events["Click"].Fire();

Assert.AreEqual( wingDings, label.Font );

}

}

[TestFixture]

public class UserInterrogatorSpec

{

[Test]

public void ShouldReturnCorrectFontWhenUserSelectsWingdingsAnd Accepts()

{

Form form = new Form();

UserInterrogator interrogator = new UserInterrogator( form );

Font f = interrogator.GetFontFromUser( new Font( "Arial", 10F ));

Assert.AreEqual( new Font( "Wingdings", 10F ), f );

}

}

and,

public Font GetFontFromUser( Font initialFont )

{

FontDialog fd = new FontDialog();

fd.Font = initialFont;

fd.ShowDialog( owner );

return fd.Font;

}

with,

void button_Click(object sender, EventArgs e)

{

label.Font = interrogator.GetFontFromUser( label.Font );

}

I'm forgoing the red green refactor mantra for this in the name of
expediency. Now, we need to delegate the showing of the dialogue in a way
that allows us to test, so I create this interface:

public interface IDialogLauncher

{

Font ShowAndReturn(FontDialog fontDialog);

}

And inject allow for injecting this dependency in to the UserInterrogator:

public class UserInterrogator : IUserInterrogator

{

private Form owner;

private IDialogLauncher launcher;

public UserInterrogator(Form owner, IDialogLauncher launcher)

: this( owner )

{

this.launcher = launcher;

}

...

This now allows us to make some headway. Firstly, let's change the test,

[TestFixture]

public class UserInterrogatorSpec

{

[Test]

public void ShouldReturnCorrectFontWhenUserSelectsWingdingsAnd Accepts()

{

DynamicMock mock = new DynamicMock( typeof( IDialogLauncher ) );

FontDialog fd = new FontDialog();

fd.Font = new Font( "Arial", 10F );

And constraint = new And( new IsTypeOf( typeof( FontDialog ) ), new
NotNull() );

mock.ExpectAndReturn( "ShowAndReturn", new Font( "Wingdings", 10F ),
constraint );

Form form = new Form();

UserInterrogator interrogator = new UserInterrogator( form,
mock.MockInstance as IDialogLauncher );

Font f = interrogator.GetFontFromUser( fd.Font );

mock.Verify();

Assert.AreEqual( new Font( "Wingdings", 10F ), f );

}

}

giving us a red light, and now we can change the GetFontFromUser method to
delegate showing the dialogue:

public Font GetFontFromUser(Font initialFont)

{

FontDialog fd = new FontDialog();

fd.Font = initialFont;

return launcher.ShowAndReturn( fd );

}

giving us green.

In the event of the user clicking on 'Cancel' on the font selection
dialogue, we'd like our ShowAndReturn method to return null and we want our
GetFontFromUser method to return the initial font. So a new test looks like:

[Test]

public void ShouldReturnInitialFontWhenUserCancels()

{

DynamicMock mock = new DynamicMock( typeof( IDialogLauncher ) );

FontDialog fd = new FontDialog();

fd.Font = new Font( "Arial", 10F );

And constraint = new And( new IsTypeOf( typeof( FontDialog ) ), new
NotNull() );

mock.ExpectAndReturn( "ShowAndReturn", null, constraint );

Form form = new Form();

UserInterrogator interrogator = new UserInterrogator( form,
mock.MockInstance as IDialogLauncher );

Font f = interrogator.GetFontFromUser( fd.Font );

mock.Verify();

Assert.AreEqual( fd.Font, f );

}

The trivial change to GetFontFromUser yields:

public Font GetFontFromUser(Font initialFont)

{

FontDialog fd = new FontDialog();

fd.Font = initialFont;

Font newFont = launcher.ShowAndReturn( fd );

if ( newFont != null )

return newFont;

return initialFont;

}

A little refactoring of the tests here leaves us with

[TestFixture]

public class UserInterrogatorSpec

{

[Test]

public void ShouldReturnCorrectFontWhenUserSelectsWingdingsAnd Accepts()

{

SetUpAndCall(new Font( "Wingdings", 10F ), new Font( "Wingdings",
10F ));

}

[Test]

public void ShouldReturnInitialFontWhenUserCancels()

{

SetUpAndCall( null, new Font( "Arial", 10F ) );

}

private static void SetUpAndCall(Font mockReturn, Font returnFont)

{

DynamicMock mock = new DynamicMock( typeof( IDialogLauncher ) );

FontDialog fd = new FontDialog();

fd.Font = new Font( "Arial", 10F );

And constraint = new And( new IsTypeOf( typeof( FontDialog ) ), new
NotNull() );

mock.ExpectAndReturn( "ShowAndReturn", mockReturn, constraint );

Form form = new Form();

UserInterrogator interrogator = new UserInterrogator( form,
mock.MockInstance as IDialogLauncher );

Font f = interrogator.GetFontFromUser( fd.Font );

mock.Verify();

Assert.AreEqual( returnFont, f );

}

}

We're still on green, so we can move on to developing our concrete class
DialogLauncher. We need to specify a couple of pieces of behaviour; firstly,
that the ShowAndReturn method calls ShowDialogue on the font selection
dialogue passing in the parent form as an argument. Secondly, that if the
user clicks OK, the method returns the currently selected font, otherwise it
returns null.

In order to do this, we're going to need one of our mods to NMock, namely
MarshalByRefMock. This is similar to the DynamicMock class with 2
differences, it can mock only those object which derive from
MarshalByRefObject, but can mock all methods, including non-virtual ones.
This makes it ideal for mocking Controls and Forms.

First, we'll specify the behaviour in the event the user clicks cancel.

[TestFixture]

public class DialogLauncherSpec

{

[Test]

public void ShouldReturnNullWhenUserClicksCancel()

{

Form parent = new Form();

DialogLauncher dl = new DialogLauncher( parent );

MarshalByRefMock mock = new MarshalByRefMock( typeof(
FontDialog ) );

mock.ExpectAndReturn( "ShowDialog", DialogResult.Cancel, parent );

Assert.IsNull( dl.ShowAndReturn( mock.MockInstance as
FontDialog ) );

mock.Verify();

}

}

The implementation seems obvious, so let's go straight for green.

public class DialogLauncher : IDialogLauncher

{

Form parent;

public DialogLauncher(Form parent)

{

this.parent = parent;

}

public Font ShowAndReturn(FontDialog fontDialog)

{

fontDialog.ShowDialog( parent );

return null;

}

}

We can now spec the case of the user clicking OK.

[Test]

public void ShouldReturnSelectedFontWhenUserCLicksOK()

{

Form parent = new Form();

DialogLauncher dl = new DialogLauncher( parent );

MarshalByRefMock mock = new MarshalByRefMock( typeof( FontDialog ) );

Font expectedFont = new Font( "Wingdings", 12f );

mock.ExpectAndReturn( "ShowDialog", DialogResult.OK, parent );

mock.ExpectAndReturn( "get_Font", expectedFont, null );

Assert.AreEqual( expectedFont, dl.ShowAndReturn( mock.MockInstance as
FontDialog ) );

mock.Verify();

}

And the corresponding change to the ShowAndReturn method,

public Font ShowAndReturn(FontDialog fontDialog)

{

if ( fontDialog.ShowDialog( parent ) == DialogResult.OK )

return fontDialog.Font;

return null;

}

With that green light, we're just about done. All that remains is to ensure
that when the MainForm and UserInterrogator are instantiated without an
injected dependency, they avail themselves of concrete implementations of
the UserInterrogator and DialogLauncher respectively.

[TestFixture]

public class UserInterrogatorSpec

{

[Test]

public void ShouldCreateADialogLauncherWhenOneIsNotSpecified()

{

UserInterrogator ui = new UserInterrogator( new Form() );

Assert.IsNotNull( ui.interrogator );

}

...

leads to

public class UserInterrogator : IUserInterrogator

{

private Form owner;

internal IDialogLauncher launcher;

public UserInterrogator(Form owner, IDialogLauncher launcher)

{

this.owner = owner;

this.launcher = launcher;

}

public UserInterrogator(Form owner)

: this ( owner, new DialogLauncher( owner ) )

{

}

...

and

[TestFixture]

public class MainFormSpec

{

[Test]

public void ShouldCreateAUserInterrogatorWhenOneIsNotSpecified ()

{

MainForm mf = new MainForm();

Assert.IsNotNull( mf.interrogator );

}

...

leads to

class MainForm : Form

{

private Button button;

private Label label;

internal IUserInterrogator interrogator;

public MainForm()

: this(null)

{

this.interrogator = new UserInterrogator( this );

}

...

There we go. Job done! At this point, I recommend that developers do the
victory dance. If you don't have one, get one, it really makes you feel
good.

Summary
The solution I arrived at here after this coding experiment is different to
the solution I arrived at originally on the TFUI list via a thought
experiment. It once again serves to remind me to question the value of
thought alone. Thinking is good, testing and coding is often better.

As to the solution, there's a clear pattern here that should be reusable.
Whenever a UI has to respond to an event, and query the user for some
interaction, the way to test drive this is by separating the 3 concerns:

1.. Responding to the event and conceptually asking the user for input.
2.. Creating the UI elements to present to the user.
3.. Displaying the UI elements.
Hopefully, this strategy should help in the noble pursuit of TFUI. Usually,
descriptions of test driven user interfaces suggest a thin layer of
untestable GUI code at the top. This approach has 100% test coverage of the
GUI. Key to this is the mocking of user controls and forms.


Nov 22 '05 #1
0 846

This thread has been closed and replies have been disabled. Please start a new discussion.

Similar topics

11
by: DrUg13 | last post by:
In java, this seems so easy. You need a new object Object test = new Object() gives me exactly what I want. could someone please help me understand the different ways to do the same thing in...
0
by: Tim Haughton | last post by:
I've just released an article on using Test Driven Development with C# and Windows Forms. GUI's are often difficult to test, so I thought it might be of interest. The article along with the...
3
by: Sasha | last post by:
Hi everybody, I am starting a new ASP.NET/SQL Server project and this time I want to do it through Test-Driven Development (TDD). The problem that I ran into is this: How do you test data access...
3
by: Yannick Tremblay | last post by:
Hi C peoples, I have been working with OO languages in recent years mostly C++ and Java and I have seen the advantages of good unit testing frameworks and/or test driven development. I've...
10
by: Michael B. Trausch | last post by:
Alright, I seem to be at a loss for what I am looking for, and I am not even really all that sure if it is possible or not. I found the 'pdb' debugger, but I was wondering if there was something...
2
by: Steven D'Aprano | last post by:
I'm working on some functions that, essentially, return randomly generated strings. Here's a basic example: def rstr(): """Return a random string based on a pseudo normally-distributed random...
9
by: Deckarep | last post by:
Hello Group, I actually have two seperate questions regarding Unit Testing with NUnit in C#. Please keep in mind that I'm new to the concept of Unit Testing and just barely coming around to...
176
by: nw | last post by:
Hi, I previously asked for suggestions on teaching testing in C++. Based on some of the replies I received I decided that best way to proceed would be to teach the students how they might write...
6
by: Joel Hedlund | last post by:
Hi! I write, use and reuse a lot of small python programs for variuos purposes in my work. These use a growing number of utility modules that I'm continuously developing and adding to as new...
0
MMcCarthy
by: MMcCarthy | last post by:
VBA is described as an Event driven programming language. What is meant by this? Access, like most Windows programs, is an event driven application. This means that nothing happens unless it is...
0
by: ryjfgjl | last post by:
ExcelToDatabase: batch import excel into database automatically...
0
by: Vimpel783 | last post by:
Hello! Guys, I found this code on the Internet, but I need to modify it a little. It works well, the problem is this: Data is sent from only one cell, in this case B5, but it is necessary that data...
1
by: PapaRatzi | last post by:
Hello, I am teaching myself MS Access forms design and Visual Basic. I've created a table to capture a list of Top 30 singles and forms to capture new entries. The final step is a form (unbound)...
1
by: CloudSolutions | last post by:
Introduction: For many beginners and individual users, requiring a credit card and email registration may pose a barrier when starting to use cloud servers. However, some cloud server providers now...
1
by: Defcon1945 | last post by:
I'm trying to learn Python using Pycharm but import shutil doesn't work
1
by: Shællîpôpï 09 | last post by:
If u are using a keypad phone, how do u turn on JavaScript, to access features like WhatsApp, Facebook, Instagram....
0
by: af34tf | last post by:
Hi Guys, I have a domain whose name is BytesLimited.com, and I want to sell it. Does anyone know about platforms that allow me to list my domain in auction for free. Thank you
0
by: Faith0G | last post by:
I am starting a new it consulting business and it's been a while since I setup a new website. Is wordpress still the best web based software for hosting a 5 page website? The webpages will be...
0
isladogs
by: isladogs | last post by:
The next Access Europe User Group meeting will be on Wednesday 3 Apr 2024 starting at 18:00 UK time (6PM UTC+1) and finishing by 19:30 (7.30PM). In this session, we are pleased to welcome former...

By using Bytes.com and it's services, you agree to our Privacy Policy and Terms of Use.

To disable or enable advertisements and analytics tracking please visit the manage ads & tracking page.