Thursday, June 16, 2005

A simple example of the "Humble Dialog Box"

At the Agile Austin lunch today, we talked a bit about different ways to apply the Model View Presenter pattern with WinForms clients. I promised to put up an example of using Michael Feather's "Humble Dialog Box" to create more testable user interface code.

The current thinking for writing automated unit tests against rich clients is a modification or variation of the classic Model View Controller (MVC) architecture. Specifically, take the view part of MVC and slice it as thin as possible so that it is only a skin around the actual UI components and make it completely passive. The controller, now called the "presenter," is responsible for all interaction with the rest of the system. There is a pattern of symbiosis between the view and the presenter. The presenter directs the view what and when to display and the view captures and relays user events to the presenter. Check the links above for a more comprehensive explanation from the professionals.

Here's a common scenario. You have some kind of form in your application for editing a piece of data. If the user trys to close the form and there are pending changes, put up a dialog box giving the user a chance to cancel the close operation. In this case the dialog box is the major impediment to automated testing, so we're going to hide the message box creation behind an interface that can be mocked (or stubbed if that's your predilection). Do the same thing for the view/presenter separation. Use the Dependency Inversion Principle to abstract the view away from the presenter and mock the view in the unit tests.




using System;
using System.Windows.Forms;
using NMock;
using NUnit.Framework;

namespace SampleCode.HumbleDialogBox
{
public interface IMessageBoxCreator
{
bool AskYesNoQuestion(string title, string message);
}

public class MessageBoxCreator : IMessageBoxCreator
{
public bool AskYesNoQuestion(string title, string message)
{
DialogResult result = MessageBox.Show(message, title, MessageBoxButtons.OKCancel);
return result == DialogResult.OK;
}
}

public interface IView
{
void Close();
bool IsDirty();
}

public class Presenter
{
public const string DIRTY_CLOSE_WARNING = "Changes are pending. "
+ "Ok to continue, cancel to return to the edit screen.";
public const string CLOSE_WARNING_TITLE = "Changes Pending";

private readonly IView _view;
private readonly IMessageBoxCreator _msgBox;

//
public Presenter(IView view, IMessageBoxCreator msgBox)
{
_view = view;
_msgBox = msgBox;
}

public void Close()
{
bool canClose = true;

if (_view.IsDirty())
{
canClose = _msgBox.AskYesNoQuestion(CLOSE_WARNING_TITLE, DIRTY_CLOSE_WARNING);
}

if (canClose)
{
_view.Close();
}
}
}

[TestFixture]
public class PresenterTester
{
private DynamicMock _viewMock;
private DynamicMock _msgBoxMock;
private Presenter _presenter;

[SetUp]
public void SetUp()
{
_msgBoxMock = new DynamicMock(typeof(IMessageBoxCreator));
_viewMock = new DynamicMock(typeof(IView));
_presenter = new Presenter((IView) _viewMock.MockInstance, (IMessageBoxCreator) _msgBoxMock.MockInstance);
}


[Test]
public void CloseViewWhenViewIsNotDirty()
{
// Define the expected interaction
_msgBoxMock.ExpectNoCall("AskYesNoQuestion", typeof(string), typeof(string));

_viewMock.ExpectAndReturn("IsDirty", false);
_viewMock.Expect("Close");

// Perform the unit of work
_presenter.Close();

// Verify the interaction
_msgBoxMock.Verify();
_viewMock.Verify();
}


[Test]
public void CloseViewWhenViewIsDirtyAndUserRespondsOk()
{
// Define the expected interaction
_msgBoxMock.ExpectAndReturn(
"AskYesNoQuestion",
true,
Presenter.CLOSE_WARNING_TITLE,
Presenter.DIRTY_CLOSE_WARNING);

_viewMock.ExpectAndReturn("IsDirty", true);
_viewMock.Expect("Close");

// Perform the unit of work
_presenter.Close();

// Verify the interaction
_msgBoxMock.Verify();
_viewMock.Verify();
}


[Test]
public void DoNotCloseViewWhenViewIsDirtyAndUserRespondsCancel()
{
// Define the expected interaction
_msgBoxMock.ExpectAndReturn(
"AskYesNoQuestion",
false,
Presenter.CLOSE_WARNING_TITLE,
Presenter.DIRTY_CLOSE_WARNING);

_viewMock.ExpectAndReturn("IsDirty", true);
_viewMock.ExpectNoCall("Close");

// Perform the unit of work
_presenter.Close();

// Verify the interaction
_msgBoxMock.Verify();
_viewMock.Verify();
}
}
}




Here's a rundown of the pieces from the example code.

  1. IMessageBoxCreator/MessageBoxCreator - An interface and wrapper class around the WinForms MessageBox class. The methods in the .NET framework for dialogs are all static, and static methods cannot be mocked.
  2. IView interface - An interface that establishes the public contract between the actual form and the presenter. I didn't show it, but assume the actual View has a reference to the Presenter.
  3. Presenter - the Presenter class drives the IView and IMessageBoxCreator interfaces. The Presenter class is completely unaware of any of the actual user interface plumbing, i.e. not one single reference to the System.Windows.Forms namespace.

In this example I used constructor injection to attach the IMessageBoxCreator. The next example is mostly the same, but I use StructureMap instead to locate the IMessageBoxCreator and take advantage of StructureMap's built in support for NMock.




using System;
using System.Windows.Forms;
using NMock;
using NUnit.Framework;
using StructureMap;

namespace SampleCode.HumbleDialogBox2
{
[PluginFamily("Default")]
public interface IMessageBoxCreator
{
bool AskYesNoQuestion(string title, string message);
}

[Pluggable("Default")]
public class MessageBoxCreator : IMessageBoxCreator
{
public bool AskYesNoQuestion(string title, string message)
{
DialogResult result = MessageBox.Show(message, title, MessageBoxButtons.OKCancel);
return result == DialogResult.OK;
}
}

public interface IView
{
void Close();
bool IsDirty();
}

public class Presenter
{
public const string DIRTY_CLOSE_WARNING = "Changes are pending. "
+ "Ok to continue, cancel to return to the edit screen.";
public const string CLOSE_WARNING_TITLE = "Changes Pending";


private readonly IView _view;

public Presenter(IView view)
{
_view = view;
}

public void Close()
{
bool canClose = true;

if (_view.IsDirty())
{
// Get the IMessageBoxCreator out of StructureMap
IMessageBoxCreator msgBox =
(IMessageBoxCreator)
ObjectFactory.GetInstance(typeof(IMessageBoxCreator));
canClose = msgBox.AskYesNoQuestion
(CLOSE_WARNING_TITLE, DIRTY_CLOSE_WARNING);
}

if (canClose)
{
_view.Close();
}
}
}

[TestFixture]
public class PresenterTester
{
private DynamicMock _viewMock;
private IMock _msgBoxMock;
private Presenter _presenter;

[SetUp]
public void SetUp()
{
_msgBoxMock = ObjectFactory.Mock(typeof(IMessageBoxCreator));
_viewMock = new DynamicMock(typeof(IView));
_presenter = new Presenter((IView) _viewMock.MockInstance);
}

[TearDown]
public void TearDown()
{
ObjectFactory.ResetDefaults();
}

[Test]
public void CloseViewWhenViewIsNotDirty()
{
// Define the expected interaction
_msgBoxMock.ExpectNoCall("AskYesNoQuestion", typeof(string), typeof(string));

_viewMock.ExpectAndReturn("IsDirty", false);
_viewMock.Expect("Close");

// Perform the unit of work
_presenter.Close();

// Verify the interaction
_msgBoxMock.Verify();
_viewMock.Verify();
}


[Test]
public void CloseViewWhenViewIsDirtyAndUserRespondsOk()
{
// Define the expected interaction
_msgBoxMock.ExpectAndReturn(
"AskYesNoQuestion",
true,
Presenter.CLOSE_WARNING_TITLE,
Presenter.DIRTY_CLOSE_WARNING);

_viewMock.ExpectAndReturn("IsDirty", true);
_viewMock.Expect("Close");

// Perform the unit of work
_presenter.Close();

// Verify the interaction
_msgBoxMock.Verify();
_viewMock.Verify();
}


[Test]
public void DoNotCloseViewWhenViewIsDirtyAndUserRespondsCancel()
{
// Define the expected interaction
_msgBoxMock.ExpectAndReturn(
"AskYesNoQuestion",
false,
Presenter.CLOSE_WARNING_TITLE,
Presenter.DIRTY_CLOSE_WARNING);

_viewMock.ExpectAndReturn("IsDirty", true);
_viewMock.ExpectNoCall("Close");

// Perform the unit of work
_presenter.Close();

// Verify the interaction
_msgBoxMock.Verify();
_viewMock.Verify();
}
}
}



Final Thoughts



It is not impossible to write automated unit tests for rich clients, but it's definitely difficult and time consuming. So what can you do? You can take a calculated risk and forgo writing the automated tests for the user interface. The biggest problem with that approach is that a complicated rich user interface can generate a large number of bugs and requires a lot of energy towards manual regression testing (duh). You can test a WinForms application with Luke Maxon's most excellent NUnitForms toolkit, but user interface tests are still more work to setup and execute. A better approach is to simply make as much code as possible independent of the WinForms (or Swing, etc.) engine. I say you still have to test the actual UI forms and controls. However, if they are passive and loosely coupled from the rest of the application your NUnitForms tests can be much simpler.



I left some implementation details out of the example. I've used the MVP pattern pretty extensively on a couple of projects now with mostly good results. Since it's such a hot topic and the book on best practices is literally being written as I type this, I'll try to blog soon on some MVP suggestions and pitfalls.

11 Comments:

Anonymous Ayende Rahien said...

i'm usng using much the same architecture, but the AskYesNo() is part of my IView interface.

I didn't see the point in creating an interface just for that. What do you think of that?

9:41 AM, June 17, 2005  
Anonymous Ayende Rahien said...

One more thing, there are quite a bit of UI's issues that I'm not sure where to put.
I'm talking about things that are not neccecarily part of the logic of the application, but are needed to give the application a proffesional look.

For instance, disabling/enabling buttons according to whatever an item is selected or not.

I'm currently putting them inside the IView, without interacting witht he Presenter for that.

9:45 AM, June 17, 2005  
Blogger Jeremy D. Miller said...

Ayende,

I waffle a bit on moving the AskYesNo() into the view interface itself. We started working that way, but moved the message box out into a separate interface because we were copying and pasting a lot of code. I definitely don't like the dialog box text in the presenter the way I did it in the example. In retrospect, I think I like your way better.

I do put flow logic around enabling/disabling buttons and hiding/showing screen elements as part of the presenter. So, IView would have a "bool EnableSubmitOrReset {get; set;}" member and the Presenter would have a method for "ItemSelected(string item)". I think the issue might be a little bit preference. I'm very comfortable with mock objects, so I'd rather test the presenter with a mock view. If you're more comfortable that I am with testing WinForms the active view you're describing is perfectly fine. I generally write and unit test the presenter first before I create the actual UI, but I'd call that preference too.

I definitely think something like RhinoMock would take some of the friction out of MVP.

10:06 AM, June 17, 2005  
Blogger ghkj said...

EVEN by wow gold the standards gold in wow of the worst financial buy wow gold crisis for at least wow gold cheap a generation, the events of Sunday September 14th and the day before were extraordinary. The weekend began with hopes that a deal could be struck,maplestory mesos with or without government backing, to save Lehman Brothers, America''s fourth-largest investment bank.sell wow gold Early Monday buy maplestory mesos morning Lehman maplestory money filed for Chapter 11 bankruptcy protection. It has more than maplestory power leveling $613 billion of debt.Other vulnerable financial giants scrambled maple money to sell themselves or raise enough capital to stave off a similar fate. billig wow gold Merrill Lynch, the third-biggest investment bank, sold itself to Bank of America (BofA), an erstwhile Lehman suitor,wow power leveling in a $50 billion all-stock deal.wow power leveling American International Group (AIG) brought forward a potentially life-saving overhaul and went maple story powerleveling cap-in-hand to the Federal Reserve. But its shares also slumped on Monday.

3:40 AM, February 01, 2009  
Anonymous Anonymous said...

Good day, sun shines!
There have been times of hardship when I didn't know about opportunities of getting high yields on investments. I was a dump and downright pessimistic person.
I have never thought that there weren't any need in large starting capital.
Now, I feel good, I begin to get real money.
It's all about how to choose a correct partner who utilizes your money in a right way - that is incorporate it in real deals, and shares the income with me.

You may ask, if there are such firms? I'm obliged to answer the truth, YES, there are. Please be informed of one of them:
http://theblogmoney.com

12:53 AM, January 14, 2010  
Anonymous Anonymous said...

Good day, sun shines!
There have been times of hardship when I felt unhappy missing knowledge about opportunities of getting high yields on investments. I was a dump and downright stupid person.
I have never thought that there weren't any need in large initial investment.
Nowadays, I feel good, I started to get real money.
It's all about how to select a proper partner who uses your funds in a right way - that is incorporate it in real deals, and shares the profit with me.

You can ask, if there are such firms? I have to answer the truth, YES, there are. Please be informed of one of them:
http://theinvestblog.com [url=http://theinvestblog.com]Online Investment Blog[/url]

2:24 AM, January 31, 2010  
Anonymous Anonymous said...

Hey. I don't normally leave comments, but I just wanted to say thanks for the great information. I have a blog too, though
I don't write as good as you do, but if you want to check it out here it is. Thanks again and have a great day!

Feral Druid PvP

12:39 AM, February 04, 2010  
Anonymous Anonymous said...

[url=http://tinyurl.com/getvpn][b]Click here to get VPN service![/b][/url]

[b]Anonymous surfing[/b]
Using our service you'll be fully anonymous in the Internet. Hide your IP address, and nobody will know that strange visitor from Germany ( Great Britain, Estonia and so ), is you.

[b]Full access to network[/b]
You can use any services, access any sites and use any software with us. BitTorrent, Skype, Facebook, MySpace, Twitter, Pocker .. we have no restrictions.

[b]Traffic protection[/b]
Don't worry, from this moment all you data will be protected using 256 bit Blowfish encryption algorithm. Nobody can access your internet data.

[b]Wide variety of countries[/b]
You can choose one of over twenty high speed servers located in different parts of the world, from South America coast to islands in Indian Ocean.

Related keywords:
anonymous surfing review
proxy server vpn
anonymous secure surfing
proxy vpn
anonymous vpn free
internet explorer vpn
vpn dial up
ssl vpn
Traffic protection
anonymous surfing freeware
anonymous surfing software
vtunnel
anonymous surfing vpn
best anonymous browser
surf the web anonymous
best anonymous surfing
anonymizer anonymous surfing review
firefox anonymous surfing
Virtual Private Networks
Free Vpn Client Software
anonymous surfing software
[url=http://dasbmw.ru] anonymous surfing software[/url]
[url=http://seobraincenter.ru] anonymous proxy[/url]
[url=http://carlwebster.com/members/Alexander-Pwnz.aspx]Buy Cheap Zoloft[/url]

11:50 AM, February 12, 2010  
Anonymous Anonymous said...

You wrote a very interesting article. And I agree with you. hair loss

2:24 AM, January 10, 2012  
Anonymous Anonymous said...

The article was very interesting and informative for me. weight loss Read a useful article about tramadol tramadol

2:38 AM, February 09, 2012  
Anonymous Anonymous said...

Have you ever called for some dough, pożyczka bez bik nevertheless really don’t are unless pay day advance? The item happens proszę kliknij so that you can an incredible number of People today in america across the nation everyday. A product comes up and you simply want pożyczki bez bik do 10000 some dough, your look at isn’t settled nevertheless. Only if www there would be the right way to pozyczki kredyty przez internet get a payday loan on the web, best suited?

12:59 PM, November 30, 2012  

Post a Comment

<< Home