One of the problems with NMock 2.0 is the inability to raise events on a mock object. I've run across this problem in many scenarios, but most frequently in testing passive views. For example in a passive view asp.net application your interface will probably look something like:
public interface IAccountSummaryView
{
event EventHandler Load;
void Display(AccountSummaryDTO[] accounts);
}
The presenter then registers an EventHandler with the Load event and performs the display as follows:
public class AccountSummaryPresenter
{
private readonly IAccountSummaryView view;
public AccountSummaryPresenter(IAccountSummaryView view)
{
this.view = view;
HookupEventHandlers(view);
}
private void HookupEventHandlers(IAccountSummaryView view)
{
view.Load += delegate { LoadAccountsInto(view); };
}
private void LoadAccountsInto(IAccountSummaryView view)
{
view.Display(GetAccountSummaries());
}
}
You'll notice that the LoadAccountsInto method is not public because the call is handled internally within the presenter class. This makes testing a little tricky since there is no facility in NMock 2.0 to raise this event and we have no way to call the LoadAccountsInto method from our test class.
[Test]
public void ShouldLoadAccountsIntoViewOnPageLoad()
{
Expect.Once.On(view).Method("Display").With(Is.Anything);
presenter.??????
mockery.VerifyAllExpectationsHaveBeenMet();
}
Which more often than not results in granting the LoadAccountsInto method public accessibility for the purpose of testing:
[Test]
public void ShouldLoadAccountsIntoViewOnPageLoad()
{
Expect.Once.On(view).Method("Display").With(Is.Anything);
presenter.LoadAccountsInto(view);
mockery.VerifyAllExpectationsHaveBeenMet();
}
Not a big deal, right? Well, I'll admit this gets the job done. However, whenever I find myself exposing more than is absolutely needed for a somewhat orthogonal task (such as testing, or data access) I get a sinking feeling in my gut that something just isn't right. Perhaps its because allowing this method to run around the application with public accessability gives other developers the wrong impression about it's intended use. And we all know code that doesn't communicate is a Bad Thing.
So, I spent a few minutes today to piece together the following NMock helper classes that will allow me to simulate raising an event:
public class MockEvent
{
private EventHandler handler;
public void Initialize(EventHandler handler)
{
this.handler = handler;
}
public void Raise()
{
handler.Invoke(null, null);
}
public static IAction Hookup(MockEvent mockEvent)
{
return new MockEventHookup(mockEvent);
}
}
public class MockEventHookup : IAction
{
private readonly MockEvent mockEvent;
public MockEventHookup(MockEvent mockEvent)
{
this.mockEvent = mockEvent;
}
public void Invoke(Invocation invocation)
{
EventHandler handler = invocation.Parameters[0] as EventHandler;
if (handler == null)
throw new Exception("Unknown event handler type.");
mockEvent.Initialize(handler);
}
public void DescribeTo(TextWriter writer)
{
// do nothing
}
}
The MockEvent class is simply a wrapper around the handler that gets registered with the exposed event on the mock object. Right now the MockEvent only handles the handlers of type EventHandler. Its pretty simplistic but I'm sure you can figure out how to extend it for your own purposes.
Here's what my test looks like now:
[TestFixture]
public class AccountSummaryPresenterTest
{
private Mockery mockery;
private IAccountSummaryView view;
private MockEvent loadEvent;
private AccountSummaryPresenter presenter;
[SetUp]
public void SetUp()
{
mockery = new Mockery();
view = mockery.NewMock<IAccountSummaryView>();
loadEvent = new MockEvent();
Expect.Once.On(view).EventAdd("Load", Is.Anything).Will(MockEvent.Hookup(loadEvent));
presenter = new AccountSummaryPresenter(view);
}
[Test]
public void ShouldLoadAccountsIntoViewOnPageLoad()
{
Expect.Once.On(view).Method("Display").With(Is.Anything);
loadEvent.Raise();
mockery.VerifyAllExpectationsHaveBeenMet();
}
}
Hope this helps somebody else out there sleep better at night ;-)
