Friday, March 23, 2012

Расширяем JUnit. Декларативные предусловия.

Нам довольно часто приходится писать тесты, в которых по мере разработки сценариев мы выносим общую логику в setUp. Это хорошая практика, но с увеличением количества тестовых сценариев setUp может разрастись до довольно больших размеров. Мне приходилось сталкиваться с такими тестами. Причем принцип Парето в них зачастую соблюдается так: 80% всех тестов используют 20% функциональности, находящейся в setUp. Кроме сложного setUp каждый тест еще обычно создает свои специфические предусловия. Это довольно сильно усложняет понимание такого теста, т.к. приходится постоянно помнить, что находится в setUp и, к тому же, понимать какие предусловия дополнительно создаются для каждого теста.

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

Итак, предположим нам нужно разработать фильтр для файлов, аналогичный тому, который мы используем в файловых менеджерах. Например если наложить фильтр *.txt, то должен вернутся список текстовых файлов.

Понятно, что тестовых сценариев может быть масса и в обычном случае мы бы создавали временную папку в setUp, удаляли ее в tearDown. В каждом тесте создавали бы файлики с разными именами, после чего проверяли бы фильтр.

А было бы легче работать с тестом, который написан таким образом?


Как такое может заработать?


Самый простой вариант - это реализовать свой ранер, наследник от базового класса org.junit.runner.Runner и указывать его в качества "запускальщика":



Полный текст кода класса FileFilterRunner


Что здесь интересного
1. Наследуемся от BlockJUnit4ClassRunner
2. Перекрыт метод runChild. В нем перед вызовом тестового метода создаем временную директорию и заполняем ее тестовыми файлами, имена которых берем из нашей аннотации Given. Для работы с файлами использую библиотеку Apache Commons IO
3. Перекрыт метод 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):


Например, нам в тесте понадобился список созданных файлов и путь к корневой директории:


Полный текст исходников и проекта находится здесь качать

No comments:

Post a Comment