在具有模块间测试依赖性的 Maven 构建中正确实施 Java 个模块

Properly implementing Java modules in a Maven build with inter-module test dependencies

我有一个使用 Maven 和 Java 的多模块项目。我现在正尝试迁移到 Java 9/10/11 并实施模块(如 JSR 376: Java Platform Module System,JPMS)。由于项目已经由 Maven 模块组成,并且依赖关系是直接的,因此为项目创建模块描述符非常简单。

每个 Maven 模块现在在 src/main/java 文件夹中都有自己的模块描述符 (module-info.java)。测试 classes 没有模块描述符。

然而,我偶然发现了一个我无法解决的问题,也没有找到任何关于如何解决的描述:

如何让 Maven 和 Java 模块具有模块间 test 依赖关系?

在我的例子中,我有一个 "common" Maven 模块,其中包含一些接口 and/or 抽象 classes(但没有具体实现)。在同一个 Maven 模块中,我有 abstract 测试以确保这些 interfaces/abstract classes 的实现的正确行为。然后,有一个或多个子模块,具有 interface/abstract class 的实现和扩展抽象测试的测试。

但是,当尝试执行 Maven 构建的 test 阶段时,子模块将失败:

[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 做了一些 "magic" 来让测试在模块范围内执行,它也不适用于我依赖的模块中的测试(出于某种原因)。我该如何解决这个问题?

项目的结构如下所示 (full demo project files available here):

├───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>

注意:以上只是我为了演示问题而创建的工程。真正的项目要复杂很多,而且found here(master分支还没有模块化),但是原理是一样的

PS:我不认为代码本身有什么问题,因为一切都使用正常的 class 路径编译和运行(即在 IntelliJ 中,或者 Maven 没有 Java 模块描述符)。 Java 模块和模块路径引入了问题。

根据您的演示项目,我能够重现您的错误。也就是说,这是我在第一次尝试失败后所做的 修订 更改,以便能够构建项目:

  1. 我将 maven-compiler-plugin 版本 3.8.0 添加到所有模块。您需要 3.7 或更高版本才能使用 Maven 编译模块 - 至少这是 NetBeans 显示的警告。由于没有危害,我将插件添加到 commonimplementation 模块的 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. 我将测试 classes 导出到它们自己的 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 测试 classes 导出到 -tests.jar 文件中 - 即 my-common-module-1.0-SNAPSHOT-tests.jar。请注意,如本 中所述,无需为常规 jar 文件添加执行。但是,这会引入我接下来要解决的错误。

  3. my-common-module 中的测试包重命名为 com.example.common.test 以便在编译实施测试 class 时加载测试 classes (西)。这纠正了 class 加载问题,当我们导出测试 classes 时引入的包名称与第一个 jar(在本例中为模块)加载的模块中的包名称相同,并且第二个 jar,测试 jar 文件,被忽略。有趣的是,我根据观察得出结论,模块路径的优先级高于 class 路径,因为 Maven 编译参数显示 tests.jar 在 class 路径中首先指定. 运行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. 我们需要使导出的测试 classes 可用于实现模块。将此依赖项添加到您的 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 测试 class 中,更新导入以指定新的测试包,com.example.common.text,以访问 my-common-module 测试class是:

    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 新变化的测试结果:

我更新了 java-cross-module-testing GitHub 存储库中的示例代码。我有一个挥之不去的问题,我相信你也有,就是为什么当我将实现模块定义为常规 jar 项目而不是模块时它会起作用。但是,改天我会玩。希望我提供的内容能解决您的问题。

我试图做完全相同的事情,不可能同时拥有白盒测试和模块测试依赖性您的项目结构,但我想我找到了一个替代结构做你想做的 90%:

1/ 白盒测试的问题在于它与模块修补一起工作,因为 JPMS 不像 Maven 那样没有测试 VS 主的概念。所以这会引发一些问题,比如不使用测试依赖项,或者不得不用测试依赖项污染你的模块信息。

2/ 那么,为什么不继续做白盒测试,而是采用黑盒测试的maven结构,即:将每个模块X拆分为X和X-test。 只有 X 在类路径中有模块-info.java、测试运行,因此您可以跳过所有这些问题。

我能想到的唯一缺点是(按重要性递增的顺序):

  1. 测试 jar 不会被模块化,但我认为这是可以接受的(至少目前是这样)。
  2. 你有两倍数量的 maven 模块,你可能不喜欢在另一个 maven 模块中分离测试。
  3. 测试将 运行 在类路径中,在不同的环境中 运行 测试总是不好的(测试将 运行 比主代码的限制更少)。这可以通过冒烟测试或专用集成测试模块来缓解吗? "official patterns"还没出现

顺便说一下(如果那是你正在做的)我怀疑是否值得模块化,例如。 spring 启动应用程序,因为 spring 本身还没有模块化,所以你会付出代价,但收获很少(但如果你正在编写一个库,那就是另一回事了)。

你可以在这里找到一个例子:

a/ 获取示例:

git 克隆 https://github.com/vandekeiser/ddd-metamodel.git

git 结帐 Whosebug

b/ 看例子:

  1. fr.cla.ddd.metamodel 依赖于 fr.cla.ddd.oo(但没有用于测试 maven 模块的模块信息)
  2. 我在 PackagePrivateOoTest 和 PackagePrivateMetamodelTest 中进行白盒测试
  3. 我有一个测试依赖 OoTestDependency