Интеграционные тесты + Maven

:

Резюме: хоть Maven и является инструментом, стандартизирующим структуру проекта и его сборку, однако он с крахом провалил стандартизацию разного рода тестов. Ща разберем полеты.

Итак, почему же все-таки Maven не справляется со стандартизацией тестов, ведь:

  • Модульные тесты:

    • Лежат в src/test каталоге
    • Названия тестовых классов содержат слово Test
  • Интеграционные тесты:

    • Выполнаются на фазе integration-test
    • В их имени должны быть буквы IT обозначающие собсно IntegrationTests.

Проблем на самом деле с этим несколько:

  • Фаза integration-test выполняется после модульных тестов, это значит что отдельно их не запустить. Каждый раз когда мы запускаем интеграционные тесты, выполняются модульные. Однако обычно мы хотим запустить модульные тесты один раз, а затем отдельно запускать интеграционные. Выходит чтоб модульные тесты не выполнялись нам нужно пропускать их с помощью -DskipTests, затем окажется что интеграционные тесты тоже не запускаются потому что failsafe плагин использует под собой surefire, начнется геморрой с созданием профилей и в конце концов начнет казаться что все это слишком сложно. Кстати, почему нам важно запускать тесты раздельно:

  • Разработчики могут быстро получить “зеленый” фидбек и продолжить работать. Именно модульные тесты способны быстро дать базовый ответ.

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

  • Интеграционные тесты медленные, в зависимости от длительности их можно запускать по каждому коммиту, раз в день и т.п. Они тоже могут дробиться на более мелкие группы тестов, например, основные Smoke тесты выполняются первыми, затем регрессия, затем приемочные, затем какие-нибудь нагрузочные и т.п. Их удобно разделять потому что мы точно будем знать что сломалось. А также мы можем запускать отдельно определенную группу тестов и не ждать 4 часа чтоб прошло все.

  • Мы можем хотеть запускать одни и те же системные несколько раз. Например, мы их прогнали по текущему коммиту, а затем еще по тому что был неделю назад для того чтоб увидеть разницу между результатами.

  • Для системных тестов часто нужно подготовить окружение прежде чем их запускать.

  • Стандартно failsafe использует каталог src/test, а нам редко когда нужно помещать интеграционные и модульные тесты и ресурсы в одни и те же пакеты.

  • Фаза integration-test запускается перед install каждый раз. Мы же не хотим запускать медленные интеграционные тесты каждый раз когда устанавливаем артефакто в локальный репозиторий.

  • К сожалению для большинства разработчиков существуют лишь модульные, интеграционные тесты и “то, что делают QA”. Однако на самом деле тесты разделяются как минимум на модульные, системные и компонентные в зависимости от масштабов. Также есть функциональные и нефункциональные тесты и т.п. Более подробно можно с видами тестирования ознакомиться в одноименной статье. Но что главное - мы можем захотеть разделить все эти тесты. Для одних нужно поднимать все приложение, для других - лишь часть, для третьих вообще одного класса хватит. Однако Maven их никак не различает и не разделяет, у него есть либо модульные, либо интеграционные.

В общем подумав немного можно прити к выводу, что раз стандартный механизм Maven настолько несовершенен и все равно не сможет поддерживать всего нужного, мы можем отойти от него. Вместо этого предлагаю использовать plain old surefire plugin. Да, этот плагин заточен на написание модульных тестов, однако они по факту ничем не будут отличаться от “немодульных” - те же JUnit/TestNG будут описывать всю их логику (хотя тут позволяется также использовать всякие BDD фреймворки навроде JBehave, однако не о них речь).

Так вот как же это будет выглядеть. Для каждого из видов тестирования мы будем создавать а) профиль б) каталог с исходниками и ресурсами. Конфигурироваться же Maven будет следующим образом:

<properties>
    <test.sourceDirectory>${project.basedir}/src/test/java</test.sourceDirectory>
    <test.resourceDirectory>${project.basedir}/src/test/resources</test.resourceDirectory>
  </properties>

  <build>
    <testSourceDirectory>${test.sourceDirectory}</testSourceDirectory>
    <testResources>
      <testResource>
        <directory>${test.resourceDirectory}</directory>
      </testResource>
    </testResources>
  </build>
  <profiles>
    <profile>
      <id>component-test</id>
      <properties>
        <test.sourceDirectory>${project.basedir}/src/component-test/java</test.sourceDirectory>
        <test.resourceDirectory>${project.basedir}/src/component-test/resources</test.resourceDirectory>
      </properties>
    </profile>
    <profile>
      <id>system-test</id>
      <properties>
        <test.sourceDirectory>${project.basedir}/src/system-test/java</test.sourceDirectory>
        <test.resourceDirectory>${project.basedir}/src/system-test/resources</test.resourceDirectory>
      </properties>
    </profile>
  </profiles>

Заметьте, что в относительно небольшом куске конфигурации мы описали 3 вида тестов и все они находятся в разных каталогах:
  • Модульные: mvn test

  • Компонентные: mvn test -Pcomponent-test

  • Системные: mvn test -Psystem-test

Структура каталогов тогда такая:

src
 |_main
      |_java
      |_resources
 |_test
      |_java
      |_resources
 |_component-test
      |_java
      |_resources
 |_system-test
      |_java
      |_resources

Правда системные тесты как правило имеет смысл выделять в отдельные модули, ато и проекты.

Единственное в чем я слукавил: фаза интеграционных тестов делиться на несколько шагов и это позволяет нам сначала поднять окружение, а затем его остановить. Подход с surefire такого не даст, вам придется вручную запускать окружение с помощью явно указанных команд. Однако:

  • Если нам нужно полностью развернутое окружение, то это не всегда возможно автоматизировать без написания скриптов, что значит что в большинстве случаев все равно придется руками дергать их

  • Сложность в конфигурировании разного рода тестов перекрывает сложность запуска какого-нибудь tomcat:start

  • Т.к. обычно системные тесты выносят в отдельные модули или проекты, то там вполне можно использовать фазу integration-test как единственную, которая запускает тесты.

Но если вам правда хочется удобным образом автоматизировать развертывание окружения, возможно вам будет лучше сконфигурировать integration-test фазу для этого.

JUnit Categories, TestNG Groups

Последнее о чем стоит упомянуть - это JUnit категории и TestNG группы. Они позволяют с помощью аннотаций как-то помечать тесты. Однако если в TestNG это хоть как-то можно сделать удобно, то в JUnit это сделано через прямую кишку и вам придется конфигурить много профилей, у вас не выйдет задать exclude=IntegrationTest по умолчанию, а затем через командную строку активировать какие-то другие категории, для этого вам придется все равно создавать профили, а значит никакого удобства такой способ не принесет. Раз не выйдет это использовать с JUnit’ом, значит не имеет смысл стандартизировать такой подход.