Moq版本:3.1.416.3
我们发现了一个未被取消订阅的错误导致的错误。我正在尝试编写一个单元测试来验证该事件是否已取消订阅。是否可以使用Mock<T>.Verify(expression)
验证这一点?
我最初的想法是:
mockSource.Verify(s => s.DataChanged -= It.IsAny<DataChangedHandler>());
但显然
表达式树可能不包含赋值运算符
然后我试了
mockSource.VerifySet(s => s.DataChanged -= It.IsAny<DataChangedHandler>());
但那给了我
System.ArgumentException:Expression不是属性setter调用。
如何确认取消订阅已经发生?
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
}
}
忽略代码的复杂性,我们处理来自外部硬件的信号。这实际上对算法有意义。
答案 0 :(得分:3)
假设ProcessData
做了一些有意义的事情,即以有意义/可观察的方式改变SUT(被测系统)的状态,或者对事件args采取行动,只需在模拟上提升事件并检查如果发生变化就足够了。
更改状态的示例:
....
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);
当然,上述内容应该适用于ProcessData中的任何实现。
在进行单元测试时,您不应该关注实现细节(即,某些事件是否取消订阅)并且不应该测试,而是关于行为 - 即如果您举起事件,则会发生某些事情。在您的情况下,足以验证未调用ProcessData
。当然,您需要另一个测试来演示在正常操作(或某些条件)期间调用事件。
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();
}
}
}
答案 1 :(得分:1)
从目标类之外的事件中获取invocationList有一种肮脏的方法,因此它只能用于测试或调试目的,因为它会破坏事件的目的。
这仅适用于未通过客户实施(添加/删除)实施事件的情况, 如果事件具有事件访问器,则eventInfo2FieldInfo将返回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>();
现在您获得了目标类的所有事件的调用列表,并且应该能够断言特殊事件是否已取消订阅。