在Maven构建中正确实现Java模块及其间的测试依赖

34
我有一个使用Maven和Java的多模块项目。现在,我正在尝试迁移到Java 9/10/11并实现模块(如JSR 376: Java Platform Module System,JPMS)。由于该项目已经由Maven模块组成,并且依赖关系很简单,因此为项目创建模块描述符相当直接。
现在,每个Maven模块都有自己的模块描述符(module-info.java),位于src/main/java文件夹中。测试类没有模块描述符。
然而,我遇到了一个问题,我一直无法解决,并且没有找到任何解决方法:
如何使用Maven和Java模块进行跨模块测试依赖?
在我的情况下,我有一个“common” Maven模块,其中包含一些接口和/或抽象类(但没有具体实现)。在同一个Maven模块中,我有抽象测试来确保这些接口/抽象类的实现行为正确。然后,有一个或多个子模块,其中包含接口/抽象类的实现以及扩展抽象测试的测试。
然而,在尝试执行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进行了某些“魔法”将测试执行在模块的范围内,它也无法解决我所依赖的模块中的测试问题(由于某种原因)。如何解决这个问题?
项目的结构如下(完整演示项目文件在此处可用):
├───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中或不带Java模块描述符的Maven中)编译和运行一切正常。问题是由Java模块和模块路径引入的。

1
毫无疑问,这些问题会引起人们的关注,对他们来说一个很好的阅读材料是Sormuras的这个答案,其中链接到各种有用的链接,可以帮助您在处理模块化项目时对测试进行分类。 - Naman
1
这个答案对你有用吗?简要概括一下,将 goal test-jar 添加到你的 my-common-module 中,然后在 my-impl-module/pom.xml 的 my-common-module 依赖中将 <classifier>tests</classifier> 改为 <type>test-jar</type>。我已经尝试过了,它可以正常工作。 - troig
@troig 谢谢。您尝试使用JPMS模块吗?我在使用“test-jar”依赖时遇到了完全相同的问题。 - Harald K
1
@haraldK,我只在一个多模块的Maven项目中尝试过,但没有使用JPMS。对此很抱歉。我会等待其他答案,这是个好问题! - troig
@nullpointer 很有趣的阅读,但我并没有在那里找到解决我的问题的方法... 你有什么建议吗?显然我正在进行白盒测试。 - Harald K
@haraldK 或许相关,也许尝试这个可能会有所帮助 Java9 多模块 Maven 项目测试依赖。 实际上在这方面没有更多建议。受到两个限制 - 时间 不给它实际的操作 && 迁移 目前正处于我主要投入时间的地方。 :| - Naman
2个回答

19

根据您的演示项目,我成功重现了您的错误。因此,在第一次失败的尝试之后,以下是我所做的修改,以便能够构建该项目:

  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. 我将测试类导出到了它们自己的jar文件中,这样它们就可以供你的实现模块或任何其他人使用。为此,你需要在my-common-module/pom.xml文件中添加以下内容:

  3. <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文件中不需要添加执行操作。然而,这会引入错误,我将在下一步中解决。

  4. my-common-module中的测试包重命名为com.example.common.test,以便在编译实现测试类时加载测试类。这样就可以纠正由于我们将测试类与模块中具有相同包名称的测试类导出时引入的类加载问题。有趣的是,根据观察结果,我得出的结论是,模块路径比类路径具有更高的优先级,因为Maven编译参数显示tests.jar首先在类路径中指定。运行mvn clean validate test -X,我们可以看到编译参数:

  5. -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
    
  6. 我们需要将导出的测试类让实现模块可用。请将以下依赖项添加到您的my-impl-module/pom.xml文件中:

  7. <dependency>
        <groupId>com.example</groupId>
        <artifactId>Declaration</artifactId>
        <version>1.0-SNAPSHOT</version>
        <type>test-jar</type>
        <scope>test</scope>
    </dependency>
    
  8. 最后,在my-impl-module测试类中,更新导入以指定新的测试包com.example.common.text,以访问my-common-module测试类:

  9. 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项目时为什么它能正常工作,但那是另一天我会处理的事情。希望我提供的内容解决了您的问题。


1
感谢您尝试解决此问题,但似乎这并没有解决HaraldK描述的确切问题。它并没有充分利用JPMS:您的示例项目没有针对"Implementation"的module-info.java。当我添加适当的Implementation/src/main/java/module-info.java时,它会失败并显示:"找不到模块:com.example.declaration"。 - Coder Nr 23
好的.. 经过一段时间的思考,似乎问题出在 module-info.java模块名称 上... :-( 如果公共模块命名为 my.common.module(与 Maven 模块 my-common-module 相同,只是用点号代替连字符),则会出现编译错误(-X 显示在这种情况下 *-test.jar 甚至不在类路径上)。如果模块名称为 common.module(或几乎任何其他名称),则一切正常。这是 Maven 的 bug 吗? - Harald K
1
好的,总之:只有当测试位于不同的包中时,您才能使测试jar依赖项正常工作。对于复杂的“白盒测试”情况,这将不是一个好的解决方案。但对于大多数其他情况(包括我的情况),那可能是一个可以接受的解决方法。 - Coder Nr 23
顺便说一下,我也深入研究了一下。据我所知,Maven Surefire Plugin 编译并运行测试。它需要进行大量的调整才能与 JPMS 兼容。在这种情况下,为了使其正常工作,它必须将 maintest 类合并到一个 jar 文件中。但这有点破坏了单独的主 jar 和“test-jar”背后的整个逻辑。所以我想 surefire 没有明显的方法可以使其正常工作。 - Coder Nr 23
1
@haraldK,你说得对!在运行测试时没有添加“-tests-jar”文件。我尝试了使用句点和连字符,但结果相同。我的直觉是Maven找不到该文件,因为jar文件名中的那些“特殊”字符在查找字符串“-tests-jar”时会破坏解析逻辑。当我从头开始创建一个新项目时,我遇到了相同的错误-以防我在重命名Maven模块时搞砸了。这绝对是Maven的一个bug。 - Jose Henriquez
显示剩余10条评论

1
我尝试做完全相同的事情,但是使用您的项目结构不可能同时进行白盒测试和模块测试依赖项。但我认为我找到了一种替代结构,可以实现您想要做的90%的内容:
1/ 白盒测试的问题在于它使用模块修补,因为JPMS没有像Maven那样的测试VS主要概念。这会导致问题,例如无法使用测试依赖项,或必须使用测试依赖项污染您的module-info。
2/ 那么,为什么不继续使用黑盒测试的maven结构进行白盒测试呢?即:将每个模块X拆分为X和X-test。只有X具有module-info.java,测试在类路径中运行,因此您可以跳过所有这些问题。
我能想到的唯一缺点是(按重要性递增的顺序):
  1. 测试jar包不会进行模块化,但我认为这是可以接受的(至少现在是这样)。
  2. 你将有两倍数量的Maven模块,并且你可能不喜欢在另一个Maven模块中分离测试。
  3. 测试将在类路径中运行,而在不同环境中运行测试总是不好的(测试将比主代码运行时具有更少的限制)。这可能可以通过冒烟测试或专用集成测试模块来缓解?“官方模式”尚未出现。

顺便说一句(如果您正在这样做),我怀疑对于像Spring Boot应用程序这样的模块化并不值得,因为Spring本身还没有进行模块化,所以您将付出代价但收获很少的好处(但如果您编写的是库,则情况不同)。

您可以在此处找到示例:

a/ 获取示例:

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

git checkout stackoverflow

b/ 查看示例:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接