Asserting events with NUnit
Suppose we want to write unit tests for a class that raises events. We want to check that the right events are raised, the right number are raised, and that they have the correct parameters. Here’s an example of a class that we might want to test:
class Blah
{
public event EventHandler Closed;
public event EventHandler<OpenedEventArgs> Opened;
public void RaiseClosed(int count)
{
for(int n = 0; n < count; n++)
{
Closed(this, EventArgs.Empty);
}
}
public void RaiseOpened(int count)
{
for(int n = 0; n < count; n++)
{
Opened(this, new OpenedEventArgs() { Message = String.Format("Message {0}", n + 1) });
}
}
}
class OpenedEventArgs : EventArgs
{
public string Message { get; set; }
}
To write NUnit test cases for this class, I would usually end up writing an event handler, and storing the parameters of the event in a field like follows:
EventArgs blahEventArgs;
[Test]
public void TestRaisesClosedEvent()
{
Blah blah = new Blah();
blahEventArgs = null;
blah.Closed += new EventHandler(blah_Closed);
blah.RaiseClosed(1);
Assert.IsNotNull(blahEventArgs);
}
void blah_Closed(object sender, EventArgs e)
{
blahEventArgs = e;
}
While this approach works, it doesn’t feel quite right to me. It is fragile – forget to set blahEventArgs
back to null and you inadvertently break other tests. I was left wondering what it would take to create an Assert.Raises method that removed the need for a private variable and method for handling the event.
My ideal syntax would be the following:
Blah blah = new Blah();
var args = Assert.Raises<OpenedEventArgs>(blah.Opened, () => blah.RaiseOpened(1));
Assert.AreEqual("Message 1", args.Message);
The trouble is, you can’t specify blah.Opened
as a parameter – this will cause a compile error. So I have to settle for second best and pass the object that raises the event, and the name of the event. So here’s my attempt at creating Assert.Raise
, plus an Assert.RaisesMany
method that allows you to see how many were raised and examine the EventArgs
for each one:
public static EventArgs Raises(object raiser, string eventName, Action function)
{
return Raises<EventArgs>(raiser, eventName, function);
}
public static T Raises<T>(object raiser, string eventName, Action function) where T:EventArgs
{
var listener = new EventListener<T>(raiser, eventName);
function.Invoke();
Assert.AreEqual(1, listener.SavedArgs.Count);
return listener.SavedArgs[0];
}
public static IList<T> RaisesMany<T>(object raiser, string eventName, Action function) where T : EventArgs
{
var listener = new EventListener<T>(raiser, eventName);
function.Invoke();
return listener.SavedArgs;
}
class EventListener<T> where T : EventArgs
{
private List<T> savedArgs = new List<T>();
public EventListener(object raiser, string eventName)
{
EventInfo eventInfo = raiser.GetType().GetEvent(eventName);
var handler = Delegate.CreateDelegate(eventInfo.EventHandlerType, this, "EventHandler");
eventInfo.AddEventHandler(raiser, handler);
}
private void EventHandler(object sender, T args)
{
savedArgs.Add(args);
}
public IList<T> SavedArgs { get { return savedArgs; } }
}
This allows us to dispense with the private field and event handler method in our test cases, and have nice clean test code:
[Test]
public void TestCanCheckRaisesEventArgs()
{
Blah blah = new Blah();
AssertExtensions.Raises(blah, "Closed", () => blah.RaiseClosed(1));
}
[Test]
public void TestCanCheckRaisesGenericEventArgs()
{
Blah blah = new Blah();
var args = AssertExtensions.Raises<OpenedEventArgs>(blah, "Opened", () => blah.RaiseOpened(1));
Assert.AreEqual("Message 1", args.Message);
}
[Test]
public void TestCanCheckRaisesMany()
{
Blah blah = new Blah();
var args = AssertExtensions.RaisesMany<OpenedEventArgs>(blah, "Opened", () => blah.RaiseOpened(5));
Assert.AreEqual(5, args.Count);
Assert.AreEqual("Message 3", args[2].Message);
}
There are a couple of non-optimal features to this solution:
- Having to specify the event name as a string is ugly, but there doesn’t seem to be a clean way of doing this.
- It expects that your events are all of type
EventHandler
orEventHandler<T>
. Any other delegate won’t work. - It throws away the sender parameter, but you might want to test this.
- You can only test on one particular event being raised in a single test (e.g. can’t test that a function raises both the
Opened
andClosed
events), but this may not be a bad thing as tests are not really supposed to assert more than one thing.
I would be interested to know if anyone has a better way of testing that objects raise events. Am I missing a trick here? Any suggestions for how I can make my Raises
function even better?
Download full source code here
Comments
Another problem is that the RaiseClosed and RaiseOpened must be public.
Anonymous