6

起订量版本: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
    }
}

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

4

2 回答 2

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未调用就足够了。当然,您需要另一个测试来证明该事件在正常操作(或某些条件)期间被调用。

编辑:以上是使用起订量。但是……起订量是一种工具,并且与任何工具一样,它应该用于正确的工作。如果您确实需要测试是否调用了“-=”,那么您应该选择一个更好的工具——比如实现您自己的 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();
        }
    }
} 
于 2013-09-27T12:44:31.367 回答
1

有一种从目标类外部的事件获取调用列表的肮脏方法,尽管这应该仅用于测试或调试目的,因为它破坏了事件的目的。

这仅在事件未通过客户实现(添加/删除)实现时才有效,如果事件具有事件访问器,则 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>();

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

于 2013-09-26T09:18:42.797 回答