C# 如何验证已从模拟中取消订阅事件
最小起订量版本:3.1.416.3 我们发现一个由未取消订阅的事件引起的错误。我正在尝试编写一个单元测试,以验证该事件是否已取消订阅。是否可以使用C# 如何验证已从模拟中取消订阅事件,c#,unit-testing,event-handling,.net-3.5,moq,C#,Unit Testing,Event Handling,.net 3.5,Moq,最小起订量版本:3.1.416.3 我们发现一个由未取消订阅的事件引起的错误。我正在尝试编写一个单元测试,以验证该事件是否已取消订阅。是否可以使用Mock.verify(expression)来验证这一点 我最初的想法是: mockSource.Verify(s => s.DataChanged -= It.IsAny<DataChangedHandler>()); 忽略代码的复杂性质,我们正在处理来自外部硬件的信号。这实际上对算法是有意义的。从目标类之外的事件获取调用列表有
Mock.verify(expression)
来验证这一点
我最初的想法是:
mockSource.Verify(s => s.DataChanged -= It.IsAny<DataChangedHandler>());
忽略代码的复杂性质,我们正在处理来自外部硬件的信号。这实际上对算法是有意义的。从目标类之外的事件获取调用列表有一种肮脏的方法,尽管这只应用于测试或调试目的,因为它破坏了事件的目的 这仅在事件未通过客户实施(添加/删除)实施时有效, 如果事件具有事件访问器,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>();
Func eventInfo2FieldInfo=eventInfo=>mockSource.GetType().GetField(eventInfo.Name,BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.GetField);
IEnumerable invocationLists=mockSource.GetType().GetEvents().Select(选择器=>eventInfo2FieldInfo(选择器).GetValue(mockSource)).OfType();
现在,您已经获得了目标类所有事件的调用列表,并且应该能够断言某个特殊事件是否被取消订阅。假设
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
。当然,您需要另一个测试来证明在正常操作(或某些条件)期间调用该事件
编辑:以上是使用最小起订量。但是Moq是一种工具,和任何工具一样,它应该用于正确的工作。如果您确实需要测试是否调用了“-=”,那么您应该选择一个更好的工具,比如实现您自己的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();
}
}
}
使用系统;
使用NUnit.Framework;
使用性;
命名空间StackOverflowExample.Moq
{
公共接口源
{
事件动作数据改变;
int InvokationCount{get;set;}
}
公共类类测试
{
公共空道具(ISource源)
{
source.DataChanged+=this.EventHanler;
}
私有void EventHanler(ISource源)
{
InvokationCount++;
source.DataChanged-=this.EventHanler;
}
}
[测试夹具]
公共类事件取消订阅测试
{
私有类TestEventSource:ISource
{
公共事件行动数据变更;
public int InvokationCount{get;set;}
public void InvokeEvent()
{
如果(DataChanged!=null)
{
数据更改(本);
}
}
公共文件已披露()
{
返回DataChanged==null;
}
}
[测试]
在第一次调用()之后,公共void DoWork应该从事件中分离
{
//安排
var testSource=new TestEventSource();
var sut=new ClassToTest();
sut.DoWork(测试源);
//表演
testSource.InvokeEvent();
testSource.InvokeEvent();//调用两次:)
//断言
testSource.InvokationCount.Should(“已钩住事件”).Be(1);
testSource.IsEventDetached().Should(“已解除事件挂钩”).Be.True();
}
}
}
是否有任何措施阻止向事件添加失败案例?您可以使用类似Assert.Fail()委托的方式订阅事件,如果事件未取消订阅,则应触发该委托。我不认为您可以从外部解析当前处理程序的事件:@AdamKewley这将有助于确保所有处理程序都已删除,但我需要确保仅删除一个处理程序。尽管如此,我可以修改代码以使事件平坦化。那可能真的有用。你能再解释一下吗?谁/什么人参加了活动?这是你的SUT吗?是SUT负责拆卸吗?你能展示一些使用场景吗?@SunnyMilenov我添加了一个示例看看这个有趣的例子。我会尝试一下。遗憾的是,eventInfo2FieldInfo
只会返回null。也许你错过了什么。我使用完全相同的代码来确定我的活动是否被正确取消订阅。它给了我预期的结果。我复制了你的代码,并将其调整为与Mock
一起工作,我得到了空引用异常。我也用一个非模拟对象进行了尝试,得到了同样的结果。我很好奇,明天我将发布一个完整的示例SUT代表什么?@MattEllen:正在测试的系统-即您正在测试的类。
....
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);
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();
}
}
}