Статьи

Интеграционные тесты из окопов

 

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

В 2013 году, если вы являетесь стандартным разработчиком, вы практически тестируете свой код. Независимо от того, являетесь ли вы специалистом по TDD или просто создаете их впоследствии, большинство понимает, что надежный автоматизированный тестовый комплект не является обязательным, а обязательным.

Модульный тест против интеграционного теста

Однако за понятием «тест» скрыт некоторый смысл. В зависимости от человека, которого вы спрашиваете, могут быть некоторые различия в том, что такое тест. Давайте предоставим два определения, чтобы предотвратить возможное недопонимание в будущем.


Модульный тест

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

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

Интеграционный тест проверяет совместную работу двух (или более) блоков.
В этом случае единица является классом. Это означает, что как только вы захотите проверить результат выполнения более чем одного класса, это будет интеграционный тест.

С одной стороны, достижения успеха в 100% UT при 100% покрытии кода недостаточно, чтобы гарантировать безошибочное приложение. С другой стороны, полное использование ИТ не возможно в контексте реального проекта, поскольку это будет стоить слишком дорого. Следствием является то, что оба дополняют друг друга.

Знание хорошего соотношения IT & UT является частью успеха разработчика. Это зависит от многих факторов, включая некоторые, не относящиеся к чисто техническим аспектам, такие как навыки и опыт команды, срок службы приложений и так далее.

Отказоустойчивые тесты

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

Крайне маловероятно, что ИТ будет успешным, а UT провалится. Поэтому, чтобы иметь самый короткий цикл обратной связи в случае сбоя, рекомендуется сначала выполнить самые быстрые тесты, проверить, не прошли ли они, а затем выполнить только самые медленные.

Maven предоставляет способ добиться этого с помощью следующей комбинации плагинов:

  1. maven-surefire-pluginвыполнить юнит-тесты; Конвенция заключается в том, что их имена заканчиваютсяTest
  2. maven-failsafe-pluginвыполнять интеграционные тесты; имена заканчиваютсяIT

По умолчанию первый выполняется на testэтапе жизненного цикла сборки Maven. К сожалению, настройка последнего обязательна:

<project>
  ...
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>2.14.1</version>
        <executions>
          <execution>
            <goals>
              <goal>integration-test</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

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

Таким образом, UT, чей шаблон имени является *Test, выполняются на testэтапе, в то время как IT, чей шаблон имеет имя *IT, выполняется на integration-testэтапе.

Границы тестируемой системы

Первым шагом перед написанием IT является определение границ SUT. Внутри лежит все, что мы можем, за пределами всего остального. Например, база данных явно принадлежит SUT, потому что мы можем легко предоставить тестовые данные, а веб-сервис — нет.

Внутри SUT подсистемы проверки или инициализации зависят от необходимого / требуемого уровня интеграции:

  1. Чтобы использовать UT, можно издеваться над DAO в классе обслуживания.
  2. В качестве альтернативы можно инициализировать тестовую базу данных и использовать данные реального мира. DBUnit является платформой для использования в этом случае, предоставляя способы инициализации данных во время настройки и проверки результатов впоследствии.

Выполнение обоих позволило бы протестировать угловые случаи в подходе насмешки (быстрее) и стандартный случай в инициализации (медленнее).

За пределами SUT необходимо принять важное решение в отношении ИТ. Если внешние зависимости в большинстве случаев не работают и стабильны, сборка будет прерываться слишком часто и выдает слишком много красной сельди. Те, которые будут, вероятно, мы отключили очень быстро.

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

В Maven этого легко достичь, создав модуль в родительском корне и не ссылаясь на него в родительском POM. Таким образом, при запуске mvn integration-testбудут запускаться только те тесты, которые находятся в стандартных модулях и достаточно надежны.

Хрупкие тесты

Как видно выше, хрупкость в ИТ происходит из-за зависимости от внешних систем. Эти хрупкие тесты лучше всего обрабатывать как отдельный модуль вне стандартного процесса сборки.

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

Таким образом, ИТ-отделы, использующие GUI, будь то специализированные тесты GUI или сквозные тесты, также должны считаться хрупкими и изолированными в выделенном модуле, как указано выше.

Мой личный опыт в сочетании с некоторыми работами Гойко Адзича научил меня обходить уровень графического интерфейса и начинать свои тесты на уровне сервлетов. Независимо от того, используете ли вы  Spring Test, вы получите множество подделок, касающихся Servlet API.

Заказ тестов

Разработчики, незнакомые с ИТ, вероятно, должны быть более чем поражены этим заголовком раздела. Фактически, поскольку UT должен быть независимым от контекста и не быть упорядоченным в любом случае.

Для ИТ все немного по-другому: мне нравится определять некоторые UT как истории пользователей. Например, пользователь входит в систему, затем выполняет одно действие, а затем другое. Эти шаги, конечно, могут быть определены в том же методе испытаний.

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

TestNG — ответвление JUnit, полностью интегрированное в Maven и Spring, давайте сделаем это легко:

public class MyScenarioIT {
 
    @Test
    public void userLogsIn() {
        ...
    }
    @Test(dependsOnMethod = "userLogsIn")
    public void someAction() {
        ...
    }
    @Test(dependsOnMethod = "someAction")
    public void anotherAction() {
        ...
    }
}

Таким образом, при возникновении ошибки или сбое подтверждения в журнале отображается неисправный метод: нам просто нужно иметь подходящее имя для каждого метода.

Рамочная специфика UT

Java EE в контейнере UT

До недавнего времени автоматизированное UT выполнялось независимо от какого-либо контейнера. Для приложений Spring это было не сложно, но для приложений с интенсивным использованием Java EE приходилось подделывать и имитировать зависимости. В конце концов, не было никакой гарантии, что приложение в контейнере действительно даст ожидаемые результаты.

Arquillian привел к смене парадигмы благодаря возможности создавать UT для приложений, использующих Java EE. Если вы еще не используете его, знайте, что Arquillian позволит вам автоматизировать создание нужного архива и его развертывание на одном или нескольких настроенных серверах приложений.

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

Весна UT

Spring Test предоставляет возможность собирать различные фрагменты конфигурации Spring (классы XML или Java). Это означает, что, проектируя наше приложение с достаточной модульностью, мы можем использовать нужные фрагменты конфигурации, готовые для производства и только для тестирования.

Например, разделив наш источник данных и DAO на два разных фрагмента, мы можем повторно использовать обычный фрагмент DAO в UT и использовать тестовый фрагмент, объявляющий базу данных в памяти. В случае с Maven это означает наличие первого src/main/resourcesи второго src/test/resources.

@ContextConfiguration(locations = {"classpath:datasource.xml", "classpath:dao.xml"})
public class MyCustomUT extends AbstractTestNGSpringContextTests {
 
    ...
}

Вывод

Это большая картина относительно моего личного опыта относительно UT. Как всегда, они не должны рассматриваться как жесткие и быстрые правила, а должны быть адаптированы к конкретному контексту вашего проекта.

Однако инструменты, перечисленные в статье, должны быть реальным активом во всех случаях.