Многим программистам трудно писать хорошие юнит-тесты для кода, который требует времени. Например, как вы тестируете тайм-ауты или периодические работы по очистке? Я видел много тестов, которые создают сложные настройки с большим количеством зависимостей или вводят пробелы в реальном времени, просто чтобы иметь возможность тестировать эти части. Однако, если вы правильно структурируете код, большая часть сложности исчезнет. Вот пример техники, которая позволяет с легкостью тестировать код, связанный со временем.
Сделать время внешним
Ключ должен сделать время внешним по отношению к коду, который вы тестируете. Например, недавно на работе я писал код для сбора нескольких частей текстового сообщения (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()); }
преимущества
Есть несколько преимуществ того, чтобы позволить времени быть внешним и передавать его только в качестве аргумента:
- Нет пробелов в реальном времени в тестах — пропуск 5 секунд — это просто вызов tick () 5 раз.
- Нет необходимости вызывать System.currentTimeMillis () , sleep () или подобное.
- Не нужно запускать другие темы.
- Легко использовать короткое время ожидания — просто настройте более низкое значение для теста.
Там нет никаких реальных недостатков. Все, что вам нужно сделать, это убедиться, что обработчик создан с правильным значением времени ожидания, и убедиться, что tick () вызывается каждую секунду. На самом деле это пример кода, о котором писал Джон Сонмез, только две роли — алгоритмы и координаторы. ConcatInMemoryHandler является алгоритм, и координация осуществляется с помощью кода , который создает обработчик и вызовы клеща () на нем периодически. Это очень полезный способ думать (и писать) о программах, и статья Джона отлично читается.
Этот способ структурирования кода является естественным результатом использования разработки через тестирование (TDD). Поскольку вы пытаетесь структурировать код таким образом, чтобы сделать его тестируемым, вы в конечном итоге разделяете проблемы. Получающаяся структура, больше чем сами тесты, является самым большим преимуществом использования TDD.
Небольшая заметка о тесте выше. Обычно я против добавления любого кода, который есть только для целей тестирования. Тем не менее, я делаю исключение для простого геттера, который показывает некоторый аспект состояния. В приведенном выше коде это метод getPartsHashMapSize (), который позволяет увидеть, сколько существует текущих коллекций. Я знаю, что некоторые люди недовольны таким способом разоблачения внутренних органов, но я считаю, что это намного лучше и проще, чем косвенная проверка того же самого. Если все меняется внутренне, тривиально изменить метод или то, что он возвращает. Другими словами, это прагматичный компромисс, что хорошо ™.
Если вы еще не пробовали этот способ структурирования кода, связанного со временем, попробуйте. Убедиться в правильности кода становится легко, потому что писать тесты так просто.