Как тестировать private методы junit
Programming stuff
Страницы
среда, 8 мая 2013 г.
Как тестировать закрытые методы?
В комментариях к одной из заметок в Г+ мне предложили рассказать о тестировании закрытых методов. Поскольку это интересная тема, то в сегодня я постараюсь ответить на этот вопрос.
Q: — Как тестировать закрытые методы?
A: — Напрямую – никак!
Ну а теперь давайте поговорим об этом более подробно.
Черный ящик vs Белый ящик
Существует две распространенных стратегии тестирование: по принципу «Белого ящика» и по принципу «Черного ящика».
В случае «белого ящика» автор теста знает внутреннюю структуру тестируемого кода («видит» его насквозь) и подбирает тестовые данные таким образом, чтобы покрыть все ветвления, условия, выполнить все операторы и т.д. В случае «черного ящика» тесты пишутся без учета внутренней структуры тестируемого кода, а лишь через призму открытых входов/выходов.
Поскольку юнит-тесты используют подход «Белого ящика», то можно подумать, что во время тестирования нам нужно сосредоточиться на содержимом закрытых методах и проверить их в юнит тестах. Однако юнит-тест – это один из видов клиентов класса, а клиенты не должны уж очень сильно интересоваться внутренностями и подробностями реализации.
“Серый ящик”
Чем привлекателен подход на основе «белого ящика»? Он привлекателен тем, что мы таким образом обеспечиваем более высокую степень покрытия кода тестами, что дает нам больше уверенности в том, что код ведет себя так, как ожидается.
Но ведь увеличение покрытия кода и проверка ветвлений тестируемого кода не является самоцелью. На самом деле, мы хотим выявить и проверить максимальное количество граничных условий, большая часть которых выражается в виде условных операторов в тестируемом коде!
Думая о классе лишь через призму открытого интерфейса бывает сложно понять, какие входные данные правильные, а какие – нет. Поэтому мы часто заглядываем в реализацию и анализируем те самые ветвления, чтобы понять граничные условия абстракции.
Если посмотреть на разработку через тестирование (a.k.a. TDD), то там мы получим обратную картину: вначале мы пишем тесты, покрывающие граничные условия, в результате которых в реализации у нас появляются условные операторы.
В результате мы получаем тестирование на основе «серого ящика», когда внутреннее устройство класса помогает нам понять возможное поведение, но добиваемся мы этого поведения лишь через открытый интерфейс.
Давайте рассмотрим класс, представляющий диапазон значений. Инвариантом этого класса является то, что нижняя граница должна быть не больше верхней. Для обеспечения этого инварианта у нас должны быть соответствующие проверки в конструкторе и, если класс изменяемый, в методе Change:
class Range
<
public int LowerBound < get ; private set ; >
public int UpperBound
public Range( int lowerBound, int upperBound)
<
if (lowerBound > upperBound)
throw new ArgumentException (
«Lower bound should be less or equal to upper bound» );
LowerBound = lowerBound;
UpperBound = upperBound;
>
>
Затем мы можем написать простые тесты, для проверки валидности диапазона:
Но для класса Range существуют и другие граничные условия (особенно если для границ интервала использовать double, а не int), которые могут быть не выражены в коде класса Range вовсе, но которые стоит проверить в тестах. Так, например, я бы добавил тест для проверки пустого интервала [TestCase(0, 0, Result = true)], и хотя такой тест не увеличивает покрытия, информация об этом граничном условии может быть полезной сама по себе, как для меня сейчас, так и для другого разработчика в будущем.
Фред Брукс в своей книге “The Design of Design” писал, что «иногда главная проблема заключается в том, чтобы понять, в чем же заключается проблема». Внутренняя структура кода может быть отличным источником для понимания граничных условий существующего кода, но для их определения нам все равно придется включать свой мозг и думать, какие у данного класса входы и выходы, и как он должен вести себя при вызове метода в определенных условиях или при переходе из одного состояния в другое.
Так как насчет тестирования закрытых методов?
Вполне понятно, что основная логика классов находится не в открытых методах непосредственно, а выделена во вспомогательные закрытые методы, которые нам тоже хотелось бы проверить. В этом случае может появится желание написать юнит-тесты непосредственно для закрытых методов, однако это не лучшая идея и вот почему:
1. Логика в закрытом методе – это граничные условия класса, доступные через открытый интерфейс
Мы можем добраться до большинства условий в закрытом методе подобрав соответствующие входные данные открытого метода или путем вызова нескольких методов для перевода объекта в требуемое состояние. Если же протестировать закрытый метод косвенным образом сложно, то это само по себе может говорить о плохом дизайне класса и о том, что он делает слишком многое.
2. Инварианты класса могут быть нарушены внутри закрытых методов
Инварианты класса – т.е. условия, истинные на протяжении всего времени жизни объекта могут быть нарушены в момент вызова закрытого метода. Другими словами, закрытые методы не автономны и могут работать с частично-невалидным объектом; а поскольку сделать частично-невалидный объект из тестов очень сложно, то и проверить граничные условия закрытого метода будет не просто (ведь тест – это клиент, а клиенты не должны иметь доступ к «частично-невалидным» объектам).
Так, если инвариантом класса Range является условие: LowerBound
Как вы тестируете приватные методы?
Я работаю над проектом Java. Я новичок в модульном тестировании. Каков наилучший способ модульного тестирования частных методов в классах Java?
Обычно вы не тестируете приватные методы напрямую. Поскольку они являются частными, рассмотрим их подробно реализации. Никто никогда не собирается звонить одному из них и ожидать, что он будет работать определенным образом.
Вместо этого вы должны проверить свой публичный интерфейс. Если методы, которые вызывают ваши приватные методы, работают так, как вы ожидаете, то вы, следовательно, предполагаете, что ваши приватные методы работают правильно.
В общем, я бы этого избегал. Если ваш закрытый метод настолько сложен, что требует отдельного модульного теста, это часто означает, что он заслуживает своего собственного класса. Это может побудить вас написать это способом, который можно использовать повторно. Затем вы должны протестировать новый класс и вызвать его открытый интерфейс в вашем старом классе.
С другой стороны, иногда разделение деталей реализации на отдельные классы приводит к классам со сложными интерфейсами, большому количеству данных, передаваемых между старым и новым классами, или к дизайну, который может выглядеть хорошо с точки зрения ООП, но не сопоставить интуиции, поступающие из проблемной области (например, разделение модели ценообразования на две части просто для того, чтобы избежать тестирования частных методов, не очень интуитивно понятно и может впоследствии привести к проблемам при обслуживании / расширении кода). Вы не хотите иметь «классы близнецов», которые всегда меняются вместе.
How do you unit test private methods?
I am working on a java project. I am new to unit testing. What is the best way to unit test private methods in java classes?
13 Answers 13
You generally don’t unit test private methods directly. Since they are private, consider them an implementation detail. Nobody is ever going to call one of them and expect it to work a particular way.
You should instead test your public interface. If the methods that call your private methods are working as you expect, you then assume by extension that your private methods are working correctly.
In general, I would avoid it. If your private method is so complex that it needs a separate unit test, it often means that it deserved its own class. This may encourage you to write it in a way which is reusable. You should then test the new class and call the public interface of it in your old class.
On the other hand, sometimes factoring out the implementation details into separate classes leads to classes with complex interfaces, lots of data passing between the old and new class, or to a design which may look good from the OOP point of view, but does not match the intuitions coming from the problem domain (e.g. splitting a pricing model into two pieces just to avoid testing private methods is not very intuitive and may lead to problems later on when maintaining/extending the code). You don’t want to have «twin classes» which are always changed together.
Любишь покрывать код тестами? Тебе нравится приятное теплое чувство защищенности, которое возникает при прохождении тестов?
Настоящие профессионалы не полагаются на случай, они стелют соломку заранее держат все под контролем.
Хочешь чтобы внутри, за публичным интерфейсом, тоже все было покрыто тестами?
Как и всегда, способов несколько. Есть получше, есть попроще. Поехали!
1. Делаем метод публичным
Нет приватного метода – нет проблемы. Можно еще комментарий к методу написать
Плюсы: очень просто.
Минусы: засоряется интерфейс, страдает инкапсуляция.
2. Делаем метод внутренним
А тестировать-то его как? Просто. Нужно добавить к сборке атрибут InternalsVisibleTo. Этот атрибут даст тестирующей сборке доступ ко всем internal методам и свойствам тестируемой сборки.
Не забудьте, что для функционирования атрибута InternalsVisibleTo требуется, чтобы обе сборки (тестирующая и тестируемая) были одновременно подписаны строгим именем, либо одновременно не подписаны.
Плюсы: достаточно просто, остается контроль за тем, кто имеет доступ к внутренностям сборки.
Минусы: все равно засоряется интерфейс, все равно страдает инкапсуляция, появляются дополнительные условия (см. выше про подписывание), атрибут остается в релизной сборке.
3. Делаем метод защищенным
Плюсы: достаточно просто, есть некоторый контроль за тем, кто имеет доступ к методу.
Минусы: все равно засоряется интерфейс, все равно страдает инкапсуляция, метод доступен всем, кто пожелает его унаследовать, класс с таким методом не может быть помечен закрытым для наследования (sealed).
4. Используем PrivateObject
Этот класс предоставляет Visual Studio Unit Testing Framework, так что если в проекте используется NUnit или еще что-то, то этот способ не подойдет.
С PrivateObject все просто. Есть класс для тестирования:
Есть тестирующий класс:
Плюсы: не требуется менять существующий код, достаточно просто.
Минусы: применимо только к Visual Studio Unit Testing Framework, при переименовании полей и методов тесты начнут падать.
5. Рефлексия нам поможет
Код будет подлиннее, чем в предыдущем способе, но жить можно.
Опять же, есть класс для тестирования:
В тесте нужно написать следующее:
В консоли снова должен появиться хороший совет.
Вышеприведенный код можно использовать в любых тестах. Хоть для NUnit, хоть для Visual Studio Unit Testing Framework, хоть для любой другой среды тестирования.
Плюсы: не требуется менять существующий код, достаточно просто, применимо для люой среды тестирования.
Минусы: без вспомогательного класса в тестах будут километры повторяющегося кода, при переименовании полей и методов тесты начнут падать.
Вместо заключения
Теперь, когда ты знаешь, как тестировать не-публичные методы, самое время задуматься: а надо ли их тестировать?
Лично я играю за команду «против». Я думаю, что тестировать нужно только публичные методы и свойства.
Тестирование в Java. JUnit
Сегодня все большую популярность приобретает test-driven development(TDD), техника разработки ПО, при которой сначала пишется тест на определенный функционал, а затем пишется реализация этого функционала. На практике все, конечно же, не настолько идеально, но в результате код не только написан и протестирован, но тесты как бы неявно задают требования к функционалу, а также показывают пример использования этого функционала.
Итак, техника довольно понятна, но встает вопрос, что использовать для написания этих самых тестов? В этой и других статьях я хотел бы поделиться своим опытом в использовании различных инструментов и техник для тестирования кода в Java.
Ну и начну с, пожалуй, самого известного, а потому и самого используемого фреймворка для тестирования — JUnit. Используется он в двух вариантах JUnit 3 и JUnit 4. Рассмотрю обе версии, так как в старых проектах до сих пор используется 3-я, которая поддерживает Java 1.4.
Я не претендую на автора каких-либо оригинальных идей, и возможно многим все, о чем будет рассказано в статье, знакомо. Но если вам все еще интересно, то добро пожаловать под кат.
JUnit 3
Для создания теста нужно унаследовать тест-класс от TestCase, переопределить методы setUp и tearDown если надо, ну и самое главное — создать тестовые методы(должны начинаться с test). При запуске теста сначала создается экземляр тест-класса(для каждого теста в классе отдельный экземпляр класса), затем выполняется метод setUp, запускается сам тест, ну и в завершение выполняется метод tearDown. Если какой-либо из методов выбрасывает исключение, тест считается провалившимся.
Примечание: тестовые методы должны быть public void, могут быть static.
Сами тесты состоят из выполнения некоторого кода и проверок. Проверки чаще всего выполняются с помощью класса Assert хотя иногда используют ключевое слово assert.
Рассмотрим пример. Есть утилита для работы со строками, есть методы для проверки пустой строки и представления последовательности байт в виде 16-ричной строки:
Напишем для нее тесты, используя JUnit 3. Удобнее всего, на мой взгляд, писать тесты, рассматривая нейкий класс как черный ящик, писать отдельный тест на каждый значимый метод в этом классе, для каждого набора входных параметров какой-то ожидаемый результат. Например, тест для isEmpty метода:
Можно разделить данные и логику теста, перенеся создание данных в метод setUp:
Дополнительные возможности
Кроме того, что было описано, есть еще несколько дополнительных возможностей. Например, можно группировать тесты. Для этого нужно использовать класс TestSuite:
Можно запустить один и тот же тест несколько раз. Для этого используем RepeatedTest:
Наследуя тест-класс от ExceptionTestCase, можно проверить что-либо на выброс исключения:
Как видно из примеров все довольно просто, ничего лишнего, минимум нужный для тестирования(хотя недостает и некоторых нужных вещей).
JUnit 4
Здесь была добавлена поддержка новых возможностей из Java 5, тесты теперь могут быть объявлены с помощью аннотаций. При этом существует обратная совместимость с предыдущей версией фреймворка, практически все рассмотренные выше примеры будут работать и здесь(за исключением RepeatedTest, его нет в новой версии).
Итак, что же поменялось?
Основные аннотации
Рассмотрим тот же пример, но уже используя новые возможности:
Если какой-либо тест по какой-либо серьезной причине нужно отключить(например, этот тест постоянно валится, но его исправление отложено до светлого будущего) его можно зааннотировать @Ignore. Также, если поместить эту аннотацию на класс, то все тесты в этом классе будут отключены.
Правила
Кроме всего вышеперечисленного есть довольно интересная вещь — правила. Правила это некое подобие утилит для тестов, которые добавляют функционал до и после выполнения теста.
Например, есть встроенные правила для задания таймаута для теста(Timeout), для задания ожидаемых исключений(ExpectedException), для работы с временными файлами(TemporaryFolder) и д.р. Для объявления правила необходимо создать public не static поле типа производного от MethodRule и зааннотировать его с помощью Rule.
Также в сети можно найти и другие варианты использования. Например, здесь рассмотрена возможность параллельного запуска теста.
Запускалки
Но и на этом возможности фреймворка не заканчиваются. То, как запускается тест, тоже может быть сконфигурировано с помощью @RunWith. При этом класс, указанный в аннотации должен наследоваться от Runner. Рассмотрим запускалки, идущие в комплекте с самим фреймворком.
JUnit4 — запускалка по умолчанию, как понятно из названия, предназначена для запуска JUnit 4 тестов.
JUnit38ClassRunner предназначен для запуска тестов, написанных с использованием JUnit 3.
SuiteMethod либо AllTests тоже предназначены для запуска JUnit 3 тестов. В отличие от предыдущей запускалки, в эту передается класс со статическим методом suite возвращающим тест(последовательность всех тестов).
Suite — эквивалент предыдущего, только для JUnit 4 тестов. Для настройки запускаемых тестов используется аннотация @SuiteClasses.
Enclosed — то же, что и предыдущий вариант, но вместо настройки с помощью аннотации используются все внутренние классы.
Categories — попытка организовать тесты в категории(группы). Для этого тестам задается категория с помощью @Category, затем настраиваются запускаемые категории тестов в сюите. Это может выглядеть так:
Parameterized — довольно интересная запускалка, позволяет писать параметризированные тесты. Для этого в тест-классе объявляется статический метод возвращающий список данных, которые затем будут использованы в качестве аргументов конструктора класса.
Theories — чем-то схожа с предыдущей, но параметризирует тестовый метод, а не конструктор. Данные помечаются с помощью @DataPoints и @DataPoint, тестовый метод — с помощью Theory. Тест использующий этот функционал будет выглядеть примерно так:
Как и в случае с правилами, в сети можно найти и другие варианты использования. Например, здесь рассмотрена та же возможность паралельного запуска теста, но с использованием запускалок.
Вывод
Это, конечно же, не все, что можно было сказать по JUnit-у, но я старался вкратце и по делу. Как видно, фреймворк достаточно прост в использовании, дополнительных возможностей немного, но есть возможность расширения с помощью правил и запускалок. Но несмотря на все это я все же предпочитаю TestNG с его мощным функционалом, о котором и расскажу в следующей статье.