When last we left our hero, D'Artagnan was chasing the evil Lady de Winter across the breadth of France trying to intercept her on her dastardly mission when he found himself beset by disparate responsibilities within the tight confines of a single autonomous view with no room for sword play. D'Artagnan, being a perspicacious young man, quickly sees a way to separate the numerous concerns he's facing by opening his attack with...
The Humble Dialog Box
I'd say the very first concept to grasp in software design is separation of concerns. Divide and conquer. Eat the elephant one bite at a time. Learning how to decompose a bigger problem into a series of approachable goals. I'd rather work serially on a screen by completing one simple task before moving onto the next instead of working with all aspects of a screen in parallel. Division of responsibility for easier programming is a major consideration by itself, but there's another piece of motivation almost as important. Because user interface code can be very complex to debug and is prone to change based on user experience, I really, really want to extend granular unit testing with automated tests as far into the presentation layer as I possibly can.
Traditionally, user interface code has appeared to repel all but the most serious attempts at test automation. Automated testing against UI code was just flat out deemed too much work for the gain. That equation has changed over the last several years with the rise of architectures inspired by The Humble Dialog Box from Michael Feathers.
Here's the canonical example of the Humble Dialog Box I use to explain the concept. Say you have a user screen for some sort of data entry. You have a requirement that reads something like:
If the user attempts to close the XYZ screen without saving any outstanding changes, a message box is displayed to warn the user that they are discarding changes. If the user wishes to retain the outstanding work, do not close the screen. The message box should not be shown if the data has not been changed.
It's not that complex of a requirement really, but it's the kind of thing that makes a user interface client easy and convenient to use. We want this code to work. The code for it might look like this:
private void ArrogantView_Closing(object sender, CancelEventArgs e)
{
// Let's not worry for now about how we figure out the screen has unsaved data
if (isDirty())
{
bool canClose = MessageBox.Show("Ok to discard changes or cancel to keep working") == DialogResult.OK;
e.Cancel = !canClose;
}
}
That code really isn't that complex, but let's think about how we could automate a test for this requirement to run within our regression test suite. That little bitty call to MessageBox.Show() and the modal dialog box that results is a serious pain to deal with in an automated test (it is possible, and I've done it before, but I'd strongly recommend you keep reading before you run off and try it). Observing the UI getting closed or not is also tricky, but I think the worst part is that to test this logic you have to fire up the UI, navigate to the screen, change some data on the screen, then trigger the close screen request. That's a lot of work just to get to the point at which you're exercising the code you care about.
Now, let's rewrite this feature as a "Humble" view, but before I show the new code, let's talk about the Humble view philosophy. The first thing to do is to put the view on a diet. Any code in a WinForms UserControl or Form is almost automatically harder to test than it would be in a POCO. A Humble view should be the smallest possible wrapper around the actual presentation code. Going farther, I don't want implementation details of the view mechanics to leak into other areas of the code, so I want to hide the View behind a POCO-ish interface. All that being said, the abstracted interface for our View could look like:
public interface IHumbleView
{
bool IsDirty();
bool AskUserToDiscardChanges();
void Close();
}
The view is also "passive," meaning that it doesn't really take any actions on its own without some sort of stimulus from outside the view. I'll discuss handling user events in depth in a later chapter, but for now let's just say that the view simply relays user input events to somewhere else with little or no interpretation.
One of the goals of a Humble view is to separate responsibilities. As in most designs, we want to assign different responsibilities to different areas of the code. In this case, we want to pull behavioral logic out of the view and into non-visual classes. If we put the view itself on a diet and pull out anything that isn't directly related to presentation, that extra code that implements things like behavior and authorization rules has to go somewhere. In this case we're going to move those responsibilities into a Presenter class:
public class OverseerPresenter
{
private readonly IHumbleView _view;
public OverseerPresenter(IHumbleView view)
{
_view = view;
}
public void Close()
{
bool canClose = true;
if (_view.IsDirty())
{
canClose = _view.AskUserToDiscardChanges();
}
if (canClose)
{
_view.Close();
}
}
}
In particular, look at the Close() method. Some user event causes a call to the OverseerPresenter.Close() method. Inside this method we check the "dirty" state of the IHumbleView member and potentially ask the user to discard changes before proceeding to close the actual view. It's just about the exact same code, only now we can write an automated unit test to express this logic -- with just a little help from our good friend RhinoMocks.
[TestFixture]
public class OverseerPresenterTester
{
[Test]
public void CloseTheScreenWhenTheScreenIsNotDirty()
{
MockRepository mocks = new MockRepository();
IHumbleView view = mocks.CreateMock<IHumbleView>();
Expect.Call(view.IsDirty()).Return(false);
view.Close();
mocks.ReplayAll();
OverseerPresenter presenter = new OverseerPresenter(view);
presenter.Close();
mocks.VerifyAll();
}
[Test]
public void CloseTheScreenWhenTheScreenIsDirtyAndTheUserDecidesToDiscardTheChanges()
{
MockRepository mocks = new MockRepository();
IHumbleView view = mocks.CreateMock<IHumbleView>();
Expect.Call(view.IsDirty()).Return(true);
Expect.Call(view.AskUserToDiscardChanges()).Return(true);
view.Close();
mocks.ReplayAll();
OverseerPresenter presenter = new OverseerPresenter(view);
presenter.Close();
mocks.VerifyAll();
}
[Test]
public void CloseTheScreenWhenTheScreenIsDirtyAndTheUserDecidesNOTToDiscardTheChanges()
{
MockRepository mocks = new MockRepository();
IHumbleView view = mocks.CreateMock<IHumbleView>();
Expect.Call(view.IsDirty()).Return(true);
Expect.Call(view.AskUserToDiscardChanges()).Return(false);
// No call should be made to view.Close()
// view.Close();
mocks.ReplayAll();
OverseerPresenter presenter = new OverseerPresenter(view);
presenter.Close();
mocks.VerifyAll();
}
}
So I know what you might be thinking, what have I really gained here? Let me try to answer this:
- Orthogonality. We've moved behavioral logic out of the actual view. We can change the presentation or the behavior independently. That is a big deal.
- The screen behavior is easier to understand. I'm going to argue that this is a case of Reg Braithwaite's Signal to Noise Ratio in code (basically, expressing the intent of the code with little code that isn't directly related to the intent). When I want to understand the screen behavior, that behavior is the only signal I care about. Seeing (object sender, CancelEventArgs e) everywhere in the middle of the behavioral code is noise. The converse is true as well when I'm working on the presentation itself.
- The screen behavior is easier to test and modify. That's enough by itself to justify the Humble View style. What if this closing behavior changes tomorrow with a requirement to set a user preference to never ask users to discard changes? If I'm working in a Humble View style, I can probably make that screen behavior change completely, including unit tests, in the Presenter class by itself without ever having to fire up the user interface until the very last sanity check. Verifying little behavior changes with NUnit is a far, far tighter feedback cycle than doing the save verification by firing up the user interface and doing the manual input steps necessary to exercise the functionality. Tight feedback cycles == productivity.
- By extending the reach of granular and automated unit tests farther into the potentially complex user interface code, we can drastically slow down the rate of screen defects getting through to the testers. If nothing else, we can knock down all the common uses of the user interface quickly through tests to give the testers more time to break the application with edge cases and exploratory testing.
An astute reader will note that we didn't write any unit tests for the View. I'll show an example in later chapters of testing the View itself, but the philosophy in general is to make the hard to test view code so simple as to be reliably verified by inspection alone. I.e., you should make the View code so simple that it's almost hard to screw it up.
A Taxonomy of Humble Views
Arguably the first Humble View is the original Model View Controller architecture handed down, as basically everything good in software developer seems to be, from the Smalltalk community. As I've mentioned before, you can read about the evolution of the Model View Controller (MVC) pattern and the formulation of the Model View Presenter (MVP) patterns that we're mostly talking about in this series as user interface toolkits changed.
D'Artagnan's masterful implementation of the Humble Dialog Box vanquishes his foes, but he knows he'll need trusted companions now that the evil Lady de Winter surely knows he is in pursuit. Fortunately, D'Artagnan's trusty three companions are riding hard to join him. D'Artagnan smiles to himself and imagines his friends coming around the bend in the road:
- Supervising Controller -- I'm here to help the View with the harder cases!
- Passive View -- I only do what I'm told
- Presentation Model -- Just do what I do
D'Artagnan sees dust rising in the air, a traveler is coming...
To be continued in Part 3: Supervising Controller
QUICK NOTE: I showed the MessageBox stuff happening as a consequence of calling a method on the IView interface. In the past I've also used some sort of IMessageBoxCreator interface to create message boxes directly. There are advantages either way.
Posted
Wed, May 23 2007 7:29 AM
by
Jeremy D. Miller