Статьи

TDD, модульные тесты и время

Многим программистам трудно писать хорошие юнит-тесты для кода, который требует времени. Например, как вы тестируете тайм-ауты или периодические работы по очистке? Я видел много тестов, которые создают сложные настройки с большим количеством зависимостей или вводят пробелы в реальном времени, просто чтобы иметь возможность тестировать эти части. Однако, если вы правильно структурируете код, большая часть сложности исчезнет. Вот пример техники, которая позволяет с легкостью тестировать код, связанный со временем.

Сделать время внешним

Ключ должен сделать время внешним по отношению к коду, который вы тестируете. Например, недавно на работе я писал код для сбора нескольких частей текстового сообщения (SMS). Части прибывают независимо, и когда все части были собраны, полное сообщение (все части) доставлено. Конечно, не все детали могут прибыть (например, из-за проблем с сетью), поэтому существует необходимость в тайм-ауте. Структура данных, которую я использовал, в основном представляет собой массив, в котором хранится каждая поступающая часть. Когда прибывает последняя часть, сообщение доставляется полностью, а массив освобождается. Однако, если все детали не поступили в течение 30 секунд, доставленные детали должны быть доставлены в любом случае, а массив освобожден.

Логика реализована в классе ConcatInMemoryHandler . В дополнение к методу collectPart () у него есть метод tick () :

public void tick() {
  ticks++;
  checkForConcatTimeout(ticks);
}

Метод tick () вызывается каждую секунду. Он увеличивает счетчик тиков, а затем проверяет время ожидания. Когда приходит первая часть сообщения, текущее значение тиков сохраняется в массиве. Если время ожидания установлено равным 30 секундам, checkForConcatTimeout () нужно только сравнить текущее значение тиков (переданное в качестве единственного аргумента) с сохраненным значением. Если разница больше 30, есть тайм-аут.

Чтобы сделать тестирование еще проще, значение времени ожидания устанавливается в конструкторе  ConcatInMemoryHandler . Установив значение 1 вместо 30, вам нужно всего лишь дважды вызвать tick (), чтобы истекло время ожидания. Вот пример теста:

public void testNotAllParts_timeOut() {
  // Adding 2 of 4 parts (two parts missing).
  // Should time out (with the parts received).
  ConcatInMemoryHandler handler;
  handler = new ConcatInMemoryHandler(1, user);
  List<SubmitImpl> submits = makeSubmits(4);
  handler.collectPart(submits.get(3), user);
  handler.collectPart(submits.get(1), user);
  assertEquals(1, handler.getPartsHashMapSize());
 
  // now time out without two of the parts
  handler.tick();
  handler.tick();
  assertEquals(0, handler.getPartsHashMapSize());
  assertEquals(2, user.timedoutSubmits.size());
}

преимущества

Есть несколько преимуществ того, чтобы позволить времени быть внешним и передавать его только в качестве аргумента:

  1. Нет пробелов в реальном времени в тестах — пропуск 5 секунд — это просто вызов tick () 5 раз.
  2. Нет необходимости вызывать System.currentTimeMillis () , sleep () или подобное.
  3. Не нужно запускать другие темы.
  4. Легко использовать короткое время ожидания — просто настройте более низкое значение для теста.

Там нет никаких реальных недостатков. Все, что вам нужно сделать, это убедиться, что обработчик создан с правильным значением времени ожидания, и убедиться, что tick () вызывается каждую секунду. На самом деле это пример кода, о котором писал Джон Сонмез, только две роли — алгоритмы и координаторы. ConcatInMemoryHandler является алгоритм, и координация осуществляется с помощью кода , который создает обработчик и вызовы клеща () на нем периодически. Это очень полезный способ думать (и писать) о программах, и статья Джона отлично читается.

Этот способ структурирования кода является естественным результатом использования разработки через тестирование (TDD). Поскольку вы пытаетесь структурировать код таким образом, чтобы сделать его тестируемым, вы в конечном итоге разделяете проблемы. Получающаяся структура, больше чем сами тесты, является самым большим преимуществом использования TDD.

Небольшая заметка о тесте выше. Обычно я против добавления любого кода, который есть только для целей тестирования. Тем не менее, я делаю исключение для простого геттера, который показывает некоторый аспект состояния. В приведенном выше коде это метод getPartsHashMapSize (), который позволяет увидеть, сколько существует текущих коллекций. Я знаю, что некоторые люди недовольны таким способом разоблачения внутренних органов, но я считаю, что это намного лучше и проще, чем косвенная проверка того же самого. Если все меняется внутренне, тривиально изменить метод или то, что он возвращает. Другими словами, это прагматичный компромисс, что хорошо ™.

Если вы еще не пробовали этот способ структурирования кода, связанного со временем, попробуйте. Убедиться в правильности кода становится легко, потому что писать тесты так просто.