Java 9 + maven + junit: heeft testcode zijn eigen module-info.java nodig en waar moet deze worden geplaatst?

Stel dat ik een Java-project heb met Maven 3 en junit. Er zijn mappen src/main/javaen src/test/javadie respectievelijk hoofdbronnen en testbronnen bevatten (alles is standaard).

Nu wil ik het project migreren naar Java 9. src/main/javainhoud vertegenwoordigt Java 9-module; er is com/acme/project/module-info.javadie er ongeveer zo uitziet:

module com.acme.project {
    require module1;
    require module2;
    ...
}

Wat als de testcode zelf module-info.javanodig heeft? Bijvoorbeeld om een ​​afhankelijkheid van een module toe te voegen die alleen nodig is voor tests, niet voor productiecode. In zo’n geval moet ik module-info.javain src/test/java/com/acme/project/zetten en de module een andere naam geven. Op deze manier lijkt Maven hoofdbronnen en testbronnen als verschillende modules te behandelen, dus ik moet pakketten van de hoofdmodule naar de testmodule exporteren en pakketten in de testmodule nodig hebben, ongeveer als volgt:

hoofdmodule (in src/main/java/com/acme/project):

module prod.module {
    exports com.acme.project to test.module;
}

testmodule (in src/test/java/com/acme/project):

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

Dit levert

[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

omdat één pakket is gedefinieerd in twee modules. Dus nu moet ik verschillende projecten in de hoofdmodule en testmodule hebben, wat niet handig is.

Ik heb het gevoel dat ik het verkeerde pad bewandel, het begint er allemaal erg lelijk uit te zien. Hoe kan ik een eigen module-info.javain testcode hebben, of hoe bereik ik dezelfde effecten (require, enz.) zonder deze?


Antwoord 1, autoriteit 100%

Het modulesysteem maakt geen onderscheid tussen productiecode en testcode, dus als u ervoor kiest om de testcode te moduleren, kunnen de prod.moduleen de test.moduleniet delen hetzelfde pakket com.acme.project, zoals beschreven in de specificaties:

Niet-interferentie— De Java-compiler, virtuele machine en runtime-systeem moeten ervoor zorgen dat modules die pakketten met dezelfde naam bevatten, elkaar niet storen. Als twee verschillende modules pakketten met dezelfde naam bevatten, worden vanuit het perspectief van elke module alle typen en leden in dat pakket alleen door die module gedefinieerd. Code in dat pakket in de ene module mag geen toegang hebben tot pakket-private typen of leden in dat pakket in de andere module.

Zoals aangegeven door Alan Bateman, gebruikt de Maven-compilerplug-in –patch -module en andere optiesdie door het modulesysteem worden geboden bij het compileren van code in de src/test/java-boom, zodat de te testen module wordt uitgebreid met de testklassen. En dit wordt ook gedaan door de Surefire-plug-in bij het uitvoeren van de testklassen (zie Ondersteuning bij het uitvoeren van unit-tests in genaamd Java 9-modules). Dit betekent dat u uw testcode niet in een module hoeft te plaatsen.


Antwoord 2, autoriteit 30%

Misschien wil je het projectontwerp dat je probeert te implementeren, heroverwegen. Aangezien u een module en de test ervan in een project implementeert, moet u zich onthouden van het gebruik van verschillende modules voor elk afzonderlijk.

Er zou slechts één enkele module-info.javamoeten zijn voor een module en de bijbehorende tests.

Uw relevante projectstructuur kan er als volgt uitzien:-

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

waar de module-info.javaverder zou kunnen zijn:-

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

Om al het bovenstaande samen te vatten volgens uw vragen —

Ik heb het gevoel dat ik het verkeerde pad bewandel, het begint er allemaal erg lelijk uit te zien. Hoe kan ik
heb een eigen module-info.java in testcode, of hoe bereik ik de
dezelfde effecten (vereisen, etc) zonder?

Ja, u moet niet overwegen om verschillende modules voor testcode te beheren, waardoor het complex wordt.

U kunt een vergelijkbaar effect bereiken door junitte behandelen als een compileertijdafhankelijkheidmet behulp van de richtlijnen als volgt-

requires static junit;

Met Maven kunt u dit bereiken door de hierboven vermelde structuur te volgen en met behulp van maven-surefire-plugindie ervoor zorgt dat de tests zelf naar de module worden gepatcht.


Antwoord 3, autoriteit 15%

Ik wil gewoon mijn 0.02$hier op de algemene testbenaderingtoevoegen, aangezien het lijkt alsof niemand gradleaanspreekt en we gebruiken het.

Eerst moet men gradlevertellen over modules. Het is vrij triviaal, via (dit staat “aan” sinds gradle-7):

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

Zodra u uw code moet testen, gradlezegt dit:

Als je geen bestand module-info.javain je testbronset (src/test/java) hebt, wordt deze bronnenset als traditioneel beschouwd Java-bibliotheek tijdens compilatie en testruntime.

In gewoon Engels, als u nieteen module-info.javadefinieert voor testdoeleinden, zullen de dingen “gewoon werken” en in de meeste gevallen is dit precies wat we willen.


Maar dat is niet het einde van het verhaal. Wat als ik een JUnit5 Extensionwil definiëren, via ServiceLocator. Dat betekent dat ik naarmodule-info.javamoet gaan, van tests; een die ik nog niet heb.

En gradleheeft dat weer opgelost:

Een andere benadering voor whitebox-testen is om in de modulewereld te blijven door de tests in de te testen module te patchen. Op deze manier blijven modulegrenzen op hun plaats, maar de tests zelf worden onderdeel van de te testen module en hebben dan toegang tot de interne onderdelen van de module.

Dus we definiëren een module-info.javain src/test/java, waar ik kan plaatsen:

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

we moeten ook --patch-moduledoen, net zoals maven plug-ins dat doen. Het ziet er zo uit:

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

Het enige probleem is dat intellijdeze patch niet “ziet” en denkt dat we ook een requires-instructie nodig hebben (requires zero.x.services), maar dat is niet echt het geval. Alle tests lopen prima vanaf de opdrachtregel en intellij.

Het voorbeeld is hier


Antwoord 4, autoriteit 10%

Enkele details toevoegen.

In Java sinds 9 kan een jar-bestand (of een directory met klassen) op classpath (zoals eerder) of op modulepad worden geplaatst. Als het wordt toegevoegd aan classpath, wordt de module-info genegeerd en worden er geen module-gerelateerde beperkingen (wat leest wat, wat exporteert wat, enz.) toegepast. Als er echter een jar wordt toegevoegd aan het modulepad, wordt deze behandeld als een module, dus de module-info wordt verwerkt en aanvullende modulegerelateerde beperkingen worden afgedwongen.

Momenteel (versie 2.20.1) kan de maven-surefire-plug-in alleen op de oude manier werken, dus het plaatst de klassen die worden getest op classpath en module-path wordt genegeerd. Dus op dit moment zou het toevoegen van module-info aan een Maven-project niets moeten veranderen aan tests die worden uitgevoerd met Maven (met een trefzekere plug-in).

In mijn geval ziet de opdrachtregel er als volgt uit:

/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

De te testen klassen worden niet als module toegevoegd, dus staan ​​ze op klassenpad.

Momenteel wordt er gewerkt aan https://issues.apache.org /jira/browse/SUREFIRE-1262(SUREFIRE-1420 is gemarkeerd als een duplicaat van SUREFIRE-1262) om de trefzekere plug-in te leren code te testen op het modulepad. Wanneer het klaar en vrijgegeven is, zal een module-info zalworden overwogen. Maar als ze ervoor zorgen dat de te testen module de junit-module automatisch leest (zoals SUEFIRE-1420 suggereert), hoeft module-info (wat een hoofdmodule-descriptor is) geen verwijzing naar junit op te nemen (wat alleen nodig is voor tests) .

Een cv:

  1. module-info moet alleen worden toegevoegd aan de hoofdbronnen
  2. voorlopig negeert trefzeker nieuwe module-gerelateerde logica (maar dit zal in de toekomst worden gewijzigd)
  3. (wanneer modules werken onder trefzekere tests)junit hoeft waarschijnlijk niet te worden toegevoegd aan de module-info
  4. (wanneer modules zullen werken onder trefzekere tests)als een module vereist is door tests (en alleen door hen), kan deze worden toegevoegd als een afhankelijkheid voor alleen compileren (met behulp van require static), zoals voorgesteld door @nullpointer. In dit geval zal de Maven-module afhankelijk moeten zijn van een artefact dat die module met alleen test levert met behulp van een compileer- (niet-test) scope die ik niet zo leuk vind.

Antwoord 5

Ik kon het ook niet laten werken met de nieuwste versie van de trefzekere plug-in Maven (3.0.0-M5). Het lijkt erop dat als de belangrijkste bronnen een module gebruiken, de compiler-plug-in bij gebruik van Java 11 ook verwacht dat pakketten waarnaar wordt verwezen in een module zitten.

Mijn oplossing was om een ​​eigen module-info.javain de testbronnen (src/test/javain Maven) te plaatsen voor de testmodule met de onderstaande inhoud .
In mijn geval moest ik het trefwoord opengebruiken (Zie Alleen runtime-toegang toestaan tot alle pakketten in een module) omdat ik Mockito gebruik in mijn test, waarvoor reflectieve toegang vereist is.

// 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;
}

Other episodes