I've been doing a fair bit of asynchronous logic recently where I'm designing objects to raise events after long-running operations have completed, and naturally this creates a few challenges during testing. When a method uses the asynchronous pattern, the work is executed on a separate thread and execution returns immediately to the caller. This is a problem for the a unit test as they are by design a series of standalone sequential steps -- the test may finish or execute assertions before the event is called.
The immediate workaround is to add delays into the tests using a Thread.Sleep statement.
[Test] public void AsynchronousTestsDontAlwaysWork() { bool eventCalled = false; var subject = new MyClass(); subject.OnComplete += (o,e) => eventCalled = true; subject.DoWorkAsync(); Thread.Sleep(100); Assert.AreTrue( eventCalled ); // can fail because the event might raise after this assertion }
This strategy will work, but it's dangerous in the long-term because the duration of the asynchronous operation is dependent on the complexity of the implementation and the environment the test is executing in. In other words, you may need to increase sleep interval if the implementation needs to do more work or if the tests fail on slower machines. In the end, the sleep interval reflects the lowest common denominator in your environment (that slow laptop that intern uses) and the entire test run becomes painfully (and unnecessarily) slow.
The solution is to add a little extra plumbing to block execution until the event is fired.
[Test] public void AsynchronousTestWithBlockingAlwaysWorks() { var reset = new System.Threading.ManualResetEvent(false); var subject = new MyClass(); subject.OnComplete += (o,e) => reset.Set(); subject.DoWorkAsync(); if (!reset.WaitOne(1000)) // block for at most 1 second { Assert.Fail("The test timed out waiting for the OnComplete event."); } }
The above example uses the ManualResetEvent, which in short is a signalling mechanism. The WaitOne method blocks execution until another thread calls the Set method or the timeout value is exceeded. In the above example, I've set up an anonymous delegate for my event so that the test finishes as soon as the asynchronous logic completes.
Cleaning it up...
As I mentioned earlier, I've been doing a lot of this type of testing so the above pattern has appeared in more than one test. It would be nice to encapsulate this a bit and streamline it into a reusable utility. This post on StackOverflow had an interesting concept to wrap the blocking logic into an object that supports IDisposable. The original poster was attempting to use Expressions to access events, which sadly isn't possible from outside the class, but here's an updated example that uses reflection and a named parameter for the event:
public sealed class EventWatcher : IDisposable { private readonly object _target; private readonly EventInfo _eventInfo; private readonly Delegate _listener; private readonly ManualResetEvent _reset; private readonly int _timeout; public static EventWatcher Create<T>(T target, string eventName, int timeout = 1000) { EventInfo eventInfo = typeof (T).GetEvent(eventName); if (eventInfo == null) throw new ArgumentException("Event not found.", "eventName"); return new EventWatcher(target, eventInfo, timeout); } private EventWatcher(object target, EventInfo eventInfo, int timeout) { _target = target; _eventInfo = eventInfo; _timeout = timeout; _reset = new ManualResetEvent(false); // Create our event listener _listener = CreateEventListenerDelegate(eventInfo.EventHandlerType); _eventInfo.AddEventHandler(target, _listener); } public void SetEventWasRaised() { _reset.Set(); } private Delegate CreateEventListenerDelegate(Type eventType) { // Create the event listener's body, setting the 'eventWasRaised_' field. var setMethod = typeof(EventWatcher).GetMethod("SetEventWasRaised"); var body = Expression.Call(Expression.Constant(this), setMethod); // Get the event delegate's parameters from its 'Invoke' method. MethodInfo invokeMethod = eventType.GetMethod("Invoke"); var parameters = invokeMethod.GetParameters() .Select((p) => Expression.Parameter(p.ParameterType, p.Name)); // Create the listener. var listener = Expression.Lambda(eventType, body, parameters); return listener.Compile(); } public void Dispose() { bool eventWasFired = _reset.WaitOne(_timeout); // Remove the event listener. _eventInfo.RemoveEventHandler(_target,_listener); if (!eventWasFired) { string message = String.Format("Test timed-out waiting for event \"{0}\" to be raised.", _eventInfo.Name); Assert.Fail(message); } } } [Test] public void DemonstrateEventWatcherWithUsingBlockAndDisposePattern() { var subject = new MyClass(); using(EventWatcher.Create(subject, "OnComplete")) { subject.DoWorkAsync(); } // throws Assert.Fail() if event wasn't called. }
Some Refinements...
This solution works, but there are a few drawbacks:
- The using block is a very succinct mechanism for controlling scope, but unfortunately if an exception occurs in the body of the using statement it will get swallowed by the exception in our EventWatcher's Dispose method.
- This only validates that the event was called, and it doesn't give us access to the event argument, which might be of interest to the test.
- The event name is a literal string value which isn't very refactor friendly.
Preventing Hidden Exceptions from the Finally block
To address the first problem where exceptions aren't properly handled inside the using statement, we change the signature slightly to perform the test logic in an Action.
// snip... public static void WaitForEvent<T>(T target, string eventName, Action action) { var watcher = Create(target, eventName); try { action(); } catch (Exception) { watcher.Clear(); throw; } finally { watcher.Dispose(); } } // ...end snip [Test] public void DemonstrateEventWatcherWithAnActionInsteadOfUsingBlock() { EventWatcher.WaitForEvent(subject, "OnComplete", () => subject.DoWorkAsync()); }
Gaining Access to the EventArgs
Addressing the second problem requires some changes to the CreateEventListenerDelegate so that the arguments of the event are recorded.
//snip public void SetEventWasRaised(EventArgs e) { EventArg = e; _reset.Set(); } public EventArgs EventArg { get; private set; } private Delegate CreateEventListenerDelegate(Type eventType) { // Get the event delegate's parameters from its 'Invoke' method. MethodInfo invokeMethod = eventType.GetMethod("Invoke"); var parameters = invokeMethod.GetParameters() .Select((p) => Expression.Parameter(p.ParameterType, p.Name)).ToList(); // Setup the callback to pass in the EventArgs var setMethod = typeof(EventWatcher).GetMethod("SetEventWasRaised"); var body = Expression.Call(Expression.Constant(this), setMethod, parameters[1]); // Create the listener. var listener = Expression.Lambda(eventType, body, parameters); return listener.Compile(); } // end snip [Test] public void DemonstrateEventWatcherCanCaptureEventArgs() { EventArgs e; EventWatcher.WaitForEvent(subject, "OnComplete", out e, t => t.DoWorkAsync()); Assert.IsNotNull(e); }
Making it Refactor-Friendly...
Tackling the last problem where we don't have type-safety for our event, we need a special workaround because we're unable to use an Expression to capture our event. From a language perspective, C# will only allow external callers to add or remove event handler assignments, so we can't write a lamda expression that points to an event reference. Hopefully this is something that will be added to the language in a future version, but until then accessing the event requires some special hackery.
Taking a play from the Moq source code, we can infer the event name by using a dynamic proxy and intercepting the method invocation when we perform an event assignment. It's a long and awkward way around, but it works, albeit it comes with all the caveats of using a dynamic proxy: non-sealed classes with virtual events, MarshalByRefObject, or interfaces.
A note about virtual events: I’m personally not a big fan of virtual events (Resharper warnings, etc), but if you’re comfortable putting the event declarations on an interface, you can explicitly specify the interface in the generic type argument. EventWatcher.Create<IMyInterface>(…)
public class EventAssignmentInterceptor : Castle.DynamicProxy.IInterceptor { private Castle.DynamicProxy.IInvocation _lastInvocation; #region IInterceptor implementation public void Intercept(IInvocation invocation) { _lastInvocation = invocation; } #endregion public string GetEventName() { string methodName = _lastInvocation.Method.Name; return methodName.Replace("add_", "").Replace("remove_", ""); } } public class EventHelper { public static string GetEventName<T>(T target, Action<T> action) where T : class { var generator = new Castle.DynamicProxy.ProxyGenerator(); var interceptor = new EventAssignmentInterceptor(); T proxy; if (typeof(T).IsInterface) { proxy = generator.CreateInterfaceProxyWithoutTarget<T>(interceptor); } else { proxy = generator.CreateClassProxy<T>(interceptor); } action(proxy); return interceptor.GetEventName(); } }
Examples
Here’s a few examples that illustrate the various syntax. Note, for sake a brevity (this post is crazy large) the overloads that define events as literals are not shown.
Basic usage in a using block. Note that exceptions inside the using may be swallowed by the dispose, and there's no access to the event args.
using(EventWatcher.Create(subject, t => t.OnComplete += null)) { subject.PerformWork(); } using(EventWatcher.Create(subject, t => t.OnComplete += null, timeout:1000) { subject.PerformWork(); }
Condensed usage. Provides access to the EventArgs and avoids the dispose problem.
EventArgs e = EventWatcher.Create(subject, t=> t.OnComplete +=null, () => subject.PerformWork() ); EventArgs e; EventWatcher.Create(subject, t=> t.OnComplete += null, out e, ()=> subject.PerformWork() );
Fluent usage. A cleaner syntax that declares the event and the action separately.
EventArgs e = EventWatcher .For(subject, t => t.OnComplete += null) .WaitOne(() => subject.PerformWork()) EventArgs e; EventWatcher.For(subject, t=> t.OnComplete += null) .WaitOne( () => subject.PerformWork(), out e);
Full Source
The full source is here.
public sealed class EventWatcher : IDisposable { private readonly object _target; private readonly EventInfo _eventInfo; private readonly Delegate _listener; private readonly ManualResetEvent _reset; private readonly int _timeout; #region Create public static EventWatcher Create<T>(T target, string eventName, int timeout = 1000) { EventInfo eventInfo = typeof(T).GetEvent(eventName); if (eventInfo == null) throw new ArgumentException("Event not found.", "eventName"); return new EventWatcher(target, eventInfo, timeout); } public static EventWatcher Create<T>(T subject, Action<T> eventAssignment, int timeout = 1000) where T : class { string eventName = EventHelper.GetEventName(subject, eventAssignment); return Create(subject, eventName, timeout); } #endregion #region For public static EventWatcherSetup<T> For<T>(T subject, Action<T> eventAssignment) where T : class { string eventName = EventHelper.GetEventName(subject, eventAssignment); return For(subject, eventName); } public static EventWatcherSetup<T> For<T>(T subject, string eventName) { return new EventWatcherSetup<T>(subject, eventName); } #endregion #region WaitForEvent public static EventArgs WaitForEvent<T>(T target, Action<T> eventAssignment, Action action) where T : class { EventArgs e; WaitForEvent(target, eventAssignment, out e, action); return e; } public static void WaitForEvent<T>(T target, Action<T> eventAssignment, out EventArgs e, Action action) where T : class { string eventName = EventHelper.GetEventName(target, eventAssignment); WaitForEvent(target, eventName, out e, action); } public static EventArgs WaitForEvent<T>(T target, string eventName, Action action) { EventArgs e; WaitForEvent(target, eventName, out e, action); return e; } public static void WaitForEvent<T>(T target, string eventName, out EventArgs e, Action action) { var watcher = Create(target, eventName); try { action(); } catch (Exception) { watcher.Clear(); throw; } finally { e = watcher.EventArg; watcher.Dispose(); } } #endregion private EventWatcher(object target, EventInfo eventInfo, int timeout) { _target = target; _eventInfo = eventInfo; _timeout = timeout; _reset = new ManualResetEvent(false); // Create our event listener _listener = CreateEventListenerDelegate(eventInfo.EventHandlerType); _eventInfo.AddEventHandler(target, _listener); } public void SetEventWasRaised(EventArgs e) { EventArg = e; _reset.Set(); } public void Clear() { _reset.Set(); } public EventArgs EventArg { get; private set; } private Delegate CreateEventListenerDelegate(Type eventType) { // Get the event delegate's parameters from its 'Invoke' method. MethodInfo invokeMethod = eventType.GetMethod("Invoke"); var parameters = invokeMethod.GetParameters() .Select((p) => Expression.Parameter(p.ParameterType, p.Name)).ToList(); var setMethod = typeof(EventWatcher).GetMethod("SetEventWasRaised"); var body = Expression.Call(Expression.Constant(this), setMethod, parameters[1]); // Create the listener. var listener = Expression.Lambda(eventType, body, parameters); return listener.Compile(); } public void Dispose() { bool eventWasFired = _reset.WaitOne(_timeout); // Remove the event listener. _eventInfo.RemoveEventHandler(_target,_listener); if (!eventWasFired) { string message = String.Format("Test timed-out waiting for event \"{0}\" to be raised.", _eventInfo.Name); Assert.Fail(message); } } } public class EventWatcherSetup<T> { private readonly T _target; private readonly string _eventName; public EventWatcherSetup(T target, string eventName) { _target = target; _eventName = eventName; } public EventArgs WaitOne(Action action) { EventArgs e; WaitOne(action, out e); return e; } public void WaitOne(Action action, out EventArgs e) { EventWatcher.WaitForEvent(_target,_eventName, out e, action); } } public class EventAssignmentInterceptor : Castle.DynamicProxy.IInterceptor { public Castle.DynamicProxy.IInvocation LastInvocation; #region Implementation of IInterceptor public void Intercept(IInvocation invocation) { LastInvocation = invocation; } #endregion public string GetEventName() { string methodName = LastInvocation.Method.Name; return methodName.Replace("add_", "").Replace("remove_", ""); } } public class EventHelper { public static string GetEventName<T>(T target, Action<T> action) where T : class { var generator = new Castle.DynamicProxy.ProxyGenerator(); var interceptor = new EventAssignmentInterceptor(); T proxy; if (typeof(T).IsInterface) { proxy = generator.CreateInterfaceProxyWithoutTarget<T>(interceptor); } else { proxy = generator.CreateClassProxy<T>(interceptor); } action(proxy); return interceptor.GetEventName(); } }
2 comments:
Sounds interesting but I tried to use it and hit the issue about the code assuming events always have two arguments.
My event declaration looks like this:
public event Action EventOccured;
I appreciate your comment and feedback, but respectfully an Action is not a valid event delegate. Best practices and well respected authorities outline that the event should publish who raised the event and any details surrounding that event.
Note that...
public event EventHandler EventOccurred;
...would satisfy best practices, and ...
foo.EventOccurred += (o,e) => CallMethod();
... would achieve the same effect of assigning an Action
Post a Comment