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

Posted

技术标签:

【中文标题】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?Java 9 + maven + junit:测试代码是否需要自己的 module-info.java 以及放在哪里? 【发布时间】:2018-03-18 16:49:59 【问题描述】:

假设我有一个使用 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+...a module-info 在测试中从我的角度来看没有意义吗?背后的特殊要求/成就? 当然是 Maven 3 【参考方案1】:

我无法使其与最新的 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;

【讨论】:

【参考方案2】:

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

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

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

一旦你需要测试你的代码,gradlesays this:

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

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


但是,这并不是故事的结局。如果我确实想通过ServiceLocator 定义JUnit5 Extension 怎么办。这意味着我需要从测试中进入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 运行。

例子是here

【讨论】:

谢谢分享。我是 Gradle 的新手,只有基本的了解,因此我可以将一些概念与 Maven 联系起来。我对 this will be "on" since gradle-7 在你的回答中的理解是,Gradle 将构建一种识别 java 模块路径与类路径意识的能力,并明智地选择代码所在的位置.作为用户,在我的观点看来,在测试插件中指定java源和目标就足够了。 但是,像provides org.junit.jupiter.api.extension.Extension with __; 这样的东西似乎是前哨。老实说,我将不得不研究 Junit5 关于定义扩展并将其与 Java 模块系统保持一致的建议。 (我想是时候回顾一下Sormuras 的一些答案了,特别是this one!)【参考方案3】:

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

一个模块及其对应的测试应该只有一个 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,或者我如何实现 没有它的相同效果(需要等)?

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

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

requires static junit;

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

【讨论】:

我建议不要在目录结构中使用模块名称,因为它没有任何优势......? @khmarbaise 我相信你的意思是com.acme.project/com/acme/project。只是跟着quick-start guide 那里。虽然我同意,但它本身并没有提供任何优势。 在模块描述符中要求 junit 对我来说看起来不太好 另外,IDEA 将requires static junit 高亮显示为错误(因为 junit 在 pom.xml 中有一个测试范围) 这个单一的module 的建议有时甚至是不可能的。 here is an example【参考方案4】:

添加一些细节。

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

目前(版本 2.20.1),maven-surefire-plugin 只能以旧方式工作,因此它将正在测试的类放在类路径上,模块路径被忽略。因此,现在,将模块信息添加到 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 的引用(仅在测试时需要) .

简历:

    module-info 只需要添加到主要来源中 目前,surefire 会忽略新的模块相关逻辑(但将来会更改) (当模块在安全测试下工作时) junit 可能不需要添加到模块信息中 (当模块在万无一失的测试下工作时)如果某些模块是测试需要的(并且只有它们),它可以被添加为仅编译依赖项(使用require static),如建议的那样@空指针。在这种情况下,Maven 模块将不得不依赖于使用编译(而非测试)范围提供该仅测试模块的工件,我不太喜欢。

【讨论】:

【参考方案5】:

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

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

正如 Alan Bateman 所指出的,Maven 编译器插件在编译 src/test/java 树中的代码时使用模块系统提供的--patch-module and other options,以便在被测模块中增加测试类。这也是在运行测试类时由 Surefire 插件完成的(参见 Support running unit tests in named Java 9 modules)。这意味着您不需要将测试代码放在模块中。

【讨论】:

将包更改为测试有一个缺点,即您的测试将不再达到默认和受保护的修饰符。 该模块提供了--patch-module 等选项来支持与被测模块在同一个包/模块中的测试的编译和执行。 maven-compiler-plugin 在编译 src/test 树中的代码时使用这些选项。同样适用于surefire-plugin。 @AlanBateman 感谢您提供此信息。我不知道Maven会这样做。我会用你的信息更新答案。 @nullpointer 好像是在issues.apache.org/jira/browse/SUREFIRE-1420中添加的。 IMO,这是非常正确答案。 gradle 采用了相同的pattern。

以上是关于Java 9 + maven + junit:测试代码是不是需要自己的 module-info.java 以及放在哪里?的主要内容,如果未能解决你的问题,请参考以下文章

如何在没有 Maven 的情况下同时运行 Eclipse Java 项目的所有 JUnit 测试?

Maven 和 JUnit 5:在一个类中运行单个测试

使用 maven surefire 使用 mpirun 运行 Junit 测试

Spring+JUnit4单元测试入门

使用 maven-surefire-plugin 进行 JUnit 和 Spock 测试

Java web项目