Recently, I got an email from someone who was trying to figure out how they should go about testing a private void class. They had an existing legacy app they were trying to get under test, and just weren’t sure how to approach it. The methods looked like:
private void menuItem15_Click(object sender, System.EventArgs e)
{
Fdialog = new FontDialog();
Fdialog.Font = rtbDisplay.Font;
if(Fdialog.ShowDialog()!= DialogResult.Cancel )
{
rtbDisplay.Font = Fdialog.Font;
}
}
which is one of the harder classes to test – an event handler that creates a UI widget, and sets some other UI widget.
So, how does one go about testing something like this? Well, first we start by asking what it is we want to test. In the above snippet, you have two seperate things going on. One, when a user clicks a certain menu item, they are asked what Font they want to change to. Two, when a user selects a Font from a specific dialog, the font of their display element should change.
My first suggestion was simply to manually test this. Click the button, choose a font from the display, and see if the display font changes. Conversely, click the button, choose cancel from the dialog, and make sure the display font doesn’t change.
And while that’s all well and good, it doesn’t answer the question of how we could test this in an automated fashion. First and foremost, we need to get this into a single-responsibilty harness so we can test the actions independently. Fairly simple, just extract method from the event handler:
private void menuItem15_Click(object sender, System.EventArgs e)
{
rtbDisplay.Font = GetFontFromUserSelection(rtbDisplay.Font);
}
public Font GetFontFromUserSelection(Font defaultFont)
{
Fdialog = new FontDialog();
Fdialog.Font = defaultFont);
if(Fdialog.ShowDialog()!= DialogResult.Cancel )
{
return Fdialog.Font;
}
return defaultFont;
}
That doesn’t really buy us a lot though, because even if we called GetFontFromUserSelection
from a test, we’d have to deal with a UI element. So, the next step is to handle only having the UI element called when we want to. To do that, I’m going to inject the control that GetFont
should use. To do that, I’m going to choose to have GetFont
act on an interface. So, now we have:
public interface IFontDialog
{
DialogResult ShowDialog();
Font Font{get; set;}
}
Which we now subclass FontDialog
so we can have it implement our interface:
public class MyFontDialog : FontDialog, IFontDialog
{
}
with those classes created, we can now do:
private void menuItem15_Click(object sender, System.EventArgs e)
{
rtbDisplay.Font = GetFontFromUserSelection(rtbDisplay.Font, new MyFontDialog());
}
public Font GetFontFromUserSelection(Font defaultFont, IFontDialog fontDialog)
{
fontDialog.Font = defaultFont);
if(fontDialog.ShowDialog()!= DialogResult.Cancel )
{
return fontDialog.Font;
}
return defaultFont;
}
Up to this point, the app should still work exactly as it did before. Unfortunately, because we don’t have any tests in place, we can’t know, but I’d be willing to bet on it (and I’m not much of a betting man).
So, now we have a nice seam where we can pass our own FontDialog
and see just what happens. So, let’s create a FontDialog
we have a little more control over:
public class MockFontDialog : IFontDialog
{
public MockFontDialog() {}
public Font userFont;
public Font MOCK_SELECTED_FONT = new Font("Times New Roman", 14);
public DialogResult ShowDialog()
{
return DialogResult.OK;
}
public Font Font
{
get{return MOCK_SELECTED_FONT;}
set{userFont = value;}
}
}
Based on that, we can now write our first test:
[Test]
public void UserSelectionOfANewFontShouldReturnTimesNewRoman()
{
Font defaultFont = new Font("Arial", 16);
Font expectedFont = new Font("Times New Roman", 14");
MockFontDialog fontDialog = new MockFontDialog();
MyForm form = new MyForm();
Assert.AreEqual(expectedFont, form.GetFontFromUserSelection(defaultFont, fontDialog));
}
And, our second, which I’m going to just call the event handler directly:
[Test]
public void UserSelectionOfANewFontShouldChangeTheDisplayFont()
{
Font defaultFont = new Font("Arial", 16);
Font expectedFont = new Font("Times New Roman", 14");
MockFontDialog fontDialog = new MockFontDialog();
MyForm form = new MyForm();
form.menuItem15_Click(null, null);
Assert.AreEqual(expectedFont, form.Display.Font);
}
Which fails! That’s because, even though we can inject a MockFontDialog
into the GetFontFromUserSelection
method, we don’t have a way of injecting it into the form.
There’s a couple of things we could do here. We could inject the FontDialog
into the Form itself. Or, we could have the form ask what FontDialog it should use, say, from a Factory method:
private void menuItem15_Click(object sender, System.EventArgs e)
{
IFontDialog fontDialog = FontDialogFactory.GetFontDialog();
rtbDisplay.Font =
GetFontFromUserSelection(rtbDisplay.Font, fontDialog);
}
We can then use the Factory to return the appropriate object based on whether we are testing. It’s not ideal, but such is the case when you are dealing with legacy code. I’ll use a configuration setting here to determine it:
public class FontDialogFactory
{
public static IFontDialog GetFontDialog()
{
bool weAreTesting =
bool.Parse(ConfigurationSettings.AppSettings["testing"]);
if(weAreTesting)
{
return new MockFontDialog();
}
else
{
return new MyFontDialog();
}
}
}
And there you have it. A way to test what is going on with UI elements using a sprinkling of mocks and a touch of dependency injection. Not pretty, but if you need to get your UI elements under test, this might be one option for you. Ideally your business and presentation logic would actually delegate to a presenter which you could then mock much easier (known as the Model-View Presenter pattern).
So, in summary. To test an “untestable” method, y
ou have to inject some sort of a seam. Whether it be a variable inside whose results you can test, a side-effect you can check, or extracting a method and injecting mocks, getting a seam in the method is the most vital part to getting it under test.
Great post! One thing though…
If you’re going to add a configuration item to influence which IFontDialog that the FontDialogFactory will create, why not just put the class name of the font dialog in the config and then instantiate it using the System.Reflection.Actovator? This will free your code from external environmental conditions, and will know couple Cyclomatic Complexity points off of the GetFontDialog method.
And… if you add the class name to configuration, you might as well just use Spring to activate the instance using it’s XmlObjectFactory or ApplicationContext so that you don’t have to write the activation code yourself :)
Doh! That should have read “…knock a couple Cyclomatic Complexity points off of the…”
I think the revision has changed behavior. In the original, the current font was unaffected if the user clicked cancel; in the revision, it is set to the default. So I’d write a test where the current font was not default and cancel gets “clicked”.
Cory…don’t know if this’ll make it back to you or not, sure hope so. Please tell Crissie that I thoroughly enjoyed the tour of your photo gallery – “baby”, wedding pics, and especially Gail’s awesome “jump”! Glad you all are doing so great! Good luck w/the pregnancy and baby preparations. Give “Mommy” a big hug from a very looooong time old friend and fan! Cheers…BJ
I think that the GetFontFromUserSeletion function belongs on a separate class. I have added an entry to my blog giving more detail at: http://www.livejournal.com/users/anthony_w/8000.html
“I think the revision has changed behavior. In the original, the current font was unaffected if the user clicked cancel; in the revision, it is set to the default. So I’d write a test where the current font was not default and cancel gets “clicked”.”
Hi Carl,
You do have a point, although the Font technically wouldn’t change, since the default font is passed in, so if the user hits cancel it would just get set to what it was before.
This could cause events to fire, like OnFontChange events. So perhaps extracting the method wasn’t the best way to preserve behavior.
Thanks for pointing it out!