Нам довольно часто приходится писать тесты, в которых по мере разработки сценариев мы выносим общую логику в setUp. Это хорошая практика, но с увеличением количества тестовых сценариев setUp может разрастись до довольно больших размеров. Мне приходилось сталкиваться с такими тестами. Причем принцип Парето в них зачастую соблюдается так: 80% всех тестов используют 20% функциональности, находящейся в setUp. Кроме сложного setUp каждый тест еще обычно создает свои специфические предусловия. Это довольно сильно усложняет понимание такого теста, т.к. приходится постоянно помнить, что находится в setUp и, к тому же, понимать какие предусловия дополнительно создаются для каждого теста.
Я решил немножко упростить такой подход и вынести установку предусловий на уровень декларации (то бишь в нашем случае это будут аннотации). Пример, который буду здесь рассматривать не имеет ничего общего с тем кодом, с которым приходилось работать, т.к. он довольно сложный для демонстрации самой идеи.
Итак, предположим нам нужно разработать фильтр для файлов, аналогичный тому, который мы используем в файловых менеджерах. Например если наложить фильтр *.txt, то должен вернутся список текстовых файлов.
Понятно, что тестовых сценариев может быть масса и в обычном случае мы бы создавали временную папку в setUp, удаляли ее в tearDown. В каждом тесте создавали бы файлики с разными именами, после чего проверяли бы фильтр.
А было бы легче работать с тестом, который написан таким образом?
Как такое может заработать?
Самый простой вариант - это реализовать свой ранер, наследник от базового класса
org.junit.runner.Runner и указывать его в качества "запускальщика":
Полный текст кода класса FileFilterRunner
Что здесь интересного
1. Наследуемся от
BlockJUnit4ClassRunner2. Перекрыт метод
runChild. В нем перед вызовом тестового метода создаем временную директорию и заполняем ее тестовыми файлами, имена которых берем из нашей аннотации Given. Для работы с файлами использую библиотеку
Apache Commons IO3. Перекрыт метод
validatePublicVoidNoArgMethods. Это позволяет запускать тестовые методы с параметрами
4. Перекрыт метод
methodInvoker. Этот метод вызывается JUnit фреймворком для каждого тестового метода
5. Класс
ParameterizedInvoker - это наша реализация "вызывальщика" тестовых методов. Мы вызываем тестовый метод с параметром testFolder
Вот сама аннотация Given:
Что получили?
1. Тест стал более читабелным за счет того, что мы вынесли инициализацию предусловий в документирующую аннотацию Given.
2. Реализованный ранер можно легко использовать в других тестах.
3. Если развивать этот подход, то уменьшится необходимость создавать суперкласс для тестовых классов, в котором хранить общую логику для всех наследуемых тестов. Все в аннотациях.
4. Нету setUp/tearDown в тесте
5. Инициализируем только то, что нужно для теста. Если нам, например, понадобится набор папок, то мы добавим в аннотацию параметер folders. Если пользователь с именем "Вася", то - параметр users.
6. Можем ужесточать или ослаблять предусловия. Что если, например, параметр files станет обязательным? Мы уберем инициализацию по умолчанию
default {}, тем самым тесты, которые не использовали обязательные параметры перестанут компилироваться и мы их сразу обнаружим
7. Если видишь, чем хорош или плох этот подход - пиши комент, дополню список
Идеи развития
Аннотированные параметры тестового метода. Если есть необходимость передавать разные параметры в тестовый метод, то мы можем их именовать с помощью анотаций, имеющих тип
@Target(ElementType.PARAMETER):
Например, нам в тесте понадобился список созданных файлов и путь к корневой директории:
Полный текст исходников и проекта находится здесь
качать