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

Posted

技术标签:

【中文标题】如何在模拟中验证事件已被取消订阅【英文标题】:How to verify an event has been unsubscribed from on a mock 【发布时间】:2013-10-02 03:06:01 【问题描述】:

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

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

【问题讨论】:

是否有任何事情阻止向事件添加失败案例?您可以使用诸如 Assert.Fail() 委托之类的东西订阅该事件,如果该事件没有取消订阅,则应该触发该委托。我认为您不能从外部解析事件当前处理程序:***.com/questions/572647/… @AdamKewley 最好确保 所有 处理程序都消失了,但我需要确保只删除一个。虽然,我可以更改代码以展平事件。这可能真的有效。 你能再解释一下吗?谁/什么与事件有关?这是你的 SUT 吗?是 SUT 负责分离吗?你能展示一些使用场景吗? @SunnyMilenov 我添加了一个例子 看到这个***.com/questions/1429587/… 【参考方案1】:

假设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 ***Example.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();
        
    
 

【讨论】:

SUT 代表什么? @MattEllen:正在测试的系统 - 即您正在测试的类。【参考方案2】:

有一种从目标类外部的事件中获取 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>();

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

【讨论】:

有趣。我会试试看。 可悲的是 eventInfo2FieldInfo 只返回 null。 也许你错过了一些我使用完全相同的代码来确定我的事件是否被正确取消订阅。它给了我预期的结果。 我已经复制了您的代码并对其进行了调整以与Mock&lt;T&gt; 一起使用,但我得到了空引用异常。我也尝试了一个非模拟对象,我得到了相同的结果。 我很好奇,我明天会发布一个完整的例子

以上是关于如何在模拟中验证事件已被取消订阅的主要内容,如果未能解决你的问题,请参考以下文章

Rxswift 在事件发生时取消观察者并重新订阅

如何取消订阅使用 lambda 表达式的事件?

如何取消订阅使用 lambda 表达式的事件?

如何正确取消订阅事件

如何模拟订阅的过期状态

如何解决”Microsoft Office 无法验证此应用程序的许可证.修复尝试失败或者已被取消.“?