Правильное внедрение модулей Java в сборке Maven с межмодульными тестовыми зависимостями

У меня есть многомодульный проект с использованием Maven и Java. Сейчас я пытаюсь перейти на Java 9/10/11 и реализовать модули (как в JSR 376: Java Platform Module System, JPMS). Поскольку проект уже состоял из модулей Maven, а зависимости были прямыми, создание дескрипторов модулей для проекта было довольно простым.

Каждый модуль Maven теперь имеет свой собственный дескриптор модуля (module-info.java) в папке src/main/java. Для тестовых классов нет дескриптора модуля.

Однако я наткнулся на проблему, которую не смог решить, и не нашел описания того, как ее решить:

Как я могу иметь межмодульные тестовые зависимости с модулями Maven и Java?

В моем случае у меня есть "общий" модуль Maven, который содержит некоторые интерфейсы и/или абстрактные классы (но без конкретной реализации). В том же модуле Maven у меня есть абстрактные тесты, чтобы гарантировать правильное поведение для реализации этих интерфейсов/абстрактных классов. Затем есть один или несколько подмодулей с реализациями интерфейса/абстрактного класса и тестами, расширяющими абстрактный тест.

Тем не менее, при попытке выполнить test фазу сборки Maven, подмодуль завершится неудачно с:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.0:testCompile (default-testCompile) on project my-impl-module: Compilation failure: Compilation failure:
[ERROR] C:\projects\com.example\my-module-test\my-impl-module\src\test\java\com\example\impl\FooImplTest.java:[4,25] error: cannot find symbol
[ERROR]   symbol:   class FooAbstractTest
[ERROR]   location: package com.example.common

Я подозреваю, что это происходит потому, что тесты не являются частью модуля. И даже если Maven совершает "магию" для выполнения тестов в рамках модуля, он не работает для тестов в модуле, от которого я зависел (по какой-то причине). Как это исправить?

Структура проекта выглядит следующим образом (полные файлы демонстрационных проектов доступны здесь):

├───my-common-module
│   ├───pom.xml
│   └───src
│       ├───main
│       │   └───java
│       │       ├───com
│       │       │   └───example
│       │       │       └───common
│       │       │           ├───AbstractFoo.java (abstract, implements Foo)
│       │       │           └───Foo.java (interface)
│       │       └───module-info.java (my.common.module: exports com.example.common)
│       └───test
│           └───java
│               └───com
│                   └───example
│                       └───common
│                           └───FooAbstractTest.java (abstract class, tests Foo)
├───my-impl-module
│   ├───pom.xml
│   └───src
│       ├───main
│       │   └───java
│       │       ├───com
│       │       │   └───example
│       │       │       └───impl
│       │       │           └───FooImpl.java (extends AbstractFoo)
│       │       └───module-info.java (my.impl.module: requires my.common.module)
│       └───test
│           └───java
│               └───com
│                   └───example
│                       └───impl
│                           └───FooImplTest.java (extends FooAbstractTest)
└───pom.xml

Зависимости в my-impl-module/pom.xml следующие:

<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>my-common-module</artifactId>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>my-common-module</artifactId>
        <classifier>tests</classifier> <!-- tried type:test-jar instead, same error -->
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

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

PS: я не думаю, что что-то не так с самим кодом, так как все компилируется и выполняется с использованием обычного пути к классам (то есть в IntelliJ или Maven без дескрипторов модулей Java). Проблема вводится с модулями Java и путем к модулю.

Ответ 1

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

  1. Я добавил maven-compiler-plugin version 3.8.0 для всех модулей. Вам нужна версия 3.7 или выше для компиляции модулей с Maven - по крайней мере, как показало предупреждение NetBeans. Поскольку вреда нет, я добавил плагин к файлам POM как общего модуля, так и модуля реализации:

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.0</version>
        <executions>
            <execution>
                <goals>
                    <goal>compile</goal>
                </goals>
                <id>compile</id>
            </execution>
        </executions>
    </plugin> 
    
  2. Я экспортировал тестовые классы в их собственный jar файл, чтобы они были доступны вашему модулю реализации или кому-либо в этом отношении. Для этого вам нужно добавить следующее в ваш файл my-common-module/pom.xml:

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.1.0</version>
        <executions>
            <execution>
                <id>test-jar</id>
                <phase>package</phase>
                <goals>
                    <goal>test-jar</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
    

    Это позволит экспортировать тестовые классы my-common-module в файл -tests.jar т.е. my-common-module-1.0-SNAPSHOT-tests.jar. Обратите внимание, что нет необходимости добавлять выполнение для обычного файла jar как отмечено в этом посте. Это, однако, будет связано с ошибкой, о которой я расскажу позже.

  3. Переименуйте ваш тестовый пакет в my-common-module на com.example.common.test чтобы тестовые классы загружались при компиляции тестового класса (классов) реализации. Это исправляет проблему загрузки классов, возникшую, когда мы экспортировали тестовые классы с тем же именем пакета, что и в модуле, где загружается первый jar, в данном случае модуль, а второй jar, файл тестового jar, игнорируется. Интересно, что на основании наблюдения я заключаю, что путь к модулю имеет более высокий приоритет, чем путь к классу, поскольку параметры компиляции Maven показывают tests.jar который указан первым в пути к классу. mvn clean validate test -X, мы видим параметры компиляции:

    -d /home/testenv/NetBeansProjects/MavenProject/Implementation/target/test-classes -classpath /home/testenv/NetBeansProjects/MavenProject/Implementation/target/test-classes:/home/testenv/.m2/repository/com/example/Declaration/1.0-SNAPSHOT/Declaration-1.0-SNAPSHOT-tests.jar:/home/testenv/.m2/repository/junit/junit/4.12/junit-4.12.jar:/home/testenv/.m2/repository/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar: --module-path /home/testenv/NetBeansProjects/MavenProject/Implementation/target/classes:/home/testenv/.m2/repository/com/example/Declaration/1.0-SNAPSHOT/Declaration-1.0-SNAPSHOT.jar: -sourcepath /home/testenv/NetBeansProjects/MavenProject/Implementation/src/test/java:/home/testenv/NetBeansProjects/MavenProject/Implementation/target/generated-test-sources/test-annotations: -s /home/testenv/NetBeansProjects/MavenProject/Implementation/target/generated-test-sources/test-annotations -g -nowarn -target 11 -source 11 -encoding UTF-8 --patch-module example.implementation=/home/testenv/NetBeansProjects/MavenProject/Implementation/target/classes:/home/testenv/NetBeansProjects/MavenProject/Implementation/src/test/java:/home/testenv/NetBeansProjects/MavenProject/Implementation/target/generated-test-sources/test-annotations: --add-reads example.implementation=ALL-UNNAMED
    
  4. Нам нужно сделать экспортированные тестовые классы доступными для модуля реализации. Добавьте эту зависимость в ваш my-impl-module/pom.xml:

    <dependency>
        <groupId>com.example</groupId>
        <artifactId>Declaration</artifactId>
        <version>1.0-SNAPSHOT</version>
        <type>test-jar</type>
        <scope>test</scope>
    </dependency>
    
  5. Наконец, в тестовом классе my-impl-module обновите импорт, чтобы указать новый тестовый пакет com.example.common.text, чтобы получить доступ к тестовым классам my-common-module:

    import com.example.declaration.test.AbstractFooTest;
    import com.example.declaration.Foo;
    import org.junit.Test;
    import static org.junit.Assert.*;
    
    /**
     * Test class inheriting from common module...
     */
    public class FooImplementationTest extends AbstractFooTest { ... }
    

Вот результаты теста из моего mvn clean package с новыми изменениями:

enter image description here

Я обновил мой пример кода в моем репозитории GitHub для тестирования java-cross-module-testing. Единственный затяжной вопрос, который у меня есть, и я уверен, что вы тоже это делаете, заключается в том, почему это сработало, когда я определил модуль реализации как обычный jar проект вместо модуля. Но я поиграю с другим днем. Надеюсь, то, что я предоставил, решит вашу проблему.

Ответ 2

Я попытался сделать то же самое, невозможно иметь тесты whitebox и зависимости модуля test от структуры вашего проекта, но я думаю, что нашел альтернативную структуру, которая выполняет 90% того, что вы хотите сделать:

1/Проблема тестирования белого ящика заключается в том, что он работает с исправлениями модулей, поскольку в JPMS нет понятия основной тестовой VS в отличие от Maven. Таким образом, это провоцирует проблемы, такие как отсутствие работы с тестовыми зависимостями или необходимость загрязнения информации вашего модуля тестовыми зависимостями.

2/Итак, почему бы не продолжать проводить тестирование белого ящика, но с maven структурой тестирования черного ящика, то есть разделить каждый модуль X на X и X-test. Только X имеет module-info.java, тесты выполняются в пути к классам, поэтому вы пропустите все эти проблемы.

Единственные недостатки, о которых я могу подумать (в порядке возрастания важности):

  1. Что тестовая банка не будет модульной, но я думаю, что это приемлемо (по крайней мере, пока).
  2. Что у вас вдвое больше модулей maven, и вам может не понравиться разделять тесты в другом модуле maven.
  3. Тесты будут выполняться в classpath, и всегда плохо запускать тесты в другой среде (тесты будут выполняться с меньшими ограничениями, чем основной код). Это может быть смягчено тестами дыма или специальным модулем интеграционных тестов? "Официальные образцы" еще не появились.

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

Вы можете найти пример здесь:

a/Получите пример:

git clone https://github.com/vandekeiser/ddd-metamodel.git

git checkout stackoverflow

б/Посмотрите на пример:

  1. fr.cla.ddd.metamodel зависит от fr.cla.ddd.oo (но нет информации о модуле для тестовых модулей maven)
  2. Я делаю тестирование whitebox в PackagePrivateOoTest и PackagePrivateMetamodelTest
  3. У меня есть тест-зависимость OoTestDependency