Testing

Beyond the Event Horizon: Delegate Basics

OLYMPUS DIGITAL CAMERAWith the release of ApprovalTests 2.0 we can now use EventApprovals to catalog the event handlers attached to our objects.  Using this feature of ApprovalTests, Stack Overflow questions like this one: How do I retrieve the Subscriber List for an Event at runtime? now have a real answer.

I asked that question almost 3 years ago.  After none other than Marc Gravell told me, “It usually means that you are doing something wrong (sorry, but it does),”  I worked out another way to test the code that led me to ask that question.  Although I accepted Marc’s answer on Stack Overflow, I think it is safe to say that I never accepted the answer in my heart.  Fast forward a few years, and yet again I found myself wishing that I could simply retrieve the subscriber list and verify it in a test.  But this time things were different.  I’m not afraid of reflection anymore, and now I have ApprovalTests available to process the potentially large amount of data that retrieving subscriber lists might produce.

So this time I wrote EventApprovals and got what I had wanted all along, the runtime subscriber list.  This CodeProject article by Alejandro Miralles was a great help getting started:  How to copy event handlers from one control to another at runtime.  With that jumping off point, I went through several revisions before telling Llewellyn, “I think I have something that might interest you…”

Along the way, I learned more about events and delegates than I ever thought I would need to know.  Indeed, my working title for this article was “More than you ever wanted to know about .NET events.”  Even though you can use ApprovalTests without knowing any of what follows, I felt like sharing some of the process that went into creating EventApprovals while it was still relatively fresh in my mind.  I thought it might be fun to re-implement a system similar to EventApprovals from memory.  You can find the code for this article on GitHub.  I figured I would make a lot of the same mistakes I made the first few times around.  I always learn better from an example that shows mistakes and how to correct them than I do from a clean implementation.  Hopefully you will too.

Along the way I’ll try to relate the concepts in this article to the parts of EventApprovals that can give you the same information without having to do the heavy lifting yourself.  So, lets get started with delegates in .NET.

Delegates

Delegates are at the heart of the event system in .NET.  And the “subscriber list” for an event is identical to the delegate’s invocation list.  When I have a direct reference to some delegate, this information is trivial to retrieve.  Here’s how.

Assume I have an instance of some delegate type:

Func<bool> truth = () => true;

In this example, our delegate is a Func<bool>, to determine the list of methods assigned to the delegate use the method called GetInvocationList.  This method returns an array of Delegate instances, and each Delegate has a Method property, which is a MethodInfo from the reflection namespace.  I’ll use a LINQ extension to project these Delegates into a collection of MethodInfos.

Something like this:

[TestMethod]
public void GetInvocationList()
{
    Func<bool> truth = () => true;
    IEnumerable<MethodInfo> methods = truth.GetInvocationList().Select(d => d.Method);
    Approvals.VerifyAll(methods, string.Empty);
}

Introduction to ApprovalTests

If you are already familiar with ApprovalTests, you can probably skip this section.  For those of you who are new to this testing library, read on for a brief introduction.  I also recommend that you watch this short video, where Llewellyn Falco, the creator of ApprovalTests, introduces the library.

Using ApprovalTests in .Net 01 MsTest

In the delegate example I wrote above, I use a call to Approvals.VerifyAll instead of using one of MSTest’s built in Assert methods.  VerifyAll will iterate over the MethodInfo collection and call ToString on each member.  These strings are collected into a buffer, then ApprovalTests shows me the results using whatever reporter I’ve specified with the UseReporter attribute.  In this case, I choose a DiffReporter.

The first time I run the test, ApprovalTests displays the results, then I can approve the results if they look right to me.  Once I have approved results, ApprovalTests will check the approved result against each subsequent test run result.  ApprovalTests will only alert me (with a failing test) if the test run produces different results from the approved results.

Invocation List Results

ApprovalTests reports these results after executing my first example:

[0] = Boolean <GetInvocationList>b__0()

I can see that only one method is assigned to this delegate. Its return type is Boolean, its name is <GetInvocationList>b__0 and it takes no parameters (indicated by the empty parenthesis()). The method has a strange compiler-generated name because it is anonymous. The list might be easier to read if I assign a ordinary named method to the delegate.

public class Domain
{
    public static bool AlwaysTrue()
    {
        return true;
    }
}

AlwaysTrue is a method which has a signature compatible with the type of the truth delegate instance, which means I can assign it to the delegate.

[TestMethod]
public void GetInvocationList()
{
    Func<bool> truth = Domain.AlwaysTrue;
    IEnumerable<MethodInfo> methods = truth.GetInvocationList().Select(d => d.Method);
    Approvals.VerifyAll(methods, string.Empty);
}

After assigning AlwaysTrue to truth I get the following output:

[0] = Boolean AlwaysTrue()

If I declare a Delegate instance I can use the static Delegate.Combine method to assign more than one method to a delegate:

[TestMethod]
public void GetMultiInvocationList()
{
    Func<bool> truth = Domain.AlwaysTrue;
    Func<bool> truthy = Domain.NeverFalse;
    Delegate multicast = Delegate.Combine(truth, truthy);
    IEnumerable<MethodInfo> methods = multicast.GetInvocationList().Select(d => d.Method);
    Approvals.VerifyAll(methods, string.Empty);
}

Here are the results:

[0] = Boolean AlwaysTrue()
[1] = Boolean NeverFalse()

Cleanup

Now that I have two tests which contain some code duplication, I’ll extract the duplication into a utility class called DelegateUtility for reuse.

public static class DelegateUtility
{
    public static void VerifyInvocationList(Delegate value)
    {
        Approvals.VerifyAll(
            value.GetInvocationList().Select(d => d.Method),
            string.Empty);
    }
}

After updating my tests they look like this, and they still pass:

[TestMethod]
public void GetInvocationList()
{
    Func<bool> truth = Domain.AlwaysTrue;
    DelegateUtility.VerifyInvocationList(truth);
}

[TestMethod]
public void GetMultiInvocationList()
{
    Func<bool> truth = Domain.AlwaysTrue;
    Func<bool> truthy = Domain.NeverFalse;
    DelegateUtility.VerifyInvocationList(Delegate.Combine(truth, truthy));
}

This was a trivial refactoring and only saved a few lines of code.  But its worth doing.  Its better to do it now when I only have two tests in my suite, than it is to wait until the suite is large and cumbersome to update.

Delegate Oddities

You may have heard that delegates are types.  This means that they are in the same category as classes, structs and interfaces.

What does it mean to be a “type”?  Well, lets look at some more familiar types: classes and interfaces.  A class and an interface are similar to each other in many ways, but they are not the same.  You can’t instantiate an interface, or define bodies for the members declared on an interface.  There are other differences, but this is enough to get an idea about what types are in .NET.  The language applies (and complier enforces) different rules about what you can and can’t do with each “type” of type.  So, delegates are not a special kind of class, they are not a special kind of interface.  Delegates are their own things with their own set of rules.  One of the rules is that the actual implementation is always compiler generated.

Lets look at what happens when the compiler encounters a delegate type definition like this:

public delegate int Operation(int a, int b);

The compiler generates a class derived from MulticastDelegate, which in turn derives from Delegate.  But only the complier can do this.  Just like you or I can’t create a method with a name like this: <GetInvocationList>b__0, we’re also prevented from inheriting from MulticastDelegate.  I’m bringing this up to explain a part of the VerifyInvocationList method that might be confusing.

Each generated MulticastDelegate descendant inherits a GetInvocationList method and a property called Method.  These two members are always related.  When the MulticastDelegate only has one method to invoke, the relationship looks like this:

[TestMethod]
public void UnicastDelegateHasInvocationList()
{
    Func<bool> truth = Domain.AlwaysTrue;
    Assert.AreEqual(
        truth.Method, 
        truth.GetInvocationList().Select(i => i.Method).Single());
}

As you can see, the Method property is also returned as a member of the Delegate array returned by GetInvocationList.  In the case of a logically unicast delegate, Method is the one and only array member.  In the case of a truly multicast delegate, things are a little different.

[TestMethod]
public void MulticastDelegateInvocationListContainsMethod()
{
    Func<bool> truth = Domain.AlwaysTrue;
    Func<bool> truthy = Domain.NeverFalse;
    Delegate combined = Delegate.Combine(truth, truthy);
    Assert.AreEqual(
        combined.Method,
        combined.GetInvocationList().Select(i => i.Method).Last());
}

In the case of a delegate that actually needs to multicast, the MethodInfo pointed at by the Method property is actually the last member of the invocation list.  In other words, when you call Delegate.Combine repeatedly, Method will always point at the last method you added to the delegate.

Here is some code that will never finish executing.

[TestMethod]
public void InfiniteLoop()
{
    Func<bool> truth = Domain.AlwaysTrue;
    this.Body(truth.GetInvocationList());
}

public void Body(Delegate[] delegates)
{
    foreach (var del in delegates)
    {
        this.Body(del.GetInvocationList());
    }
}

So we can’t crawl into each Delegate looking for more delegates, that leads to infinite recursion.  However, we don’t need to worry about it.  Delegate.Combine flattens invocation lists before combining them.  Here’s a test to demonstrate:

[TestMethod]
public void CombinedDelegatesAreFlattened()
{
    Func<bool> truth = Domain.AlwaysTrue;
    Func<bool> truthy = Domain.NeverFalse;
    Func<bool> answer43 = Domain.AlwaysTrue;
    Delegate combined = Delegate.Combine(truthy, answer43);
    Delegate flattened = Delegate.Combine(truth, combined);
    DelegateUtility.VerifyInvocationList(flattened);
}

And these are the results:

[0] = Boolean AlwaysTrue()
[1] = Boolean NeverFalse()
[2] = Boolean AlwaysTrue()

So, after playing with these tests we can see that the VerifyInvocationList implementaion is correct.

value.GetInvocationList().Select(d => d.Method)

First, I call GetInvocationList to get the flattened array of delegates.  Because these delegates are flattened, all the array members are logically unicast, so I can access the Method property to get the only member, rather than dereferencing through the array returned by GetInvocationList.

Relationship with EventApprovals

Like I said at the top, delegates are at the core of the .NET event system.  Likewise, the projection from a delegate into a collection of MethodInfos,(shown here as part of VerifyInvocationList) is at the heart of EventApprovals.

We didn’t extract VerifyInvocationList as a separate verification method the way I have done here.  If you have a delegate in hand, getting data from it isn’t very hard at all, and you can just use Approvals.VerifyAll instead of getting into the more specialized methods in ApprovalTests.Events or ApprovalUtilities.ReflectionUtilities.

Up Next

The next installment of this article is “Beyond the Event Horizon: Event Basics”  We’ll look at how events are implemented by the compiler and how they relate to delegates.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s