Статьи

Асинхронное и отрицательное тестирование

Фотография, описывающая эту статью, представляет собой температурный датчик для проверки, конечно же, разницы температур.

Разработка через тестирование — это метод, который можно применять на любом уровне детализации: не только для классов и небольших групп объектов, но и в масштабе системы. В этом случае вы пишете сквозные тесты, охватывающие ввод пользователя и конечный результат, ответ, который он получает, или некоторые побочные эффекты.

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

Поскольку эта операция требует значительных ресурсов процессора, она обычно перемещается в фоновом режиме после получения видео. Затем на запрос POST сразу же приходит ответ. Если мы хотим выполнить утверждение, то мы не можем проверить сразу после ответа тестируемой системы, поскольку операция еще не завершена.

Под проверкой я подразумеваю выполнение нового запроса GET на странице списка воспроизведения или, в любом случае, повторный запрос системы о его состоянии. Мы должны ждать недетерминированное количество времени, прежде чем что-то утверждать.

Так сколько же нам ждать?

Два метода Фаулера для асинхронного тестирования

Антишаблон для решения этих проблем называется голым сном : он состоит в ожидании X (милли) секунд с помощью вызова sleep () перед выполнением утверждения.

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

Мартин Фаулер описывает две схемы устранения этих нестабильных тестов.

Первый — это цикл опроса : проверки на наличие ответа выполняются многократно, и проверка не выполняется только в том случае, если результат неверен или не поступает в течение длительного времени:

int limit = 30;
int elapsed = 0;
while (elapsed < limit) {
    bool result = makeAssertion();
    if (result) {
        break;
    }
    elapsed++;
    sleep(1);
}
if (!result) {
    fail("After 30 seconds the expected result has not been produced.");
}

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

Второй шаблон — обратный вызов : система предоставляет механизм синхронизации, либо для целей интеграции, либо даже для конечного пользователя, чтобы узнать, когда закончилось асинхронное действие. Например, журнал может быть записан или письмо может быть отправлено пользователю, когда его видео получило миниатюры и, наконец, было добавлено в выбранный список воспроизведения. Вот пример в Java, предполагая, что система рассылки была смоделирована:

// setup
int timeout = 30;
final AssertionToken synchronizationObject = new AssertionToken(); // an empty class
// inside the mailer mock, called when the operation has finished
public void send(String address, ...) {
    synchronized (synchronizationObject) {
        synchronizationObject.notify();
    }
}
// in the test
synchronized (synchronizationObject) {
    synchronizationObject.wait(timeout); // also have to catch exceptions
}
assertEquals(...);

Метод Ната Прайса для отрицательного тестирования

Один из авторов Growing Object-Oriented Software описывает в одном из своих выступлений дополнительные примеры механизмов синхронизации для создания утверждений, которые могут быть даже многопоточными (синхронизация в цикле событий Swing).

Один из описанных методов включает отрицательное тестирование: как асинхронно проверить, что что-то * не * происходит.

Основная логика этой техники:

  1. делать (X).
  2. не может утверждать, что эффект (X) не происходит за конечное время. Так же и (Y).
  3. эффект подтверждения (Y) произошел .
  4. Эффект подтверждения (X) не произошел : из-за упорядочения это могло произойти только до эффекта (Y). Теперь вы можете безопасно выполнить утверждение.

Чтобы исправить идеи, давайте посмотрим на реальный пример в коде Erlang. В этом тесте участвуют два узла, которые являются разными процессами, возможно, на разных машинах, в Милане и Генуе.
Тест должен проверить, что когда мы запрашиваем запись, начинающуюся с «а», мы получаем только два ответа: один из Милана и один из Генуи. Генуе был передан запрос: каждый узел передается другому, который знает запрос на чтение, прежде чем попытаться ответить.

Запрос должен прекратиться после того, как он достигнет обоих узлов, и не должен быть снова отправлен в Милан, обратно в Геную и так далее на неопределенный срок.

flood_without_loops(Milan) ->
    Genoa = tuplenode:init(),
    tuplenode:input(Milan, {a, 1}),
    tuplenode:input(Genoa, {a, 2}),
    tuplenode:addFloodTarget(Milan, Genoa),
    tuplenode:addFloodTarget(Genoa, Milan),
    tuplenode:readNonBlocking(Milan, startingWith(a), 1001),
    receive
        {Promise1, _} -> ?assertEqual(1001, Promise1)
    end,
    receive
        {Promise2, _} -> ?assertEqual(1001, Promise2)
    end,
    % what to do now?

Мы не можем детерминистически проверить, что никакие другие сообщения не получены. Даже если мы подождем, мы не будем уверены, что сообщения не будут отправлены: они могут быть в рейсе между Генуей и Миланом. Исходя из производственного кода (не показан), мы уверены только в том, что перед обработкой запроса произойдет наводнение: поэтому, если запрос был снова отправлен в Милан, он был отправлен к концу теста из-за заказа, так как Генуя ответила. Но мы не можем ждать, пока сообщение не придет.

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

Таким образом, основная проблема заключается в следующем: как ждать достаточно долго, чтобы убедиться, что ошибочное сообщение не было отправлено, но достаточно, чтобы не запускать медленный, нестабильный тест?
Прайс предполагает, что запросы обрабатываются по порядку. Если вы можете гарантировать, что в этой системе мы можем добавить этот код в тест:

    tuplenode:readNonBlocking(Genoa, startingWith(a), 1002),
    receive
        {Promise3, _} -> ?assertEqual(1002, Promise3)
    end,
    receive
        {Promise4, _} -> ?assertEqual(1002, Promise4)
    end.

Мы выдаем новый запрос с идентификатором 1002 в Геную . Мы получаем все поступающие сообщения (без какой-либо фильтрации), и следующие два должны иметь идентификатор 1002, соответствующий новому запросу.

Поскольку:

  • Узлы выполняются в одном цикле событий (а-ля Node JS);
  • сообщения отправляются по порядку;

Сообщение о затоплении 1002 будет обработано Миланом только после предыдущего сообщения 1001, так как 1001 было затоплено Генуей перед отправкой {Promise2, _}. Поэтому, если мы получаем {Promise3, _} и {Promise4, _}, один из них должен прибыть из Милана; тогда 1001 больше не существует, или это задержало бы два ответа на 1002.