Тестирование maven плагинов

Часть моей хобби инфраструктуры завязана на .deb пакеты и apt репозитории. Чтобы со всем этим работать из java, я написал несколько maven плагинов и выложил в открытый доступ. Один из этих плагинов - deb-maven-plugin. Он позволяет собирать из java проекта .deb пакет. Этот формат пакетов имеет достаточно сложную структуру и большое историческое наследие. Я очень тщательно разобрался с тем, как собирать такой пакет для java проектов и создал плагин, который сильно облегчает сборку. Со временем ко мне приходили различные разработчики и добавляли в этот плагин нужную им функциональность. Мы вместе исправляли ошибки. Всю идиллию портило только одно - полное отсутствие тестов. Каждое изменение, которое мы добавляли могло потенциально сломать уже существующие проекты.

Однажды я решил проверить собранный пакет с помощью утилиты lintian и обнаружил множество критичных ошибок. Это сподвигло меня собраться с мыслями и всё-таки разобраться, как тестируются maven плагины. И поправить ошибки, о которых предупреждал lintian.

Требования

Прежде чем начать писать тесты, необходимо определиться какие: интеграционные или юнит. Юнит-тесты для maven плагинов достаточно бесполезны. Дело в том, что maven плагин - это один большой класс, реализующий AbstractMojo. Иногда полезно иметь несколько вспомогательных классов, чтобы декомпозиторовать задачу. Но в большинстве случаев - это один большой класс, который нужно тестировать интеграционно.

Ещё одну вещь, которую юнит тесты не смогут проверить - то, как конфигурация пробрасывается внутрь плагина. До версии 3.0 каноничным способом указания конфигурации был javadoc определённого формата. Это крайне плохой и неудобный способ задания внешних параметров.

/**
 * @parameter default-value="${project.basedir}/src/main/deb";
 */
private String debBaseDir;

Соответственно необходимо протестировать правильность написания этого javadoc. Без полноценного запуска PlexusContainer это достаточно сложно. PlexusContainer - это DI контейнер, который чем-то похож на Spring и Dagger. Однако, из-за своей малой распространённости он имеет крайне скудную документацию. Определить какие компоненты доступны в контейнере можно только методом проб и ошибок. Основные настройки плагина можно получать из MavenProject:

/**
 * The maven project.
 * 
 * @parameter property="project"
 * @readonly
 */
private MavenProject project;

Такую конфигурацию можно проверить только интеграционными тестами. Нужно честно запустить PlexusContainer и загрузить тестовый pom.xml.

Если начать искать в сети “тестирование maven плагинов”, то можно найти официальную документацию. В ней как раз описаны два способа:

  1. Интеграционные тесты с помощью плагина maven-plugin-testing-harness
  2. Функциональные тесты с помощью maven-verifier

Мне идеально подходил первый способ. Но для полноты картины я решил разобраться в чём же заключается второй способ. По своей сути maven-verifier ищет инсталляцию maven в системном свойстве MVN_HOME и запускает в отдельном процессе mvn <some plugin>. После этого проверяется, что процесс завершился без ошибки, что нет логов с ошибками и тому подобное. Этот способ идеально подходит для тестирования плагинов с разными версиями maven. В моём же случае хотелось бы запускать maven в одной и той же JVM, что и юнит тесты. Это быстро и позволяет оценить покрытие кода тестами.

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

Свои тесты я предполагал сделать следующим образом:

  • создать конфигурацию проекта success, в которой использованы всевозможные настройки плагина
  • написать тест для конфигурации success
  • для того, чтобы протестировать опции по-умолчанию или граничные значения, необходимо взять конфигурацию success и изменить (перезаписать) параметры

В результате у меня получился следующий тест:

@Rule
public MojoRule mrule = new MojoRule();

@Rule
public TemporaryFolder folder = new TemporaryFolder();

@Test
public void testSuccess() throws Exception {
	File basedir = new File("src/test/resources/success");
	MavenProject mavenProject = mrule.readMavenProject(basedir); // загрузка конфигурации проекта из pom.xml
	mavenProject.getBuild().setDirectory(folder.getRoot().getAbsolutePath());
	Mojo mm = mrule.lookupConfiguredMojo(mavenProject, "package"); // загрузка и конфигурирования Mojo для тестирования
	mm.execute();
	assertEquals(1, mavenProject.getAttachedArtifacts().size()); // получение .deb пакета
	Artifact artifact = mavenProject.getAttachedArtifacts().get(0); 
	assertDeb(new File("src/test/resources/expected/success"), artifact.getFile(), artifact.getClassifier()); // сравнение директории с ожидаемыми файлами и содержимого .deb пакета
}
  • MojoRule - это вспомогательный класс для удобной работы с PlexusContainer. В нём есть множество удобных методов для загрузки плагина из pom.xml и инициализации вспомогательных классов.
  • TemporaryFolder - это правило для junit, которое создаёт временную папку вначале теста и удаляет её в конце.

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

Самым сложным и одновременно долгим и неприятным оказалась реализация метода assertDeb. По сути этот метод должен рекурсивно сравнивать директории в ожидаемой папке и внутренности .deb пакета.

После того, как я написал этот тест, обнаружилось множество ошибок в тестируемом коде. В частности структура .tar архива, который входит в .deb пакет, создавалась неправильно. По стандарту все вложенные директории должны присутствовать в .tar файле. Например, для файла usr/share/doc/copyright должны быть следующие записи в .tar:

usr/
usr/share/
usr/share/doc/
usr/share/doc/copyright

Я же ошибочно создавал только:

usr/share/doc/copyright

Выводы

Писать тесты для maven плагинов можно. Однако, это требует достаточно высокий порог вхождения и глубокого понимания того, как работает maven. Нужно быть как минимум готовым к тому, что придётся заглянуть в исходники. Результат же стоит того. Автоматические тесты позволяют спокойно редактировать код плагина без опаски сломать обратную совместимость с сотнями других проектов, которые его используют. Причём редактирование включает в себя улучшение производительности. А представьте, что 15% улучшение сможет улучшить тысячи сборок сотен проектов на 15%!