Java 9 + Maven + JUnit:测试代码需要自己的module-info.java文件吗?应该放在哪里?

66

假设我有一个使用Maven 3和junit的Java项目。有src/main/javasrc/test/java目录,分别包含主要源代码和测试源代码(所有内容都是标准的)。

现在我想将项目迁移到Java 9。 src/main/java目录下的内容表示Java 9模块; 存在com/acme/project/module-info.java文件,大致如下:

module com.acme.project {
    require module1;
    require module2;
    ...
}
如果测试代码需要自己的module-info.java怎么办?例如,为了添加对一些仅在测试中需要而不是在生产代码中需要的模块的依赖关系。在这种情况下,我必须将module-info.java放到src/test/java/com/acme/project/中,并给模块命名一个不同的名称。这样,Maven似乎将主要资源和测试资源视为不同的模块,因此我必须将包从主模块导出到测试模块中,并在测试模块中要求包,类似于这样: 主模块(位于src/main/java/com/acme/project):
module prod.module {
    exports com.acme.project to test.module;
}

测试模块(在src/test/java/com/acme/project中):

module test.module {
    requires junit;
    requires prod.module;
}

这会产生

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.7.0:testCompile (default-testCompile) on project test-java9-modules-junit: Compilation failure: Compilation failure:
[ERROR] /home/rpuch/git/my/test-java9-modules-junit/src/test/java/com/acme/project/GreeterTest.java:[1,1] package exists in another module: prod.module

因为一个包在两个模块中被定义了。所以现在我必须在主模块和测试模块中拥有不同的项目,这不太方便。

我感觉我走错了路,一切开始变得非常丑陋。我该如何在测试代码中拥有自己的module-info.java,或者如何在没有它的情况下实现相同的效果(require等)?


首先忘记Maven 2...使用Maven 3+...在测试区域中使用module-info在我看来没有意义?这背后有特殊的要求/成就吗? - khmarbaise
1
当然是 Maven 3。 - Roman Puchkovskiy
7个回答

21

模块系统不区分生产代码和测试代码,因此如果您选择将测试代码模块化,则prod.moduletest.module不能共享相同的包com.acme.project ,如规范所述:

非干扰性 - Java编译器、虚拟机和运行时系统必须确保包含相同名称包的模块不会互相干扰。如果两个不同的模块包含相同名称的包,则从每个模块的角度来看,该包中的所有类型和成员仅由该模块定义。一个模块中该包中的代码不得能够访问另一个模块中该包中的package-private类型或成员。

正如Alan Bateman指出的那样,Maven编译插件在编译src/test/java目录下的代码时使用了--patch-module和其他选项,使被测试的模块增加了测试类。Surefire插件在运行测试类时也会进行同样的操作(参见支持在命名的Java 9模块中运行单元测试)。这意味着您不需要将测试代码放在一个模块中。


9
将软件包更改为测试的缺点是您的测试将无法访问默认和受保护修饰符。 - Absurd-Mind
3
该模块提供了--patch-module和其他选项,以支持编译和执行与测试模块在同一包/模块中的测试。当编译src/test目录下的代码时,maven-compiler-plugin会使用这些选项。surefire-plugin也是如此。 - Alan Bateman
@AlanBateman 谢谢您提供这个信息。我不知道 Maven 会这样做。我会用您的信息更新答案。 - M A
2
@nullpointer 看起来已经添加在 https://issues.apache.org/jira/browse/SUREFIRE-1420 中了。 - M A
1
在我看来,这是非常正确的答案。gradle 采用了相同的模式 - Eugene
显示剩余2条评论

7

您可能需要重新考虑您正在尝试实现的项目设计。由于您正在将一个模块及其测试实现到一个项目中,因此不应针对每个模块单独使用不同的模块。

一个模块及其相应的测试只应有一个单一的module-info.java

您的相关项目结构可能如下所示:

Project/
|-- pom.xml/
|
|-- src/
|   |-- test/
|   |   |-- com.acme.project
|   |   |        |-- com/acme/project
|   |   |        |      |-- SomeTest.java
|   |   
|   |-- main/
|   |   |-- com.acme.project
|   |   |    |-- module-info.java
|   |   |    |-- com/acme/project
|   |   |    |    |-- Main.java

其中module-info.java可以进一步解释为:

module com.acme.project {
    requires module1;
    requires module2;
    // requires junit; not required using Maven
}

根据您的问题,总结如下:

我觉得我走错了路,它开始变得非常丑陋。我该如何在测试代码中拥有自己的module-info.java,或者如何在没有它的情况下实现相同的效果(require等)?

是的,您不应该考虑为测试代码管理不同的模块以使其更加复杂。

您可以通过将junit视为编译时依赖项,并使用以下指令来实现类似的效果-

requires static junit;

使用Maven,您可以按照上述结构,并使用 maven-surefire-plugin 使测试自动运行。这个插件会自动将测试补丁到模块中。

1
我建议不要在目录结构中使用模块名称,因为这样做没有任何优势....? - khmarbaise
@khmarbaise 我相信你的意思是 com.acme.project/com/acme/project。我只是按照快速入门指南进行操作。虽然我同意,这并没有提供任何优势。 - Naman
3
在模块描述符中要求使用“junit”对我来说看起来不太好。 - ZhekaKozlov
Intellij IDEA需要不同的目录结构,似乎不允许测试或主目录。请参考此链接:https://www.jetbrains.com/help/idea/getting-started-with-java-9-module-system.html。 - JMess
1
这个单一“模块”的建议有时甚至是不可能的。这里有一个例子 - Eugene
显示剩余4条评论

4

还需注意,maven-surefire-plugin现在有一个配置选项useModulePath false

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>3.0.0-M6</version>
  <configuration>
    <useModulePath>false</useModulePath>  <!-- tests use classpath -->
  </configuration>
</plugin>

这是一种选项,在该项目中,主要使用模块路径(module-path),但在测试和测试中使用类路径(classpath)。如果"修补"模块路径变得繁琐,采用这种方法可能不是一个坏选择。

编辑:我们也可以通过属性surefire.useModulePath进行设置,例如:

<properties>
  <surefire.useModulePath>false</surefire.useModulePath>
</properties>

3

添加一些细节。

自Java 9以来,可以将jar文件(或带有类的目录)放在类路径上(与早期版本相同),也可以放在模块路径上。如果将其添加到类路径,则会忽略其module-info,并且不会应用任何模块相关的限制(例如读取什么,导出什么等)。但是,如果将jar添加到模块路径中,则将其视为模块,因此将处理其module-info,并强制执行其他模块相关的限制。

当前(版本2.20.1),maven-surefire-plugin只能以旧方式工作,因此它将被测试的类放在类路径上,而模块路径则被忽略。因此,现在将module-info添加到Maven项目中不应更改使用Maven(使用surefire插件)运行的测试。

在我的情况下,命令行如下:

/bin/sh -c cd /home/rpuch/git/my/test-java9-modules-junit && /home/rpuch/soft/jdk-9/bin/java --add-modules java.se.ee -jar /home/rpuch/git/my/test-java9-modules-junit/target/surefire/surefirebooter852849097737067355.jar /home/rpuch/git/my/test-java9-modules-junit/target/surefire 2017-10-12T23-09-21_577-jvmRun1 surefire8407763413259855828tmp surefire_05575863484264768860tmp

被测试的类没有作为模块添加,因此它们在类路径上。

目前正在进行一项工作https://issues.apache.org/jira/browse/SUREFIRE-1262(SUREFIRE-1420被标记为SUREFIRE-1262的重复),教育surefire插件将测试代码放置在模块路径下。当它完成并发布后,将会考虑使用module-info。但是如果他们自动使被测试的模块读取junit模块(如SUREFIRE-1420所建议的),则模块信息(主模块描述符)将不必包含对junit的引用(仅测试需要)。

简而言之:

  1. 只需将module-info添加到主要源代码中
  2. 目前,surefire忽略了新的模块相关逻辑(但这将来会改变)
  3. (当模块在surefire测试下工作时) junit可能不需要添加到module-info中
  4. (当模块在surefire测试下工作时) 如果某个模块仅被测试所需,则可以将其作为编译依赖项添加(使用require static),如@nullpointer建议。在这种情况下,Maven模块必须依赖于使用编译(而不是测试)范围提供该仅限于测试的模块的构件。

3

我想在这里加上我的0.02美元,就一般测试方法而言,因为似乎没有人提到gradle,而我们正在使用它。

首先,需要让gradle知道有哪些模块。这相当简单,可以通过以下方式实现(自从gradle-7版本以后):

plugins.withType(JavaPlugin).configureEach {
    java {
        modularity.inferModulePath = true
    }
}

如果您需要测试代码,gradle 如下所述:

如果您的测试源集(src/test/java)中没有module-info.java文件,则此源集在编译和测试期间将被视为传统的Java库。

简单来说,如果您不定义用于测试目的的module-info.java,则事情“只是工作”,在大多数情况下,这正是我们想要的。


但这并不是故事的结尾。如果我确实想要通过ServiceLocator定义一个JUnit5扩展,那该怎么办呢?这意味着我需要从测试中进入module-info.java,而我还没有这个文件。

Gradle再次解决了这个问题:

另一种白盒测试的方法是将测试修补到正在测试的模块中,这样模块边界仍然存在,但测试本身成为正在测试的模块的一部分,然后可以访问该模块的内部。

因此,在src/test/java中定义一个module-info.java,可以在其中放置以下内容:

 provides org.junit.jupiter.api.extension.Extension with zero.x.extensions.ForAllExtension;

我们还需要执行--patch-module命令,就像Maven插件那样。它的格式如下:

def moduleName = "zero.x"
def patchArgs = ["--patch-module", "$moduleName=${tasks.compileJava.destinationDirectory.asFile.get().path}"]
tasks.compileTestJava {
    options.compilerArgs += patchArgs
}
tasks.test {
    jvmArgs += patchArgs
}

唯一的问题是,intellij 没有看到此补丁,并认为我们还需要一个 requires 指示符 (requires zero.x.services),但实际上并不需要。所有测试从命令行和 intellij 中都可以正常运行。
示例在这里

谢谢分享。我在Gradle方面还是个新手,只有基本的了解,可以将一些概念与Maven相关联。我理解你回答中的“this will be 'on' since gradle-7”是指Gradle将构建能力以识别Java模块路径与类路径意识,并自行选择代码所在位置。作为用户,在测试插件中指定Java源和目标应该就足够了,这是从我的角度来看的。 - Naman
但是,像 provides org.junit.jupiter.api.extension.Extension with __; 这样的东西似乎已经过时了。老实说,我需要研究一下 Junit5 在定义扩展和与 Java 模块系统对齐方面的建议。(我想现在可以回头看看Sormuras在这里的一些答案,特别是这个!) - Naman

0

我无法使它与最新的Maven surefire插件版本(3.0.0-M5)一起工作。似乎如果主要源使用模块,则在使用Java 11时,编译器插件也期望引用的包在模块中。

我的解决方案是在测试源代码(Maven中的src/test/java)中放置一个自己的module-info.java文件,其内容如下。 在我的情况下,我必须使用关键字open(请参见允许运行时访问模块中的所有包),因为我在测试中使用了Mockito,这需要反射访问。

// the same module name like for the main module can be used, so the main module has also the name "com.foo.bar"
open module com.foo.bar {
// I use junit4
    requires junit;
// require Mockito here
    requires org.mockito;
// very important, Mockito needs it
    requires net.bytebuddy;
// add here your stuff
    requires org.bouncycastle.provider;
}

尽可能简单明了:模块和测试不应该使用相同的包名,例如:模块包名:com.acme.project 测试包名可以是:test.com.acme.project - undefined
1
除了简洁性之外,将测试代码放在同一个包(和Maven项目)中的最大优势是能够访问非导出/非公开的代码。请参阅我上面的答案,了解不需要单独模块的解决方案。 - undefined

0
你不需要一个额外的module-info.java文件,你可以在同一个文件中指定所有内容:
对于仅在测试中需要的任何依赖项,
1. 在项目的module-info.java文件(位于src/main/java下)中添加一个条目requires static <module-name> 2. 将Maven依赖范围(在pom.xml中)设置为<scope>provided</scope> 为什么这样做可以? require static告诉编译器这些依赖项在运行时不是必需的。它们也不是传递性的,因此不会出现在其他项目中。 <scope>provided</scope>类似于compile+runtime+test,但不是传递性的,因此也不会出现在其他项目中。
这种方法在Eclipse中也可以正常工作。
如果对你不起作用,请尝试更新你的Maven插件依赖(maven-compiler-plugin,maven-surefire-plugin)和/或你的IDE。

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