c# - 测试事件驱动的行为

标签 c# unit-testing events test-framework

在 GUI 应用程序的上下文中,似乎有很多关于这类事情的建议。我认为我的特殊情况足以让我提出要求。总结一下我的问题,你们如何测试事件?

现在进行冗长的解释。我已经使用服务点硬件一段时间了。这意味着我必须编写一些 OPOS 服务对象。经过几年的努力,我设法编写了我的第一个 COM 可见 C# 服务对象并将其投入生产。我尽力对整个项目进行单元测试,但发现它相当困难,但能够为大多数服务对象的接口(interface)实现提供良好的单元测试。最难的部分是事件的部分。诚然,这种情况是我遇到的最大和最常见的情况,但我在我的其他应用程序中遇到过类似的情况,在这些情况下测试事件似乎很尴尬。因此,为了设置场景,公共(public)控制对象 (CO) 最多有 5 个事件,一个人也可以订阅这些事件。当 CO 调用 Open OPOS 方法时,会找到服务对象 (SO) 并创建它的一个实例,然后调用它的 OpenService 方法。 SO的第三个参数是对CO的引用。现在我不用定义整个CO了,只需要定义那5个事件的回调方法即可。 msr 定义的一个例子是这个

[ComImport, InterfaceType(ComInterfaceType.InterfaceIsDual), Guid("CCB91121-B81E-11D2-AB74-0040054C3719")]
internal interface MsrControlObject
{
    [DispId(1)]
    void SOData(int status);

    [DispId(2)]
    void SODirectIO(int eventNumber, ref int pData, ref string pString);

    [DispId(3)]
    void SOError(int resultCode, int resultCodeExtended, int errorLocus, ref int pErrorResponse);

    [DispId(4)]
    void SOOutputCompleteDummy(int outputId);

    [DispId(5)]
    void SOStatusUpdate(int data);
} 

我的 OpenService 方法会有这行代码

public class FakeMsrServiceObject : IUposBase
{
    MsrControlObject _controlObject;
    public int OpenService(string deviceClass, string deviceName, object dispatchObject)
    {
        _controlObject = (MsrControlObject)dispatchObject;
    }
    //example of how to fire an event 
    private void FireDataEvent(int status)
    {
        _controlObject.SODataEvent(status);
    }
}

所以我心想,为了更好的测试,让我们制作一个 ControlObjectDispatcher。它将允许我对事件进行排队,然后在条件正确时将它们发送给 CO。这就是我所在的地方。现在我知道如何试驾它的实现了。但就是感觉不对。让我们以 DataEvent 为例。必须满足 2 个条件才能触发 DataEvent。首先, bool 属性 DataEventEnabled 必须为真,另一个 bool 属性 FreezeEvents 必须为假。此外,所有事件都是严格的 FIFO。所以.. 队列是完美的。因为我在知道实现是什么之前就已经写了这篇文章。但是为它编写一个测试来向项目的新人灌输信心是很困难的。考虑这个伪代码

[Test]
public void WhenMultipleEventsAreQueuedTheyAreFiredSequentiallyWhenConditionsAreCorrect()
{
    _dispatcher.EnqueueDataEvent(new DataEvent(42));
    _dispatcher.EnqueueStatusUpdateEvent(new StatusUpdateEvent(1));

    Sleep(1000);
    _spy.AssertNoEventsHaveFired();
    _spy.AssertEventsCount(2);

    _serviceObject.SetNumericProperty(PIDX_DataEventEnabled, 1);

    _spy.AssertDataEventFired();
    _spy.AssertStatusUpdateEventFired();

    _serviceObject.GetnumericProperty(PIDX_DataEventEnabled).Should().BeEqualTo(0, "because firing a DataEvent sets DataEventEnabled to false");
}

每个读到这篇文章的人都可能想知道(不知道实现)我怎么知道 1 分钟后这个事件触发了?我怎么知道那个疯狂的 Robert Snyder 人没有使用 for 循环并忘记在迭代全部结束后退出 FireDataEvent 例程?你真的不知道。当然,您可以测试一分钟……但这违背了单元测试的目的。

所以再总结一下……一个人怎么写一个事件的测试呢?事件可以在任何时候触发……有时它们可​​能需要比预期更长的时间来处理和触发。我在我的第一次实现的集成测试中看到,如果我在断言事件被调用之前没有睡 50 毫秒,那么测试将失败并出现类似的情况。 预期数据事件已触发,但从未触发 他们是否为事件构建了任何测试框架?他们是否有涵盖此内容的任何通用编码实践?

最佳答案

有点不清楚您是要为您的事件进行单元测试还是集成测试,因为您谈到了两者。但是,鉴于问题的标签,我假设您的主要兴趣是从单元测试的角度来看。从单元测试的角度来看,如果您正在测试一个事件或一个普通方法,并没有太大区别。该单元的目标是隔离测试各个功能 block ,因此同时拥有 sleep在集成测试中可能是有意义的(尽管我仍然会尽量避免它并尽可能使用其他类型的同步),在单元测试中我会把它作为一个标志,表明被测试的功能没有被隔离足够了。

因此,对我来说,事件驱动测试分为两部分。首先,您要测试在满足适当条件时是否会触发您的类触发的任何事件。其次,您想要测试事件的任何处理程序是否执行预期的操作。

测试处理程序的行为是否符合预期应该与您认为正确的任何其他测试类似。您将处理程序设置为预期状态,设置您的期望,就像您是事件生成器一样调用它,然后验证任何相关行为。

测试是否触发事件本质上是相同的,设置您希望触发事件的状态,然后验证是否触发了适当填充的事件以及是否发生了任何其他状态更改。 所以,看看你的伪代码,我会说你至少有两个测试:

// Setup (used for both tests)
// Test may be missing setup for dispatcher state == data event disabled.
    _dispatcher.EnqueueDataEvent(new DataEvent(42));
    _dispatcher.EnqueueStatusUpdateEvent(new StatusUpdateEvent(1));

//    Sleep(1000);    // This shouldn’t be in a unit test.

// Test 1 – VerifyThatDataAndStatusEventsDoNotFireWhenDataEventDisabled
    _spy.AssertNoEventsHaveFired();
    _spy.AssertEventsCount(2);

// Test 2 – VerifyThatEnablingDataEventFiresPendingDataEvents
    _serviceObject.SetNumericProperty(PIDX_DataEventEnabled, 1);

    _spy.AssertDataEventFired();
    _spy.AssertStatusUpdateEventFired();

// Test 3? – This could be part of Test 2, or it could be a different test to VerifyThatDataEventsAreDisabledOnceADataEventHasBeenTriggered.
    _serviceObject.GetnumericProperty(PIDX_DataEventEnabled).Should().BeEqualTo(0, "because firing a DataEvent sets DataEventEnabled to false");

查看测试代码,没有任何迹象表明您已经使用任何类型的 for 循环实现了实际的 serviceObject,或者一分钟的测试会对 serviceObject 的行为产生任何影响。如果你不从事件编程的角度考虑它,你真的会考虑调用SetNumericProperty吗?会导致您使用“for 循环并忘记退出 FireDataEvent”迭代全部完成后的例行程序?' 似乎如果是这样的话,那么要么 SetNumericProperty不会返回,或者你有一个非线性的实现,你可能在错误的地方测试了错误的东西。如果没有看到您的事件生成代码,很难就此提出建议……

Events can fire whenever… and they can sometimes take longer to process and fire then expected

虽然这在您的应用程序运行时可能是正确的,但在您进行单元测试时不应该是正确的。您的单元测试应该针对定义的条件触发事件,并测试这些事件是否已被正确触发和生成。要实现这一点,您需要以单元隔离为目标,并且您可能不得不接受一些瘦元素需要进行集成测试,而不是单元测试才能实现这种隔离。因此,如果您正在处理从您的应用程序外部触发的事件,您最终可能会遇到这样的事情:

public interface IInternalEventProcessor {
    void SOData(int status);
    void SODirectIO(int eventNumber, int pData, string pString);
};

public class ExternalEventProcessor {
    IInternalEventProcessor _internalProcessor;

    public ExternalEventProcessor(IInternalEventProcessor internalProcessor, /*Ideally pass in interface to external system to allow unit testing*/) {
        _internalProcessor = internalProcessor;
        // Register event subscriptions with external system
    }

    public void SOData(int status) {
        _internalProcessor.SOData(status);
    }

    void SODirectIO(int eventNumber, ref int pData, ref string pString) {
        _internalProcessor.SODirectIO(eventNumber, pData, pString);
    }
}

ExternalEventProcessor 的目的是解耦对外部系统的依赖,以便在 InternalEventProcessor 中对事件处理进行单元测试更容易。理想情况下,您仍然可以对 ExternalEventProcessor 进行单元测试。正确注册事件并通过提供外部系统和内部实现的模拟来传递,但是如果你不能那么因为类被削减到最低限度,只对这个类进行集成测试可能是一个现实的选择。

关于c# - 测试事件驱动的行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29545777/

相关文章:

c# - 如何判断一个类是特定于 WSS 还是 MOSS?

c# - 使用约束随机值自定义 AutoFixture 属性生成

c# - 如果我的代码是为 x86 或任何 CPU 编译的,我的代码如何在运行时检测到

c# - 控制反转/依赖注入(inject)和转换运算符

c# - 在 C# 中使用十六进制文字将整数设置为负值

c# - 设置 myButton.Enabled = true 会触发单选按钮的 CheckedChanged 处理程序

c# - 静态 EventHandler 事件的发送者

C# 事件传递/冒泡

c# - Office VSTO 加载项可能的权限问题 - HRESULT 0x80004004 (E_ABORT)

unit-testing - 测试 Yeoman 生成的 Angular 工厂/服务