如何TDD异步事件?

时间:2010-03-10 22:14:06

标签: c# events asynchronous tdd

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

如果你有时间进一步阅读,这是一个场景:

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

想象一下,有问题的班级是电梯,我想测试给出电梯所在楼层号的方法。以下是我的虚拟测试现在的样子:

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

4 个答案:

答案 0 :(得分:5)

我认为你已经走上了正确的道路。测试需要等到事件发生或你判断它已经花了太长时间才到达并且应该放弃等待。

为此,您可以在测试中使用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()调用应该替换为您的单元测试工具用于显式地使测试失败的任何机制 - 或者您可以抛出异常。)

答案 1 :(得分:2)

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

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

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

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

答案 2 :(得分: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()调用的返回值,以检查是否调用了事件处理程序。

答案 3 :(得分: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);
}

我不喜欢这种解决方案,因为如果电梯在500ms内到达,你就会等待另一个4500ms。如果您有这样的许多测试,并且您希望测试快速,我将完全避免这种情况。但是,这种测试也可以作为性能/健全性检查。

想要确保电梯在2秒内到达?更改超时。

相关问题