Java 9 + maven + junit: 测试代码是否需要自己的module-info.java,放在哪里?

Java 9 + maven + junit: does test code need module-info.java of its own and where to put it?

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

现在想把项目迁移到Java9。src/main/java内容代表Java9模块; 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,等等)?

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

一个模块及其相应的测试应该只有一个 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
}

根据您的问题总结以上所有内容 --

I feel I follow wrong path, it all starts looking very ugly. How can I have module-info.java of its own in test code, or how do I achieve the same effects (require, etc) without it?

,你不应该考虑为测试代码管理不同的模块使它变得复杂。

您可以通过使用如下指令将 junit 视为 compile-time dependency 来实现类似的效果-

requires static junit;

使用 Maven,您可以按照上述结构并使用 maven-surefire-plugin 来实现此目的,它会自行将测试修补到模块。

模块系统不区分生产代码和测试代码,所以如果您选择模块化测试代码,prod.moduletest.module不能共享同一个包com.acme.project ,如 specs:

中所述

Non-interference — The Java compiler, virtual machine, and run-time system must ensure that modules that contain packages of the same name do not interfere with each other. If two distinct modules contain packages of the same name then, from the perspective of each module, all of the types and members in that package are defined only by that module. Code in that package in one module must not be able to access package-private types or members in that package in the other module.

正如 Alan Bateman 所指出的,Maven 编译器插件使用 --patch-module and other options provided by the module system when compiling code in the src/test/java tree, so that the module under test is augmented with the test classes. And this is also done by the Surefire plugin when running the test classes (see Support running unit tests in named Java 9 modules)。这意味着您不需要将测试代码放在模块中。

添加一些细节。

从 9 开始,在 Java 中,可以将 jar 文件(或带有 类 的目录)放在类路径(如前所述)或模块路径中。如果将其添加到类路径,则忽略其模块信息,并且不应用与模块相关的限制(什么读取什么,什么导出什么等)。但是,如果将 jar 添加到模块路径,它将被视为一个模块,因此会处理其模块信息,并将强制执行其他与模块相关的限制。

目前(版本2.20.1),maven-surefire-plugin只能以旧方式工作,所以它把被测试的类放到classpath中,module-path被忽略。所以,现在,将模块信息添加到 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 插件在模块路径上测试代码。当它完成并发布时,将考虑一个模块信息。但是如果他们让被测模块自动读取 junit 模块(如 SUREFIRE-1420 建议的那样),module-info(这是一个主模块描述符)将不必包含对 junit 的引用(仅在测试时需要) .

一份简历:

  1. 模块信息只需要添加到主要来源
  2. surefire暂时忽略新的模块相关逻辑(但以后会改变)
  3. (当模块将在 surefire 测试下工作时) junit 可能不需要添加到模块信息中
  4. (当模块将在 surefire 测试下工作时) 如果测试需要某些模块(并且只有它们需要),它可以作为仅编译依赖项添加(使用require static),正如@nullpointer 所建议的。在这种情况下,Maven 模块将不得不依赖于使用我不太喜欢的编译(而非测试)范围提供该仅测试模块的工件。

我只想在这里 添加我的 0.02$ 关于一般测试方法 ,因为似乎没有人在解决 gradle 而我们使用它。

首先,需要告诉 gradle 模块。这是相当微不足道的,通过(自 gradle-7 以来这将是“on”):

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

一旦您需要测试您的代码,gradle says this:

If you don’t have a module-info.java file in your test source set (src/test/java) this source set will be considered as traditional Java library during compilation and test runtime.

用简单的英语来说,如果你定义一个module-info.java用于测试目的——事情“会正常工作”,在大多数情况下这正是我们想要。


但是,故事还没有结束。如果我确实想通过 ServiceLocator 定义 JUnit5 Extension 怎么办?这意味着我需要从测试进入module-info.java;我还没有。

并且 gradle 再次解决了这个问题:

Another approach for whitebox testing is to stay in the module world by patching the tests into the module under test. This way, module boundaries stay in place, but the tests themselves become part of the module under test and can then access the module’s internals.

所以我们在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.

都很好

例子是here

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

我的解决方案是在测试源(Maven 中的src/test/java)中为具有以下内容的测试模块放置一个自己的 module-info.java。 我的情况是我必须使用关键字 open(参见 Allowing runtime-only access to all packages in a module),因为我在测试中使用 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;
}

另请注意,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 作为主路径,但使用类路径进行测试和测试。如果“修补”module-path 变得痛苦,那么人们采用这种方法可能不是一个坏选择。

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

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