如何验证某个事件已取消订阅模拟

时间:2013-09-26 08:38:35

标签: c# unit-testing event-handling .net-3.5 moq

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
    }
}

忽略代码的复杂性,我们处理来自外部硬件的信号。这实际上对算法有意义。

2 个答案:

答案 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。当然,您需要另一个测试来演示在正常操作(或某些条件)期间调用事件。

编辑:以上是使用Moq。但是...... 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();
        }
    }
} 

答案 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>();

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