Как TDD асинхронные события?

Основной вопрос: каким образом я могу создать unit test, который должен вызвать метод, дождаться, когда событие произойдет в тестируемом классе, а затем вызвать другой метод (тот, который мы действительно хотим проверить)?

Здесь сценарий, если у вас есть время для дальнейшего чтения:

Я разрабатываю приложение, которое должно контролировать часть оборудования. Чтобы избежать зависимости от доступности оборудования, когда я создаю свой объект, я указываю, что мы работаем в тестовом режиме. Когда это произойдет, тестируемый класс создает соответствующую иерархию драйверов (в данном случае тонкий макет уровня драйверов оборудования).

Представьте себе, что класс, о котором идет речь, является Лифт, и я хочу проверить метод, который дает мне номер пола, который является лифтом. Вот как выглядит мой фиктивный тест прямо сейчас:

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

    elevator.GoToFloor(5);

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

    int floor = elevator.GetCurrentFloor();

    Assert.AreEqual(floor, 5);
}

Edit:

Спасибо за все ответы. Вот как я это сделал:

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

Ответ 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() должен быть заменен любым механизмом, используемым вашим инструментом тестирования единицы измерения, для явного отказа от проверки - или вы можете исключить исключение).

Ответ 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 секунд? Изменение таймаута.