Как TDD Асинхронные События? - PullRequest
12 голосов
/ 11 марта 2010

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

Вот сценарий, если у вас есть время, чтобы прочитать дальше:

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

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

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

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

Ответы [ 4 ]

5 голосов
/ 11 марта 2010

Я думаю, вы уже на правильных линиях. Тест должен подождать, пока не произойдет событие, или вы решите, что прибытие заняло слишком много времени и должно перестать ждать.

Для этого вы можете использовать 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 голосов
/ 11 марта 2010

Это мой похожий подход.

    [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 голосов
/ 11 марта 2010

Возможно, это просто плохой пример, но ваш лифт звучит больше как конечный автомат, чем что-то, что просто обрабатывает асинхронно.

Таким образом, ваш первый набор тестов может проверить, что GoToFloor () установит состояние перемещения и правильное направление его движения.

Тогда следующий набор тестов будет на TestElevatorArrived () и будет проверять, что если ваше состояние двигается к определенному этажу, то фактическое движение (то есть, функция, которая вызывается после асинхронного ожидания, или обработчик для аппаратное обеспечение, запускающее событие «Move») установит состояние на ожидаемый этаж.

В противном случае, то, что вы тестируете, скорее всего, то, что вы издеваетесь над аппаратным обеспечением, правильно имитируют время и движение, что кажется неправильным.

0 голосов
/ 07 октября 2010

Мне действительно не нравится состояние гонки с методом 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 секунд? Измените время ожидания.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...