private override

3Sep/095

Testing Events with Awesomeness

How many of you have written this code when testing an event?

[Test]
public void LameTest()
{
    var objectWithEvent = new ClassWithEvent();
 
    var awesomeFired = false;
    objectWithEvent.AwesomeEvent += ( o, e ) => awesomeFired = true;
 
    objectWithEvent.DoSomethingAwesome();
 
    Assert.IsTrue( awesomeFired );
}
 

view raw This Gist brought to you by GitHub.

or its big brother:

[Test]
public void LessLameTest()
{
    var objectWithEvent = new ClassWithEvent();
 
    var awesomeCount = 0;
    objectWithEvent.AwesomeEvent += ( o, e ) => awesomeCount++;
 
    objectWithEvent.DoSomethingAwesome();
 
    Assert.AreEqual( 1, awesomeCount );
}
 

view raw This Gist brought to you by GitHub.

Every time I write that code, it feels WET (read: not DRY), especially when I’m doing it for more than one event in the same test.

What I’d like to see is something like:

[Test]
public void AwesomeTest()
{
    var objectWithEvent = new ClassWithEvent();
 
    var awesomeTracker = new EventTracker<EventHandler>();
    objectWithEvent.AwesomeEvent += awesomeTracker.Delegate;
 
    objectWithEvent.DoSomethingAwesome();
 
    Assert.IsTrue( awesomeTracker.WasCalled );
}
 

view raw This Gist brought to you by GitHub.

Yeah!  Now that’s what I’m looking for.  Is this possible?

Yup.

How?  Let’s see.

So basically what this means, is that we have to generate a method (at runtime) that matches the signature of the delegate we pass to the tracker as the type argument (so it can be added as a handler for the event).  Inside the method, we need to update our tracking variable (either setting a flag, or incrementing a counter).  We can ignore any of the arguments passed to the delegate (e.g. the sender, event args, or anything else), since for our purpose we really don’t care about them.  What I think is so cool about this is that it works for ANY type of delegate you pass to it!  Sort of like generics, but for methods (makes sense in a meta sort of way).

Here is the implementation:

public class EventTracker<TDelegate>
    where TDelegate : class
{
    public static implicit operator TDelegate( EventTracker<TDelegate> tracker )
    {
        return tracker.Delegate;
    }
 
    public EventTracker()
    {
        var delegateMeta = typeof( TDelegate ).GetMethod( "Invoke" );
        var delegateParams = delegateMeta
            .GetParameters()
            .Map( param => param.ParameterType )
            .Prepend( GetType() )
            .ToArray<Type>();
 
        var invokedMethod = GetType().GetMethod( "Invoked", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public );
 
        var dynamicHandler = new DynamicMethod( "", delegateMeta.ReturnType, delegateParams, GetType(), true );
        var generator = dynamicHandler.GetILGenerator();
        generator.Emit( OpCodes.Ldarg_0 );
        generator.Emit( OpCodes.Call, invokedMethod );
        generator.Emit( OpCodes.Ret );
 
        Delegate = dynamicHandler.CreateDelegate( typeof( TDelegate ), this ) as TDelegate;
    }
 
    public TDelegate Delegate
    {
        get;
        private set;
    }
 
    public int CallCount
    {
        get;
        private set;
    }
 
    public bool WasCalled
    {
        get { return CallCount > 0; }
    }
 
    public bool WasNotCalled
    {
        get { return !WasCalled; }
    }
 
    // called via the dynamic method
    private void Invoked()
    {
        CallCount++;
    }
}

view raw This Gist brought to you by GitHub.

This is my first foray into IL generation… not exactly my forte.  A little more on IL generation next time.  Oh yeah, must give credit where it is due… thanks to Shawn for the help when my fu was weak.

Comments (5) Trackbacks (0)
  1. Can you explain why the implicit operator is needed in EventTracker?

  2. The implicit operator is there as a convenience so you could actually assign your tracker to the event, rather than tracker.Delegate.
    (e.g. objectWithEvent.AwesomeEvent += tracker;)

    It’s definitely not needed, but an added convenience if you like it.

  3. I like it!

  4. More awesomeness:


    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Reflection.Emit;
    using System.Reflection;
    using System.Collections;

    namespace TestLibrary.Library
    {
    public class AnotherEventTracker
    where TDelegate : class
    {
    public static implicit operator TDelegate(AnotherEventTracker tracker)
    {
    return tracker.Delegate;
    }

    public static implicit operator bool(AnotherEventTracker tracker)
    {
    return tracker.Verify();
    }

    private int _callCount;
    private List<Func> _checks;

    public AnotherEventTracker()
    {
    _checks = new List<Func>();

    var delegateMeta = typeof(TDelegate).GetMethod("Invoke");

    var delegateParams = delegateMeta
    .GetParameters()
    .Map(param => param.ParameterType)
    .Prepend(GetType())
    .ToArray();

    var invokedMethod = GetType().GetMethod("Invoked", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
    var dynamicHandler = new DynamicMethod("", delegateMeta.ReturnType, delegateParams, GetType(), true);
    var generator = dynamicHandler.GetILGenerator();

    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Call, invokedMethod);
    generator.Emit(OpCodes.Ret);
    Delegate = dynamicHandler.CreateDelegate(typeof(TDelegate), this) as TDelegate;
    }

    public TDelegate Delegate
    {
    get;
    private set;
    }

    public AnotherEventTracker WasCalled()
    {
    _checks.Add(() => _callCount > 0);
    return this;
    }

    public AnotherEventTracker Once()
    {
    return Exactly(1);
    }

    public AnotherEventTracker Twice()
    {
    return Exactly(2);
    }

    public AnotherEventTracker Exactly(int numberOfTimes)
    {
    _checks.Add(() => _callCount == numberOfTimes);
    return this;
    }

    public bool Verify()
    {
    return _checks.TrueForAll(func => func());
    }

    // called via the dynamic method
    private void Invoked()
    {
    _callCount++;
    }
    }
    }

    I might make this into a post myself. This was pretty fun, making a closed event tracker that looks like Moq.

  5. Since the code blocks apparently fail…

    http://snipt.org/mJp


Leave a comment


No trackbacks yet.