Unit testing that an event is raised in C#, using reflection
Asked Answered
A

5

13

I want to test that setting a certain property (or more generally, executing some code) raises a certain event on my object. In that respect my problem is similar to Unit testing that an event is raised in C#, but I need a lot of these tests and I hate boilerplate. So I'm looking for a more general solution, using reflection.

Ideally, I would like to do something like this:

[TestMethod]
public void TestWidth() {
    MyClass myObject = new MyClass();
    AssertRaisesEvent(() => { myObject.Width = 42; }, myObject, "WidthChanged");
}

For the implementation of the AssertRaisesEvent, I've come this far:

private void AssertRaisesEvent(Action action, object obj, string eventName)
{
    EventInfo eventInfo = obj.GetType().GetEvent(eventName);
    int raisedCount = 0;
    Action incrementer = () => { ++raisedCount; };
    Delegate handler = /* what goes here? */;

    eventInfo.AddEventHandler(obj, handler);
    action.Invoke();
    eventInfo.RemoveEventHandler(obj, handler);

    Assert.AreEqual(1, raisedCount);
}

As you can see, my problem lies in creating a Delegate of the appropriate type for this event. The delegate should do nothing except invoke incrementer.

Because of all the syntactic syrup in C#, my notion of how delegates and events really work is a bit hazy. This is also the first time I dabble in reflection. What's the missing part?

Ackley answered 2/4, 2010 at 14:11 Comment(0)
A
9

I recently wrote a series of blog posts on unit testing event sequences for objects that publish both synchronous and asynchronous events. The posts describe a unit testing approach and framework, and provides the full source code with tests.

I describe the implementation of an "event monitor" which allows writing event sequencing unit tests to be written more cleanly i.e. getting rid of all the messy boilerplate code.

Using the event monitor described in my article, tests can be written like so:

var publisher = new AsyncEventPublisher();

Action test = () =>
{
    publisher.RaiseA();
    publisher.RaiseB();
    publisher.RaiseC();
};

var expectedSequence = new[] { "EventA", "EventB", "EventC" };

EventMonitor.Assert(publisher, test, expectedSequence);

Or for a type that implements INotifyPropertyChanged:

var publisher = new PropertyChangedEventPublisher();

Action test = () =>
{
    publisher.X = 1;
    publisher.Y = 2;
};

var expectedSequence = new[] { "X", "Y" };

EventMonitor.Assert(publisher, test, expectedSequence);

And for the case in the original question:

MyClass myObject = new MyClass();
EventMonitor.Assert(myObject, () => { myObject.Width = 42; }, "Width");

The EventMonitor does all the heavy lifting and will run the test (action) and assert that events are raised in the expected sequence (expectedSequence). It also prints out nice diagnostic messages on test failure. Reflection and IL are used under the hood to get the dynamic event subscription working, but this is all nicely encapsulated, so only code like the above is required to write event tests.

There's a lot of detail in the posts describing the issues and approaches, and source code too:

http://gojisoft.com/blog/2010/04/22/event-sequence-unit-testing-part-1/

Aara answered 23/4, 2010 at 10:14 Comment(5)
Slick! If that doesn't answer the question, nothing does.Ackley
Hi Thomas, updated code examples after re-reading original question. I tweaked the API of EventMonitor to make it a little bit more pithy. Cheers. :)Aara
Very nice indeed, but it doesn't even come close to handling "arbitrary events" as you've claimed in your blog. Probably 99% of all events in the wild follow MS's recommendation though, so the lack of generality should be a big problem in practice.Accomplice
I think I understand what you're driving at, but I wouldn't say it doesn't "even come close" - it will cover just about 99% (and probably 99.99%) of what's out there. I will update the source code with the MSIL that will manage ref and out argument mangling (if that's what you mean) as I do have it to hand in something else I did. I thought the blog article was getting a bit bloated as it was! But for correctness, it should at least be in the source... Thanks.Aara
Would that source code be available still? Would like to see if i could use it to unit test event handlers nowdays :)Addressee
A
8

With lambdas you can do this with very little code. Just assign a lambda to the event, and set a value in the handler. No need for reflection and you gain strongly typed refactoring

[TestFixture]
public class TestClass
{
    [Test]
    public void TestEventRaised()
    {
        // arrange
        var called = false;

        var test = new ObjectUnderTest();
        test.WidthChanged += (sender, args) => called = true;

        // act
        test.Width = 42;

        // assert
        Assert.IsTrue(called);
    }

    private class ObjectUnderTest
    {
        private int _width;
        public event EventHandler WidthChanged;

        public int Width
        {
            get { return _width; }
            set
            {
                _width = value; OnWidthChanged();
            }
        }

        private void OnWidthChanged()
        {
            var handler = WidthChanged;
            if (handler != null)
                handler(this, EventArgs.Empty);
        }
    }
}
Ackley answered 2/4, 2010 at 14:25 Comment(2)
Yah, I know. But it's still 4 lines where 1 should suffice. I'm just trying to see if I can do it more generally.Ackley
+1 this is an under-appreciated solution. It's easy to read, understand and debug. It avoids dependencies on complicated code (MSIL, Reflection, etc.), and can cover pretty much all edge cases of custom event handlers, or whatever. Should be voted higher at the very least.Pelota
A
2

A solution in the style you propose that covers ALL cases will be extremely difficult to implement. But if you're willing to accept that delegate types with ref and out parameters or return values won't be covered, you should be able to use a DynamicMethod.

At design time, create a class to hold the count, lets call it CallCounter.

In AssertRaisesEvent:

  • create an instance of your CallCounterclass, keeping it in a strongly typed variable

  • initialize the count to zero

  • construct a DynamicMethod in your counter class

    new DynamicMethod(string.Empty, typeof(void), parameter types extracted from the eventInfo, typeof(CallCounter))

  • get the DynamicMethod's MethodBuilder and use reflection.Emit to add the opcodes for incrementing the field

    • ldarg.0 (the this pointer)
    • ldc_I4_1 (a constant one)
    • ldarg.0 (the this pointer)
    • ldfld (read the current value of the count)
    • add
    • stfld (put the updated count back into the member variable)
  • call the two-parameter overload of CreateDelegate, first parameter is the event type taken from eventInfo, second parameter is your instance of CallCounter

  • pass the resulting delegate to eventInfo.AddEventHandler (you've got this) Now you're ready to execute the test case (you've got this).

  • finally read the count in the usual way.

The only step I'm not 100% sure how you'd do is getting the parameter types from the EventInfo. You use the EventHandlerType property and then? Well, there's an example on that page showing that you just grab the MethodInfo for the Invoke method of the delegate (I guess the name "Invoke" is guaranteed somewhere in the standard) and then GetParameters and then pull out all the ParameterType values, checking that there are no ref/out parameters along the way.

Accomplice answered 2/4, 2010 at 15:1 Comment(2)
I'm sure Marc Gravell will come up with a way to use expression trees to avoid the MethodBuilder, but it would be pretty much the same idea. And getting the right Expression<TDelegate>.Compile, when TDelegate isn't known at compile-time, is non-trivial.Accomplice
If this is really the only way to do it, I think I'll settle for the boilerplate...Ackley
D
1

How about this:

private void AssertRaisesEvent(Action action, object obj, string eventName)
    {
        EventInfo eventInfo = obj.GetType().GetEvent(eventName);
        int raisedCount = 0;
        EventHandler handler = new EventHandler((sender, eventArgs) => { ++raisedCount; });
        eventInfo.AddEventHandler(obj, handler );
        action.Invoke();
        eventInfo.RemoveEventHandler(obj, handler);

        Assert.AreEqual(1, raisedCount);
    }
Downhill answered 2/4, 2010 at 15:33 Comment(1)
This only works if the declared event is of type EventHandler, not, for example, PropertyChangedEvent or even a custom event type.Ackley
S
1

This is an old thread but I think it is still a relevant problem to which I have not found a simple solution. At work, I needed something to do event testing and since there was no satisfying existing solution I decided to implement a small library. Since I am quite happy with my solution I have decided to put it on GitHub. Maybe you find it helpful too:

https://github.com/f-tischler/EventTesting

Here is what it looks like in action:

using EventTesting;

// ARRANGE
var myObj = new MyClass();

var hook = EventHook.For(myObj)
    .HookOnly((o, h) => o.WidthChanged+= h);

// ACT
myObj.Width = 42;

// ASSERT
hook.Verify(Called.Once());
Sallie answered 28/9, 2019 at 18:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.