测试事件驱动的行为

Testing Event driven behavior

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

现在进行冗长的解释。我已经使用服务点硬件一段时间了。这意味着我必须编写一些 OPOS 服务对象。经过几年的努力,我设法编写了我的第一个 COM 可见 C# 服务对象并将其投入生产。我尽力对整个项目进行单元测试,但发现它相当困难,但能够为大多数服务对象的接口实现提供良好的单元测试。最难的部分是事件的部分。诚然,这种情况是我遇到的最大和最常见的情况,但我在我的其他应用程序中遇到过类似的情况,在这些情况下测试事件似乎很尴尬。因此,为了设置场景,公共控制对象 (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。首先,布尔值 属性 DataEventEnabled 必须为真,另一个布尔值 属性 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 毫秒,那么测试将失败并出现类似的情况。 expected data event to have fired, but was never fired 他们是否为事件构建了任何测试框架?他们有任何常见的编码实践吗?

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

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

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

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

// 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 是否会导致您使用 'a for 循环并忘记在迭代后退出 FireDataEvent 例程都起来了吗?”似乎如果是这种情况,那么 SetNumericProperty 不会 return 或者你有一个非线性实现,你可能在错误的地方测试了错误的东西。虽然没有看到您的事件生成代码,但很难就此提出建议……

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 寄存器进行单元测试并通过,但是如果您不能那么因为 class 是减少仅为此进行集成测试的最低限度 class 可能是一个现实的选择。