>最小起订量版本: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:表达式不是属性设置器调用。
如何验证取消订阅是否已发生?
如何使用事件
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
}
}
忽略代码的复杂性质,我们正在处理来自外部硬件的信号。这实际上对算法有意义。
假设ProcessData
做了一些有意义的事情,即要么以有意义/可观察的方式改变SUT(被测系统(的状态,要么对事件参数采取行动,只需在模拟上引发事件并检查更改是否发生就足够了。
状态变化的示例:
....
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
就足够了。当然,您需要另一个测试来证明事件是在正常操作(或某些条件(期间调用的。
编辑:以上是使用最小起订量。但。。。最小起订量是一种工具,像任何工具一样,它应该用于正确的工作。如果你真的需要测试是否调用了"-=",那么你应该选择一个更好的工具 - 比如实现你自己的ISource存根。下面的示例有非常无用的测试类,它只是订阅然后取消订阅事件,只是为了演示如何测试。
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();
}
}
}
从目标类之外的事件获取调用列表的肮脏方法,尽管这应该仅用于测试或调试目的,因为它会破坏事件的目的。
这仅在事件未与客户实现(添加/删除(时有效,如果事件具有事件访问器,则事件信息2字段信息将返回 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>();
现在,您获得了目标类的所有事件的调用列表,并且应该能够断言特殊事件是否被取消订阅。