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?
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.
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();
}
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With