Статьи

Модульное тестирование частных методов

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

Прежде всего, не все методы должны быть проверены модулем. Некоторые методы настолько просты, что нет смысла их юнит-тестирование. Например:

private void startLowBalanceTimer(int timeout) {
if (lowBalanceTimer != null) {
lowBalanceTimer.start(timeout);
}
}

Использование  lowBalanceTimer , включая время ожидания, необходимо проверить, но это лучше сделать при функциональном тестировании. Это быстро станет очевидным, если описанный выше метод не будет работать должным образом. Методы, которые должны быть проверены модулем, — это те, которые содержат алгоритмическую логику некоторого вида В статье «  Выборочное модульное тестирование — затраты и выгоды»  проводится различие между алгоритмами и координаторами, что, на мой взгляд, очень полезно. Лучшее использование модульного тестирования для алгоритмов, но не для координаторов. Кроме того, эти два типа функциональности не должны смешиваться в одном методе.

Косвенное тестирование?

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

Кроме того, это не работает, если вы практикуете разработку через тестирование (TDD). Если вы используете подход «снизу вверх» и разрабатываете и тестируете строительные блоки перед их объединением, то при разработке вспомогательных методов вы часто не готовы к использованию открытого метода. Таким образом, вы не получаете преимуществ тестирования при разработке.

Отдельный класс?

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

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

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

Отражение?

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

Решение: пакет Private

При удалении модификатора private метод отображается в пакете класса и больше нигде. Это означает, что метод теперь можно тестировать модулем (при условии, что в классе тестирования используется один и тот же пакет). Но разве мы не выставляем слишком много внутренностей класса?

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

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

пример

На работе мы используем инструмент генерации трафика. Одной из его функций является то, что вы можете генерировать тестовый трафик с заданной скоростью и продолжительностью, например, 200 SMS в секунду в течение 1 часа. Как скорость, так и длительность являются параметрами, которые можно установить в скрипте генерации трафика. Тем не менее, часто интересно иметь некоторые изменения в нагрузке. Например, может быть полезно проверить, как система обрабатывает нагрузку, которая начинается со 200 SMS в секунду, затем уменьшается до 10 SMS в секунду, а затем снова увеличивается.

На нашем последнем хакатоне в работе я добавил возможность добавить «масштабирующие значения» в скрипт для достижения этой цели. Значения масштабирования — это число целых чисел от 0 до 100, которые указывают, какой процент от номинальной скорости следует использовать. Таким образом, если заданы значения [100, 80, 20, 100], то скорость будет изменяться линейно между 100% и 80% в течение первой трети продолжительности. Затем ставка изменяется линейно между 80% и 20% для средней трети и между 20% и 100% для последней трети.

У этого формата есть три преимущества. Во-первых, нет необходимости указывать абсолютные значения для ставки. Если скорость составляет 200 SMS в секунду, 50% этой скорости составляет 100 SMS в секунду, но если скорость составляет 300 SMS в секунду, то же значение масштабирования 50 вместо этого даст 150 SMS в секунду. Во-вторых, значения масштабирования относятся к продолжительности сценария. Одинаковые значения масштабирования могут использоваться как для 10-минутного сценария, так и для 1-часового сценария. В-третьих, можно получить более подробную форму тарифа, увеличив число значений масштабирования. Например, значения [100, 90, 70, 40, 20, 10, 0, 0, 10, 20, 40, 70, 90, 100] дают более детальную форму скорости, чем в предыдущем примере.

Чтобы найти масштабированную скорость в любой момент времени в сценарии, класс  ScalingValue предоставляет следующий открытый метод:

public double getCurrentRate(long now, double maxRate)

Учитывая время, номинальную скорость и значения масштабирования (предоставленные при создании  экземпляра ScalingValue ), возвращается текущая скорость. Внутри есть несколько вспомогательных методов. Один внутренний метод  getNextIndex  находит, между какими двумя значениями масштабирования находится текущая отметка времени. Другой метод, getScalingFactor , принимает два значения масштабирования и отметку времени и вычисляет коэффициент масштабирования в этой точке. Например, если значение масштабирования в момент времени 1000 равно 60, а значение масштабирования в момент времени 2000 равно 80, тогда значение масштабирования в момент 1500 составляет 70, а в момент 1750 — 75 и так далее.

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

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

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

Заключение

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