如何使用TDD测试异步事件?

12

如何创建一个单元测试,需要调用一个方法,等待被测试类上的某个事件发生,然后再调用另一个方法(我们真正想要测试的方法)是一个基本问题。

以下是一个情景描述:

我正在开发一个必须控制硬件的应用程序。为了避免依赖于硬件可用性,在创建对象时我指定了运行在测试模式下。这样做时,被测试的类会创建适当的驱动程序层次结构(在这种情况下是一个薄的模拟硬件驱动程序层)。

假设被测试的类是电梯,我想要测试的方法是返回电梯所在楼层数的方法。现在,我的虚构测试案例看起来像这样:

[TestMethod]
public void TestGetCurrentFloor()
{
    var elevator = new Elevator(Elevator.Environment.Offline);
    elevator.ElevatorArrivedOnFloor += TestElevatorArrived;

    elevator.GoToFloor(5);

    //Here's where I'm getting lost... I could block
    //until TestElevatorArrived gives me a signal, but
    //I'm not sure it's the best way

    int floor = elevator.GetCurrentFloor();

    Assert.AreEqual(floor, 5);
}

编辑:

感谢所有的回答,以下是我最终采用的实现方式:

    [TestMethod]
    public void TestGetCurrentFloor()
    {
        var elevator = new Elevator(Elevator.Environment.Offline);
        elevator.ElevatorArrivedOnFloor += (s, e) => { Monitor.Pulse(this); };

        lock (this)
        {
            elevator.GoToFloor(5);

            if (!Monitor.Wait(this, Timeout))
                Assert.Fail("Elevator did not reach destination in time");

            int floor = elevator.GetCurrentFloor();

            Assert.AreEqual(floor, 5);
        }
    }

在目标操作系统上,这些调用会是阻塞的吗? - Kaleb Pederson
不会。只要调用.GoToFloor(floor),它就会返回,但电梯会更改状态为“移动”,需要一段时间才能到达该楼层,然后它将引发一个事件(可能在不同的线程中)。 - Padu Merloti
嗨,这应该解决我测试异步操作时遇到的问题。我使用了您上面的代码作为起点,但是Monitor.Pulse没有导致Wait重新获取锁定,它超时并触发Assert.Fail(“事件未到达”)。有任何想法为什么会这样吗...!? - Kildareflare
上面的编辑中的解决方案在 Monitor.Pulse 周围缺少一个 lock - Paul Ruane
4个回答

5

我认为你已经朝着正确的方向努力了。测试需要等待事件发生或者判断事件等待时间过长而放弃等待。

为此,你可以在测试中使用带有超时的 Monitor.Wait,并在事件到达时使用 Monitor.Pulse 进行信号通知。


[TestMethod]
public void TestGetCurrentFloor()
{
    var elevator = new Elevator(Elevator.Environment.Offline);
    elevator.ElevatorArrivedOnFloor += TestElevatorArrived;
lock (this) { elevator.GoToFloor(5); // 注意:这必须交给第二个线程处理,ElevatorArrivedOnFloor 必须由该其他线程引发,否则 Monitor 将在我们开始等待之前发出脉冲
if (!Monitor.Wait(this, TIMEOUT)) Assert.Fail("未及时收到事件。"); }
int floor = elevator.GetCurrentFloor();
Assert.AreEqual(floor, 5); } private void TestElevatorArrived(int floor) { lock (this) { Monitor.Pulse(this); } }

(这里的 Assert.Fail() 调用应该替换为你的单元测试工具用于显式失败测试的机制,或者你可以抛出异常。)


这就是我的做法,只不过我使用了WaitHandle和lambda而不是分配给事件的方法,但是思路完全相同。 - Padu Merloti
嗨,这应该解决我测试异步操作时遇到的问题。我使用了您上面的代码作为起点,但是Monitor.Pulse没有导致Wait重新获取锁定,它超时并触发Assert.Fail(“事件未到达”)。有任何想法为什么会这样吗...!? - Kildareflare
@Kildareflare:脉冲会导致 Monitor.Wait() 结束等待。请注意,当 Monitor.Wait() 由于脉冲或超时而结束等待时,它必须重新获取锁定,因此可能是 Monitor.Wait() 已经超时,但是当它能够重新获取锁定后,您看到了消息。我猜想你将超时设置得太小了:它是以毫秒为单位的,你是否指定了秒? - Paul Ruane
@Paul。超时时间为毫秒(5000)。电梯每层需要250毫秒。当电梯在一秒钟内到达第五层并触发电梯到达事件时,会发生什么情况。然后测试会一直等待,直到Monitor.Wait超时导致测试失败。顺便说一句,我可以使用下面的ManualResetEvent获取备用版本来工作。 - Kildareflare
@Kildareflare:我想我知道出了什么问题。你的GoToFloor()实现:我敢打赌你正在使用它来触发ElevatorArrivedOnFloorEvent?如果是这样,它将使用随后将等待事件的相同线程。跟随线程:它将(1)获取锁定,(2)调用GoToFloor()方法,(3)引发事件,这将转换为对TestElevatorArrived的调用,(4)脉冲监视器,(5)从TestElevatorArrived()方法返回,(6)从GoToFloor()调用返回,(7)开始等待已经错过的脉冲。 - Paul Ruane
@Kildareflare:为了使这段代码正常工作,您需要让电梯移动并异步地触发事件,即使用单独的线程。我已经在代码中添加了注释以强调这一点。 - Paul Ruane

2
这是我的相似方法。
    [TestMethod]
    public void TestGetCurrentFloor()
    {
        var completedSync = new ManualResetEvent(false);
        var elevator = new Elevator(Elevator.Environment.Offline);

        elevator.ElevatorArrivedOnFloor += delegate(object sender, EventArgs e)
        {
            completedSync.Set();
        };

        elevator.GoToFloor(5);

        completedSync.WaitOne(SOME_TIMEOUT_VALUE);

        int floor = elevator.GetCurrentFloor();

        Assert.AreEqual(floor, 5);
    } 

您可以通过测试WaitOne()调用的返回值来检查事件处理程序是否被调用。

2
也许这只是一个糟糕的例子,但你的电梯听起来更像是一个状态机,而不仅仅是异步处理的东西。
因此,你的第一组测试可以测试GoToFloor()是否会将状态设置为移动,并且移动方向是否正确。
然后下一组测试将在TestElevatorArrived()上进行,并测试如果您的状态朝着某个楼层移动,那么实际移动(即在异步等待之后调用的函数或硬件触发“移动”事件的处理程序)将设置状态为预期楼层。
否则,您正在测试的很可能是您对硬件的模拟正确地模拟了时间和移动,这似乎是不正确的。

1
你说得对,我的电梯确实实现了状态模式,我已经在进行这种测试了。我只是有些困惑,在这种特定情况下等待是否是最好的选择。 - Padu Merloti
1
针对这个特定情况,我会说不需要。我不想一概而论地说不需要,因为可能(很可能?)有一些使用计时器/超时等进行测试的好案例。但是在这种情况下,你所“等待”的是硬件执行一些工作,这并不是你要进行单元测试的内容。 - Tanzelax

0

我真的不喜欢使用Monitor.Pulse/Wait方法时可能出现的竞态条件。

一种不太好但有效的方式可能是以下内容:

[TestMethod]
public void TestGetCurrentFloor()
{
    // NUnit has something very similar to this, I'm going from memory
    this.TestCounter.reset(); 

    var elevator = new Elevator(Elevator.Environment.Offline);
    elevator.ElevatorArrivedOnFloor += (s,e) => { Assert.That(e.floor).Is(5) }

    elevator.GoToFloor(5);

    // It should complete within 5 seconds..
    Thread.Sleep(1000 * 5);
    Assert.That(elevator.GetCurrentFloor()).Is(5);

    Assert.That(this.TestCounter.Count).Is(2);
}

我不喜欢这个解决方案,因为如果电梯在500毫秒内到达,你还需要等待4500毫秒。如果你有很多类似的测试,并且希望测试速度快,我会完全避免这种情况。然而,这种类型的测试也可以作为性能/合理性检查。
想要确保电梯在2秒内到达吗?改变超时时间。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接