Sinds de introductie van CDI in Java EE zie ik een verschuiving ontstaan van Spring naar Java EE. Een belangrijke reden van deze verschuiving is het gemak van CDI om middels annotaties dependency injection te bewerkstelligen.
Het testen van dit mechanisme is niet altijd triviaal, want CDI heeft een container nodig. Frameworks zoals Mockito maken het mogelijk om mock dependencies toe te voegen, maar het is geen container. Applicatieservers zijn met de tijd steeds sneller geworden, hierdoor zijn er geen reden meer om deze niet embedded te gebruiken anders dan de meegeleverde API.
In listing 1 staat een voorbeeld van een test case die gebaseerd is op Mockito. Dit soort testen kom ik vaak tegen in de praktijk. Je kan je afvragen wat er hier wordt getest. Immers is de class JpaRepository weinig meer dan een proxy. Het gemak van deze class is dat je met Mockito de functionaliteit schijnbaar kan testen.
@Stateless
public class JpaRepository {
@PersistenceContext
private EntityManager em;
public boolean create(User user) {
em.persist(user);
return user.getId() != null;
}
}
@RunWith(MockitoRunner.class)
public class JpaRepositoryTest {
@Mock
EntityManager em;
@InjectMocks
JpaRepository repo;
@Test
public void insertUserTest() {
User user = new User(...);
boolean created = repo.create(user);
verify(em, times(1)).persist(user);
}
}
listing 1
Kan het ook anders?
Met de hierboven gegeven test case willen eigenlijk testen of de User op een correcte manier in de database wordt opgeslagen. Dit is een integratietest waar je de business logica, configuratie en het datamodel test. Het liefste wil je deze functionaliteit testen in een container zodat weet dat het echt werkt. Dit kan met Arquillian.
Arquillian is een framework dat je in staat stelt om per unit test een container op te tuigen die alleen dat bevat wat jij nodig hebt. In listing 2 is dit de EntityManager en zijn configuratie.
1 @RunWith(Arquillian.class)
2 public class JpaRepositoryIT {
3 @Inject
4 JpaRepository repo;
5 @Deployment
6 public static Archive<?> deployment() {
7 return ShrinkWrap.create(WebArchive.class)
8 .addPackage(JpaRepository.class.getPackage())
9 .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml")
10 .addAsResource("test-persistence.xml",
"META-INF/persistence.xml");
}
@Test
public void insertUserTest() {
User user = new User("Pieter", 42);
boolean created = repo.create(user);
assertThat(created, is(true));
}
}
listing 2
Bovenstaande test lijkt sterk op de voorgaande. Het grote verschil tussen de tests is dat deze laatste daadwerkelijk binnen een container wordt uitgevoerd.
Zoals te zien zijn er een aantal zaken anders en die zijn specifiek voor het uitvoeren van testen met Arquillian. Op regel 1 wordt wel de veel gebruikte annotatie @RunsWith gebruik maar nu met de class Arquillian.
Regels 3 en 4 zijn regels die we gewend zijn voor CDI class. Omdat we nu te maken hebben met een Arquillian test class hebben we ook in de test class de CDI annotaties tot onze beschikking. De unit test draait immers in de container.
Het echte werk vind plaats op de regels 5 tot en met 10. De class ShrinkWrap is de core van Arquillian. ShrinkWrap is de manier om een deployable archive te maken. Het biedt een fluent API aan voor de verschillende beschikbare archives. Door middel van de annotatie @Deployment geeft je aan dat dit het archive oplevert dat je wilt gaan deployen op de gewenste container. Let wel dit moet een public static zijn met een return type van Archive<?>. In het voorbeeld wordt een WebArchive (WAR) gemaakt maar het JavaArchive (JAR), EnterpriseArchive (EAR) en ResourceAdapter (RAR) zijn ook mogelijk.
Zoals gezegd is ShrinkWrap de core van Arquillian voor het maken van de deployment. de regels 8, 9 en 10 bepalen de inhoud van het archive. Als eerste worden er alle klassen die in het package van JpaRepository staan toegevoegd. Als tweede wordt er een beans.xml toegevoegd. De CDI specificatie stelt dat deze in een CDI applicatie aanwezig moet zijn. door middel van het keyword EmptyAsset.INSTANCE zorgen we er voor dat er een 0-byte file gemaakt wordt met de naam beans.xml. Als derde en laatste stap wordt er een persistence.xml toegevoegd. als bron voor deze file wordt de test resource test-persistence.xml gebruikt.
In listing 2 hebben we slechts een zeer kleine deployment. In de praktijk heb je al snel te maken met andere dependencies. Voor de ondersteuning van Maven dependencies is er de Maven class. Dit is niets meer dan een depenency resolver voor maven die een resultaat op levert dat je kan toevoegen aan je test archive.
Met listing 3 voeg je alle dependencies van module toe aan je deployment inclusief alle dependencies die hij heeft. Met de methode (regel 2) loadPomFromFile("pom.xml") laad je de pom file van je huidige project in. op regel 3 defineer je de dependency die in de pom file opgezocht moet worden. De versie is expliciet niet meegegeven zodat hij door maven bepaald wordt. Vervolgens (regel 4) zeggen willen we dat alle transitieve dependencies van dit artifact meegenomen moeten worden en dat wij die als (regel 5) lijst van jars wordt terug gegeven. Op regel 6 worden de gevonden dependencies als library toegevoegd aan het WebArchive.
1 final JavaArchive[] as = Maven.resolver()
2 .loadPomFromFile("pom.xml")
3 .resolve("my.group.id:module")
4 .withTransitivity()
5 .as(JavaArchive.class);
6 archive.addAsLibraries(as);
listing 3
Let wel, dit is een integratietest. Het is niet nodig om deze altijd te draaien. Jenkins is goed in staat om dit voor je te doen. Iets wat misschien niet direct opvalt is de naamgeving van de testcase. De de facto standaard is dat testcases eindigen op Test. Deze postfix is expliciet gekozen voor het gebruik van de Maven failsafe plugin. De combinatie van de de failsafe en surefire plugin stelt ons in staat om door middel van naamgeving dit onderscheid te maken.
Het vorige voorbeeld was duidelijk een geval van een integratietest. De volgende class is als volgt (zie listing 4):
public class WinningNumberGenerator {
static double SALT = Math.random()*1E4;
public int draw(){
return (int)(Math.random() * SALT);
}
}
listing 4
In deze class zien we duidelijk dat we het hier hebben over business logica. Voor het testen van deze logica hebben we geen container nodig. Door middel van een unit test kunnen we verifiëren dat de logica correct geïmplementeerd is. De test cases voor deze logica willen we daarom ook vaak en snel uitvoeren. Het gebruik van mocked objecten maakt het ook gemakkelijk (zie listing 5).
public class WinningNumberGeneratorTest {
@Test
public void winningNumber_should_return_0(){
WinningNumberGenerator gen =
new WinningNumberGenerator();
gen.SALT = 0;
assertThat(gen.draw(),is(0));
}
}
listing 5
Maven integratie
Zoals hiervoor vermeld kan je door middel van build profiles goed onderscheid maken tussen unit en integratie tests.
Testen worden in Maven uitgevoerd door de surefire plugin. Deze plugin kijkt naar classes die voldoen aan het volgende patterns: "**/Test*.java", "**/*Test.java" en "**/*TestCase.java". Voor de failsafe plugin gelden de volgende patterns: "**/IT*.java", "**/*IT.java" en "**/*ITCase.java". Een ander belangrijk punt van de failsafe plugin is dat alle maven *integration* lifecycle stappen worden doorlopen. Als de failsafe plugin is opgenomen in de pom file kan je door middel van mvn verify je integratie testen uitvoeren.
Listing 6 heeft twee verschillende profiles. Het eerste profile genaamd with-integration is het profile dat je activeert als de integratie tests wil gaan uitvoeren. De failsafe plugin is gebonden aan de custom maven lifecycle failsafe:integration-test en verify. Het tweede profile schakelt de failsafe plugin expliciet uit.
<profiles>
<profile>
<id>with-integration</id>
<!-- Dependencies o.a.Arquillian -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.18.1</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>unit</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<skipITs>true</skipITs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
listing 6
Het activeren van een specifiek profiel kan je op eenvoudige wijze op de commandline doen. Het commando: mvn clean install-P with-integration zal er voor zorgen dat er een build uitgevoerd wordt dat zowel de unit tests ook de integratie tests uitvoert.Als je alleen de unit test wilt uitvoeren kan je dit doen door mvn clean install -P unit of nog gemakkelijker gewoon mvn clean install. In bovenstaande situatie zullen allen de unit tests uitgevoerd worden immers de failsafe plugin wordt niet geactiveerd.
Arquillian dependencies
Als je initieel met Arquillian van start heb je al snel last van dependency hell. Om dit op te lossen bied Arquillian Maven BOM's ( Bill Of Materials) aan. Sinds de introductie van Maven 2.0.9 heeft maven het keyword import ingevoerd. Hiermee wordt het mogelijk om in je pom files slechts een maal een dependency met een versie op te nemen en voor alle andere gerelateerde dependencies. Met de opname van deze import ben je zo goed als verlost van de dependency problemen. Andere aan Arquillian gerelateerde modules bieden ook BOM's, maak er gebruik van.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jboss.arquillian</groupId>
<artifactId>arquillian-bom</artifactId>
<version>${version.arquillian_core}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
listing 7
Conclusie
De verschuiving die is ontstaan door de introductie van CDI en de daarmee verschuiving van Spring naar EE is op een goede manier op te vangen. Zelf ben ik van mening dat hiermee het mogelijk is geworden om tot implementaties die minder last hebben van integratie problemen.
Om hier maximaal resultaat uit te halen is het belangrijk om goed onderscheidt te maken tussen unit en integratie testen. De unit tests zullen vaak uitgevoerd worden tijdens de ontwikkel cyclus. Integratie testen worden op de build server uitgevoerd door middel van activatie van het Maven profile .
Als ondersteuning voor dit artikel heb ik op github (https://github.com/elucidator/java_magazine_arquillian) een aantal voorbeelden van de in dit artikel behandelde aspecten.
Bronnen:
- http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Importing_Dependencies
- https://docs.jboss.org/author/display/ARQ/Reference+Guide
- http://www.mountaingoatsoftware.com/books (Testing Pyramid)
- http://blogs.oracle.com/arungupta/entry/why_java_ee_6_is
- http://maven.apache.org/surefire/maven-failsafe-plugin/examples/inclusion-exclusion.html