Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to verify an event has been unsubscribed from on a mock

Moq version: 3.1.416.3

We found a bug caused by an event not being unsubscribed. I am trying to write a unit test to verify that the event is unsubscribed from. Is it possible to verify this using Mock<T>.Verify(expression)?

My initial thought was:

mockSource.Verify(s => s.DataChanged -= It.IsAny<DataChangedHandler>());

But apparently

An expression tree may not contain an assignment operator

Then I tried

mockSource.VerifySet(s => s.DataChanged -= It.IsAny<DataChangedHandler>());

But that gives me

System.ArgumentException: Expression is not a property setter invocation.

How can I verify that the unsubscribe has taken place?

How the event is used

public class Foo
{
    private ISource _source;

    public Foo(ISource source)
    {
        _source = source;
    }

    public void DoCalculation()
    {
        _source.DataChanged += ProcessData;

        var done = false;

        while(!done)
        {
            if(/*something is wrong*/)
            {
                Abort();
                return;
            }
            //all the things that happen
            if(/*condition is met*/)
            {
                done = true;
            }
        }

        _source.DataChanged -= ProcessData;
    }

    public void Abort()
    {
        _source.DataChanged -= ProcessData; //this line was added to fix the bug
         //other cleanup
    }

    private void ProcessData(ISource)
    {
        //process the data
    }
}

Ignore the convoluted nature of the code, we're dealing with signals from external hardware. This actually makes sense for the algorithm.

like image 742
Matt Ellen Avatar asked Oct 02 '22 20:10

Matt Ellen


2 Answers

Assuming that ProcessData does something meaningful, i.e. either changes the state of the SUT (system under test) in a meaningful/observable way, or acts on the event args, just raising the event on the mock and inspecting if the change happen should be enough.

Example with changing state:

....
public void ProcessData(ISource source)
{
   source.Counter ++;
}

...
[Test]
.....

sut.DoWork();
var countBeforeEvent = source.Count;
mockSource.Raise(s => s.DataChanged += null, new DataChangedEventArgs(fooValue));
Assert.AreEqual(countBeforeEvent, source.Count);

Of course, the above should be adapted to whatever implementation you have in ProcessData.

When doing unit testing, you should not be concerned about the implementation details (i.e. if some event is unsubscribed) and should not test that, but about behavior - i.e. if you raise an event, does something happen. In your case it's enough to verify that ProcessData is not called. Of course you need another test that demonstrates that the event is called during normal operation (or certain conditions).

EDIT: the above is with using Moq. But ... Moq is a tool, and like any tool it should be used for the right job. If you really need to test that the "-=" is invoked, then you should pick a better tool - like implementing your own stub of the ISource. The following example has pretty useless class under test which just subscribes and then unsubscribes from the event, just to demonstrate how you can test.

using System;
using NUnit.Framework;
using SharpTestsEx;

namespace StackOverflowExample.Moq
{
    public interface ISource
    {
        event Action<ISource> DataChanged;
        int InvokationCount { get; set; }
    }

    public class ClassToTest
    {
        public void DoWork(ISource source)
        {
            source.DataChanged += this.EventHanler;
        }

        private void EventHanler(ISource source)
        {
            source.InvokationCount++;
            source.DataChanged -= this.EventHanler;
        }
    }

    [TestFixture]
    public class EventUnsubscribeTests
    {
        private class TestEventSource :ISource
        {
            public event Action<ISource> DataChanged;
            public int InvokationCount { get; set; }

            public void InvokeEvent()
            {
                if (DataChanged != null)
                {
                    DataChanged(this);
                }
            }

            public bool IsEventDetached()
            {
                return DataChanged == null;
            }
        }

        [Test]
        public void DoWork_should_detach_from_event_after_first_invocation()
        {
            //arrange
            var testSource = new TestEventSource();
            var sut = new ClassToTest();
            sut.DoWork(testSource);

            //act
            testSource.InvokeEvent();
            testSource.InvokeEvent(); //call two times :)

            //assert
            testSource.InvokationCount.Should("have hooked the event").Be(1);
            testSource.IsEventDetached().Should("have unhooked the event").Be.True();
        }
    }
} 
like image 194
Sunny Milenov Avatar answered Oct 13 '22 09:10

Sunny Milenov


there is a dirty way to get the invocationList from a event outside the target class, although this should be used only for testing or debugging purposes as it breaks the purpose of events.

This only works if the event is not implemented with a customer implementation (add/remove), If the event has event Accessors the eventInfo2FieldInfo will return null.

 Func<EventInfo, FieldInfo> eventInfo2FieldInfo = eventInfo => mockSource.GetType().GetField(eventInfo.Name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField);
 IEnumerable<MulticastDelegate> invocationLists = mockSource.GetType().GetEvents().Select(selector => eventInfo2FieldInfo(selector).GetValue(mockSource)).OfType<MulticastDelegate>();

now you got the invocation Lists for all events of the target class, and should be able to assert if a special event got unsubscribed.

like image 44
quadroid Avatar answered Oct 13 '22 11:10

quadroid