如何在模拟中验证事件是否已取消订阅



>最小起订量版本: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>();

现在,您获得了目标类的所有事件的调用列表,并且应该能够断言特殊事件是否被取消订阅。

相关内容

  • 没有找到相关文章

最新更新