【问题标题】:How to TDD Asynchronous Events?如何TDD异步事件?
【发布时间】:2010-03-10 22:14:06
【问题描述】:

基本问题是如何创建需要调用方法的单元测试,等待被测类上发生事件,然后调用另一种方法(我们实际要测试的方法)?

如果您有时间进一步阅读,以下是场景:

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

假设有问题的类是电梯,我想测试给出电梯楼层号的方法。这是我的虚拟测试现在的样子:

[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);
        }
    }

【问题讨论】:

  • 在目标操作系统上调用是否会被阻塞?
  • 没有。一旦你调用 .GoToFloor(floor) 它就会返回,但是电梯将状态更改为“正在移动”,需要一段时间才能到达该楼层,然后它会引发一个事件(可能在不同的线程中)
  • 嗨,这应该可以解决我测试异步操作的问题。我使用上面的代码作为起点,但是 Monitor.Pulse 不会导致 Wait 重新获取锁,它会超时导致 Assert.Fail("Event did notarrive") 触发。任何想法为什么这可能是......!?
  • 以上编辑中的解决方案在 Monitor.Pulse 周围缺少 lock

标签: c# events asynchronous tdd


【解决方案1】:

我认为你已经走对了。测试需要等到事件发生或者你判断它到达的时间太长,应该放弃等待。

为此,您可以在测试中使用 Monitor.Wait 超时,并在事件到达时使用 Monitor.Pulse 发出信号。

[TestMethod] public void TestGetCurrentFloor() { var elevator = new Elevator(Elevator.Environment.Offline); elevator.ElevatorArrivedOnFloor += TestElevatorArrived; lock (this) { elevator.GoToFloor(5); // NOTE: this must hand off to a second thread, and the ElevatorArrivedOnFloor must be raised by this other thread otherwise the Monitor will be pulse before we've started waiting for it if (!Monitor.Wait(this, TIMEOUT)) Assert.Fail("Event did not arrive in time."); } int floor = elevator.GetCurrentFloor(); Assert.AreEqual(floor, 5); } private void TestElevatorArrived(int floor) { lock (this) { Monitor.Pulse(this); } }

(此处的Assert.Fail() 调用应替换为您的单元测试工具用于显式失败测试的任何机制——否则您可能会抛出异常。)

【讨论】:

  • 我就是这么干的,只不过我是用 WaitHandle 和 lambda 代替分配给事件的方法,但思路完全一样。
  • 嗨,这应该可以解决我测试异步操作的问题。我使用上面的代码作为起点,但是 Monitor.Pulse 不会导致 Wait 重新获取锁,它会超时导致 Assert.Fail("Event did notarrive") 触发。任何想法为什么这可能是......!?
  • @Kildareflare:脉冲导致 Monitor.Wait() 结束等待。请注意,当 Monitor.Wait() 结束等待(由于脉冲或因为它已超时)时,它必须重新获取锁,因此 Monitor.Wait() 可能已经计时但您稍后会看到该消息当它能够取回锁时。我的猜测是您将超时设置得太小:它以毫秒为单位,您是否指定了秒数?
  • @保罗。超时以毫秒 (5000) 为单位。每层乘电梯 250 毫秒。所发生的情况是电梯在一秒钟内到达 5 楼并触发电梯到达事件。然后测试会一直放在那里,直到 Monitor.Wait 超时导致测试失败。顺便说一句,我可以使用下面的 ManualResetEvent 获得备用版本。
  • @Kildareflare:我想我知道出了什么问题。您的 GoToFloor() 实现:我敢打赌您正在使用它来提高 ElevatorArrivedOnFloorEvent?如果是这样,它将使用随后将等待事件的同一线程。跟随线程:它将 (1) 获得锁,(2) 调用 GoToFloor() 方法,(3) 引发转换为调用 TestElevatorArrived 的事件,(4) 脉冲监视器,(5) 从TestElevatorArrived() 方法,(6) 从 GoToFloor() 调用返回,(7) 开始等待它已经错过的脉冲。
【解决方案2】:

也许这只是一个糟糕的例子,但您的电梯听起来更像是一个状态机,而不是异步处理的东西。

所以你的第一组测试可以测试 GoToFloor() 将状态设置为移动并且它移动的方向是正确的。

然后下一组测试将在 TestElevatorArrived() 上进行,并将测试如果您的状态正在向某个楼层移动,那么实际的移动(即在异步等待之后调用的函数,或处理程序触发“已移动”事件的硬件)会将状态设置为预期的楼层。

否则,您正在测试的很可能是您对硬件的模拟正确地模拟了时间和移动,这似乎不正确。

【讨论】:

  • 你说得对,我的电梯确实实现了状态模式,而且我已经在做这些测试了。在这种特定情况下,如果等待是最好的选择,我只是感到困惑。
  • 啊,对于这种特殊情况,我会说不。不想笼统地说不,因为可能(可能?)一些很好的案例可以用计时器/超时等进行测试。但是在这里,你“等待”的是你的硬件来做一些工作,这不是你想要进行单元测试的。
【解决方案3】:

这是我的类似方法。

    [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() 调用的返回值,以检查您的事件处理程序是否被调用。

【讨论】:

    【解决方案4】:

    我真的不喜欢上面 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 秒内到达?更改超时时间。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-12-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-06-11
      • 1970-01-01
      相关资源
      最近更新 更多