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 or EventHandler<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 and Closed events), but this may not be a bad thing as tests are not really supposed to assert more than one thing.
Download full source code here
1 comment:
Another problem is that the RaiseClosed and RaiseOpened must be public.
Post a Comment