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

2

6

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.

Dollie answered 26/9, 2013 at 8:38 Comment(5)
Is there anything preventing adding a fail case to event? You could subscribe to the event with something like a Assert.Fail() delegate which, if the event didn't unsubscribe, should trigger. I don't think you can externally parse an events current handlers: #573147Imponderable
@AdamKewley that would be good to make sure all handlers are gone, but I need to make sure that just one is removed. Although, I could alter the code to flatten the event. That might actually work.Dollie
Can you explain a little bit more? Who/what attaches the event? Is this your SUT? It the SUT responsible for the detaching? Can you show some usage scenario?Lipchitz
@SunnyMilenov I've added an exampleDollie
See this #1430087Garzon
L
3

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();
        }
    }
} 
Lipchitz answered 27/9, 2013 at 12:44 Comment(2)
What does SUT stand for?Dollie
@MattEllen: System under test - i.e. the class you are testing.Lipchitz
P
1

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.

Precept answered 26/9, 2013 at 9:18 Comment(5)
Interesting. I will give this a try.Dollie
Sadly eventInfo2FieldInfo only ever returns null.Dollie
maybe you missed something i use the exact same code to determine if my events are unsubscribed correctly. It gives me the expected results.Precept
I have copied your code and adjusted it to work with Mock<T> and I get null reference exceptions. I tried it with a non-mock object, too and I get the same result.Dollie
I'm curious, i will post a complete example tomorrowPrecept

© 2022 - 2024 — McMap. All rights reserved.