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.
- 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.
- 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.
- 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.