ShloEmi
ShloEmi

Reputation: 1984

Raising complex event using Moq in C#

The following code should be self explanetory: we have an adaptor, who consumes events from the transport (layer), which holds the MessageRegistrar (object type because we can't tell it's type, and basically because this is legacy code :-) ). The transport layer have a concrete which have an event. I want to test a case where the event is triggered, so..

After hours of trying to figure why it won't pass, I present the following challenge:

[TestFixture]
public class AdaptorTests
{
    public delegate void TracksEventHandler(object sender, List<int> trklst);

    public class MyEventHolder
    {
        public virtual event TracksEventHandler EventName;
    }

    public interface ITransport
    {
        object MessageRegistrar { get; }
    }

    public class MyTransport : ITransport
    {
        private readonly MyEventHolder m_eventHolder;

        public MyTransport(MyEventHolder eventHolder)
        {
            m_eventHolder = eventHolder;
        }

        public virtual object MessageRegistrar
        {
            get { return m_eventHolder; }
        }
    }

    public class MyAdaptor
    {
        private readonly ITransport m_transport;

        public MyAdaptor(ITransport transport)
        {
            EventTriggered = false;
            m_transport = transport;
        }

        public void Connect()
        {
            MyEventHolder eventHolder = m_transport.MessageRegistrar as MyEventHolder;
            if (eventHolder != null)
                eventHolder.EventName += EventHolderOnEventName;
        }

        private void EventHolderOnEventName(object sender, List<int> trklst)
        {
            EventTriggered = true;
        }

        public bool EventTriggered { get; private set; }
    }

    [Test]
    public void test1()
    {
        Mock<MyEventHolder> eventHolderMock = new Mock<MyEventHolder> {CallBase = true};

        Mock<MyTransport> transportMock = new Mock<MyTransport>(eventHolderMock.Object) {CallBase = true};

        MyAdaptor adaptor = new MyAdaptor(transportMock.Object);
        adaptor.Connect();

        MyEventHolder eventHolder = transportMock.Object.MessageRegistrar as MyEventHolder;
        Mock.Get(eventHolder).Raise(eh => eh.EventName += null, new List<int>());

        Assert.IsTrue(adaptor.EventTriggered);
    }

    [Test]
    public void test2()
    {
        Mock<MyEventHolder> eventHolderMock = new Mock<MyEventHolder> { CallBase = true };

        Mock<MyTransport> transportMock = new Mock<MyTransport>(eventHolderMock.Object) { CallBase = true };

        MyAdaptor adaptor = new MyAdaptor(transportMock.Object);
        adaptor.Connect();

        MyEventHolder eventHolder = transportMock.Object.MessageRegistrar as MyEventHolder;
        Mock.Get(eventHolder).Raise(eh => eh.EventName += null, null, new List<int>());

        Assert.IsTrue(adaptor.EventTriggered);
    }

}

My question is: why wont the test (at least one of them) pass?

EDIT @151217-0822 Addded 'adaptor.Connect()' to the original post (still won't fix the issue).

WORKAROUND

Credits to @Patrick Quirk: Thanks!!

For those encountering the same issue: after I understood what Patrick-Quirk detected, and trying couple of failed workarounds, I've ended up adding the following verified fix: 'eventHolder.FireEventNameForTestings(new List());':

    public class MyEventHolder
    {
        public virtual event TracksEventHandler EventName;

        public virtual void FireEventNameForTestings(List<int> trklst)
        {
            TracksEventHandler handler = EventName;
            if (handler != null)
                handler(this, trklst);
        }
    }

    [Test]
    public void test3()
    {
        Mock<MyEventHolder> eventHolderMock = new Mock<MyEventHolder> { CallBase = true };

        Mock<MyTransport> transportMock = new Mock<MyTransport>(eventHolderMock.Object) { CallBase = true };

        MyAdaptor adaptor = new MyAdaptor(transportMock.Object);
        adaptor.Connect();

        MyEventHolder eventHolder = transportMock.Object.MessageRegistrar as MyEventHolder;
        eventHolder.FireEventNameForTestings(new List<int>());

        Assert.IsTrue(adaptor.EventTriggered);
    }

HTH..

Upvotes: 4

Views: 326

Answers (1)

Patrick Quirk
Patrick Quirk

Reputation: 23747

It seems that CallBase and Raise() have an unexpected (to me) interaction.

When you are attaching an event handler to a virtual event on a mock, you go through this code in Moq:

if (invocation.Method.IsEventAttach())
{
    var delegateInstance = (Delegate)invocation.Arguments[0];
    // TODO: validate we can get the event?
    var eventInfo = this.GetEventFromName(invocation.Method.Name.Substring(4));

    if (ctx.Mock.CallBase && !eventInfo.DeclaringType.IsInterface)
    {
        invocation.InvokeBase();
    }
    else if (delegateInstance != null)
    {
        ctx.AddEventHandler(eventInfo, (Delegate)invocation.Arguments[0]);
    }

    return InterceptionAction.Stop;
}

You can see that if CallBase is true, then it will add your handler to the concrete object's event (via invocation.InvokeBase()). If CallBase is false, it will add it to an invocation list on the mock (via AddEventHandler). Now let's look at the code for Raise(), which gets the event object from the Expression and then calls DoRaise():

internal void DoRaise(EventInfo ev, EventArgs args)
{
    // ... parameter validation

    foreach (var del in this.Interceptor.InterceptionContext.GetInvocationList(ev).ToArray())
    {
        del.InvokePreserveStack(this.Object, args);
    }
}

See the call to GetInvocationList()? That retrieves the invocation list from the mock that I mentioned above. This code never invokes the actual event on the base object.

So, it seems there's no way to raise an event on a mocked object where CallBase is set to true.

The only workaround I see, if you require CallBase being true, is to add a method to your concrete MyEventHolder to trigger your event. Obviously what you posted is a simplified example so I can't give you more guidance than that, but hopefully I've shown you why what you have does not work.

Upvotes: 3

Related Questions