これは、なにをしたくて書いたもの?
Java EE(Jakarta EE)のテストなら、Arquillianかなぁと思うのですが。
Webサイトを見ていると、最近は静かな気がします。
とはいうものの、各アプリケーションサーバー向けの実装は継続しているようです。
GitHub - arquillian/arquillian-container-tomcat: Arquillian Tomcat Containers
GitHub - arquillian/arquillian-container-jetty: Arquillian Jetty Containers
WildFlyに関しては、WildFlyのOrganizationにAdapterがありました。
GitHub - wildfly/wildfly-arquillian: The Wildfly Arquillian adaptor
今回、このWildFly Arquillian Adapterを使って、簡単なインテグレーションテストを書いてみたいと思います。
WildFly Arquillian Adapter
WildFly Arquillian Adapterは、WildFlyやJBoss EAP向けのモジュールです。
GitHub - wildfly/wildfly-arquillian: The Wildfly Arquillian adaptor
現時点で見ると、バージョン3以降で見るのがポイントでパターンとしては以下の4つがあり
- Managed … インストール済みのアプリケーションサーバーを対象にする
- Remote … リモートのアプリケーションサーバーを対象にする
- Embedded … 実行時にアプリケーションサーバーをダウンロードしてきて、それを対象にする
- Bootable … WildFly Bootable JARで作成したJARファイルを対象にする
ManagedおよびRemoteに関しては、スタンドアロンモードとドメインモードがあります。
今回は、Managed、Remote、Bootableをそれぞれ試していこうと思います。
Managed、Remoteについてはスタンドアロンモードです。
WildFly自体は、Jakarta EE 8対応の25を使います。
また、JUnitについては5向けの対応が進んでいるので、こちらを使います。
環境
今回の環境は、こちら。
$ java --version openjdk 11.0.11 2021-04-20 OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04) OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.3 (ff8e977a158738155dc465c6a97ffaf31982d739) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-89-generic", arch: "amd64", family: "unix"
テスト対象のプログラムの作成
まずは、テスト対象のプログラムを作成しましょう。
pom.xml
から、順次載せていきます。
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.littlewings</groupId> <artifactId>wildfly-arquillian-adapter-example</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <failOnMissingWebXml>false</failOnMissingWebXml> </properties> <dependencies> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-web-api</artifactId> <version>8.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.21.0</version> <scope>test</scope> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <version>4.4.0</version> <scope>test</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.arquillian.junit5</groupId> <artifactId>arquillian-junit5-container</artifactId> <version>1.7.0.Alpha10</version> <scope>test</scope> </dependency> </dependencies> <profiles> <!-- あとで --> </profiles> </project>
プログラムの作成自体に必要な依存関係は、こちらだけです。
<dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-web-api</artifactId> <version>8.0.0</version> <scope>provided</scope> </dependency>
JAX-RS有効化のためのクラス。
src/main/java/org/littlewings/wildfly/arquillian/JaxrsActivator.java
package org.littlewings.wildfly.arquillian; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; @ApplicationPath("") public class JaxrsActivator extends Application { }
JAX-RSリソースクラス。
src/main/java/org/littlewings/wildfly/arquillian/EchoResource.java
package org.littlewings.wildfly.arquillian; import java.util.Map; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; @Path("echo") public class EchoResource { @Inject MessageService messageService; @GET @Produces(MediaType.TEXT_PLAIN) public String get(@QueryParam("message") String message) { return messageService.format(message); } @POST @Produces(MediaType.APPLICATION_JSON) public Map<String, Object> post(Map<String, Object> request) { return Map.of("message", messageService.format((String) request.get("message"))); } }
CDI管理Bean。
src/main/java/org/littlewings/wildfly/arquillian/MessageService.java
package org.littlewings.wildfly.arquillian; import javax.enterprise.context.ApplicationScoped; @ApplicationScoped public class MessageService { public String format(String message) { return String.format("★★★ %s ★★★", message); } }
リクエストで受け取った文字列を、★で装飾して返すアプリケーションです。
動作イメージはこんな感じですね。
※これはBootable JARで動作させていて、コンテキストパスが/
になっています
$ curl localhost:8080/echo?message=Hello+World ★★★ Hello World ★★★ $ curl -H 'Content-Type: application/json' localhost:8080/echo -d '{"message": "Hello World"}' {"message":"★★★ Hello World ★★★"}
このアプリケーションに対するテストを、Arquillianを使って書いていきます。
テストコードの準備
テストは、今回作成したCDI管理Beanと、JAX-RSリソースクラスに対して作成します。
テスト向けのライブラリとして、JUnit 5、AssertJ、REST Assuredを使うことにします。
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.21.0</version> <scope>test</scope> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <version>4.4.0</version> <scope>test</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.0</version> <scope>test</scope> </dependency>
また、ArquillianでJUnit 5を使うためには、こちらのモジュールが必要です。このモジュールの依存関係として、
JUnit 5はprovided
になっているので、自分で明示的に依存関係を追加する必要があります。
<dependency> <groupId>org.jboss.arquillian.junit5</groupId> <artifactId>arquillian-junit5-container</artifactId> <version>1.7.0.Alpha10</version> <scope>test</scope> </dependency>
そして、作成したテストコードはこちら。
src/test/java/org/littlewings/wildfly/arquillian/WildFlyArquillianIT.java
package org.littlewings.wildfly.arquillian; import java.io.IOException; import java.net.URL; import java.util.Map; import javax.inject.Inject; import javax.ws.rs.ApplicationPath; import io.restassured.http.ContentType; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.container.test.api.RunAsClient; import org.jboss.arquillian.junit5.ArquillianExtension; import org.jboss.arquillian.test.api.ArquillianResource; import org.jboss.modules.maven.ArtifactCoordinates; import org.jboss.modules.maven.MavenResolver; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; @ExtendWith(ArquillianExtension.class) public class WildFlyArquillianIT { @Deployment public static WebArchive createDeployment() throws IOException { return ShrinkWrap .create(WebArchive.class) .addPackages(true, JaxrsActivator.class.getPackage()) .addAsLibraries( MavenResolver .createDefaultResolver() .resolveJarArtifact(ArtifactCoordinates.fromString("org.assertj:assertj-core:3.21.0")) ); } @Inject MessageService messageService; @Test public void cdiBeanTest() { assertThat(messageService.format("Hello World")) .isEqualTo("★★★ Hello World ★★★"); } @ArquillianResource URL deploymentUrl; String resourcePrefix = JaxrsActivator.class.getAnnotation(ApplicationPath.class).value(); @Test @RunAsClient public void jaxrsResourceTest() { given() .queryParam("message", "Hello World") .when() .get(String.format("%s%s%s", deploymentUrl, resourcePrefix, "echo")) .then() .assertThat() .statusCode(200) .body(is("★★★ Hello World ★★★")); given() .contentType(ContentType.JSON) .body(Map.of("message", "Hello World")) .when() .post(String.format("%s%s%s", deploymentUrl, resourcePrefix, "echo")) .then() .assertThat() .statusCode(200) .body("message", is("★★★ Hello World ★★★")); } }
最近のArquillianやJUnit 5と合わせた時の使い方は、こちらを参考にしました。
Testing Jakarta EE 9 Components With Arquillian and JBoss Weld | by Hantsy | The Startup | Medium
Testing Jakarta EE 9 Applications with Arquillian and WildFly | by Hantsy | ITNEXT
@ExtendWith
アノテーションには、ArquillianExtension
を指定します。
@ExtendWith(ArquillianExtension.class) public class WildFlyArquillianIT {
Deploymentの作り方や
※CDI管理BeanのテストでAssertJを使っているので、こちらをMavenResolverでデプロイ対象に追加しています
@Deployment public static WebArchive createDeployment() throws IOException { return ShrinkWrap .create(WebArchive.class) .addPackages(true, JaxrsActivator.class.getPackage()) .addAsLibraries( MavenResolver .createDefaultResolver() .resolveJarArtifact(ArtifactCoordinates.fromString("org.assertj:assertj-core:3.21.0")) ); }
テストの書き方は、以前とあまり変わりませんね。
@Inject MessageService messageService; @Test public void cdiBeanTest() { assertThat(messageService.format("Hello World")) .isEqualTo("★★★ Hello World ★★★"); } @ArquillianResource URL deploymentUrl; String resourcePrefix = JaxrsActivator.class.getAnnotation(ApplicationPath.class).value(); @Test @RunAsClient public void jaxrsResourceTest() { given() .queryParam("message", "Hello World") .when() .get(String.format("%s%s%s", deploymentUrl, resourcePrefix, "echo")) .then() .assertThat() .statusCode(200) .body(is("★★★ Hello World ★★★")); given() .contentType(ContentType.JSON) .body(Map.of("message", "Hello World")) .when() .post(String.format("%s%s%s", deploymentUrl, resourcePrefix, "echo")) .then() .assertThat() .statusCode(200) .body("message", is("★★★ Hello World ★★★")); }
前者はデプロイ先で動かすCDI管理Beanのテスト、後者はデプロイ後にクライアントとして動作させるJAX-RSリソース
クラスに対するテストです。
ところで、このコードはこれまでに挙げたテストライブラリやarquillian-junit5-container
といった依存関係では、
コンパイルが通りません。
アプリケーションサーバー用のライブラリが必要になります。
※具体的にはMavenResolver
が通りません…
ここから先は、WildFly Arquillian Adapterを組み込んでいきます。
WildFly Arquillian Adapterを使う
Managed、Remote、Bootableの3パターンを使っていくわけですが、今回は各パターンごとにMavenのプロファイルで
わけることにします。
Arquillianの設定ファイルはひとつにまとめ、システムプロパティarquillian.launch
で使用する設定を切り替えます。
src/test/resources/arquillian.xml
<?xml version="1.0" encoding="UTF-8" ?> <arquillian xmlns="http://jboss.org/schema/arquillian" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jboss.org/schema/arquillian http://jboss.org/schema/arquillian/arquillian_1_0.xsd"> <container qualifier="wildfly-managed" default="true"> <configuration> <property name="jbossHome">${jboss.home}</property> <!-- deploy started managed server --> <property name="allowConnectingToRunningServer">${allow.connecting.to.running.server:false}</property> </configuration> </container> <container qualifier="wildfly-remote"> </container> <container qualifier="wildfly-bootable"> <configuration> <property name="jarFile">${bootable.jar}</property> </configuration> </container> </arquillian>
デフォルトはManaged用のwildfly-managed
として、Remote用はwildfly-remote
、Bootable用はwildfly-bootable
と
しています。
また、${...}
のプレースホルダー部も、システムプロパティで指定します。
確認は、いずれもmvn verify
で行います。
WildFlyを使うケースでは、以下のようにしてダウンロードしたWildFlyを使います。
$ curl -OL https://github.com/wildfly/wildfly/releases/download/25.0.1.Final/wildfly-25.0.1.Final.zip $ unzip wildfly-25.0.1.Final.zip $ cd wildfly-25.0.1.Final
ところで、今回WildFly Arquillian Adapterは4.xを使うのですが、現時点での最新版は5.xです。いずれも開発中ですが。
ここで、5.xを選ぶとJakarta EE 9が対象になるようで、以下のようにjakarta.servlet
が見つからないと言われ、
延々とハマりました…。
Caused by: java.lang.NoClassDefFoundError: Failed to link org/jboss/arquillian/protocol/servlet5/runner/ServletTestRunner (Module "deployment.test-7b3f36a4-511c-4384-8547-8d9952ad55d6.war" from Service Module Loader): jakarta/servlet/http/HttpServlet
現時点でのWildFlyはJakarta EE 8までで、9はPreview段階なので4.xを使うものとします。
Managed
Managed用のプロファイル。今回はこちらをデフォルトにしています。
<profile> <id>managed</id> <activation> <activeByDefault>true</activeByDefault> </activation> <dependencies> <dependency> <groupId>org.wildfly.arquillian</groupId> <artifactId>wildfly-arquillian-container-managed</artifactId> <version>4.0.0.Alpha3</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> <plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>3.0.0-M5</version> <configuration> <systemPropertyVariables> <jboss.home>/path/to/wildfly-25.0.1.Final</jboss.home> <allow.connecting.to.running.server>false</allow.connecting.to.running.server> <!-- すでに起動しているWildFlyに接続する場合 --> </systemPropertyVariables> </configuration> <executions> <execution> <id>integration-test</id> <goals> <goal>integration-test</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </profile>
Managed用のWildFly Arquillian Adapterは、こちらですね。
<dependency> <groupId>org.wildfly.arquillian</groupId> <artifactId>wildfly-arquillian-container-managed</artifactId> <version>4.0.0.Alpha3</version> <scope>test</scope> </dependency>
あとは、Maven Failsafe Pluginでインテグレーションテストの設定を行います。
<plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>3.0.0-M5</version> <configuration> <systemPropertyVariables> <jboss.home>/path/to/wildfly-25.0.1.Final</jboss.home> <allow.connecting.to.running.server>false</allow.connecting.to.running.server> <!-- すでに起動しているWildFlyに接続する場合 --> </systemPropertyVariables> </configuration> <executions> <execution> <id>integration-test</id> <goals> <goal>integration-test</goal> </goals> </execution> </executions> </plugin>
システムプロパティはこのように設定していますが、
<systemPropertyVariables> <jboss.home>/path/to/wildfly-25.0.1.Final</jboss.home> <allow.connecting.to.running.server>false</allow.connecting.to.running.server> <!-- すでに起動しているWildFlyに接続する場合 --> </systemPropertyVariables>
arquillian.xml
のこの部分と対応しています。WildFlyのインストール先が主な設定ですね。
<container qualifier="wildfly-managed" default="true"> <configuration> <property name="jbossHome">${jboss.home}</property> <!-- deploy started managed server --> <property name="allowConnectingToRunningServer">${allow.connecting.to.running.server:false}</property> </configuration> </container>
この状態で
$ mvn verify
と実行すると、jbossHome
で指定したWildFlyを起動してテストを行い、終了したらWildFlyを停止します。
また、allowConnectingToRunningServer
なのですが、こちらをtrue
に設定すると、すでに起動済みのWildFlyに
対してデプロイとテストを行うようになります。Remoteみたいな動きになりますね。
その他、設定項目はこちらを参照。
Remote
次は、Remote。
<profile> <id>remote</id> <dependencies> <dependency> <groupId>org.wildfly.arquillian</groupId> <artifactId>wildfly-arquillian-container-remote</artifactId> <version>4.0.0.Alpha2</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> <plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>3.0.0-M5</version> <configuration> <systemPropertyVariables> <arquillian.launch>wildfly-remote</arquillian.launch> </systemPropertyVariables> </configuration> <executions> <execution> <id>integration-test</id> <goals> <goal>integration-test</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </profile>
Remote用の依存関係はこちらなのですが、Managedと違ってバージョンをひとつ下げています。
<dependency> <groupId>org.wildfly.arquillian</groupId> <artifactId>wildfly-arquillian-container-remote</artifactId> <version>4.0.0.Alpha2</version> <scope>test</scope> </dependency>
システムプロパティは、arquillian.xml
で定義していたwildfly-remote
を使うように指定。
<systemPropertyVariables> <arquillian.launch>wildfly-remote</arquillian.launch> </systemPropertyVariables>
WildFlyをローカルで動かしているのであれば、特に設定は不要です。
<container qualifier="wildfly-remote"> </container>
最初はManagedと同じように4.0.0.Alpha3
を使っていたのですが、ずっと以下のエラーに悩まされ、試しにバージョンを
下げたら発生しなくなりました…。
java.lang.RuntimeException: java.net.MalformedURLException: Unsupported protocol: remote+http Caused by: java.net.MalformedURLException: Unsupported protocol: remote+http
なので、今回は4.0.0.Alpha2
を使おうと思います。
https://github.com/wildfly/wildfly-arquillian/blob/4.0.0.Alpha3/container-bootable/pom.xml
WildFlyをあらかじめ起動した状態で
$ bin/standalone.sh
実行。
$ mvn verify -Premote
これで、起動中のWildFlyへのデプロイとテストが行われます。
Remoteの場合は、共通項目以外の固有の設定はなさそうですね。
Bootable
最後は、Bootableです。WildFly Arquillian Adapter 3.0.0から利用可能になったものみたいです。
これは、Bootable JARで作成したアプリケーションを使って、テストを行うものですね。
Bootable用の設定は、このようにしました。
<profile> <id>bootable</id> <dependencies> <dependency> <groupId>org.wildfly.arquillian</groupId> <artifactId>wildfly-arquillian-container-bootable</artifactId> <version>4.0.0.Alpha3</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> <plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>3.0.0-M5</version> <configuration> <systemPropertyVariables> <arquillian.launch>wildfly-bootable</arquillian.launch> <bootable.jar>target/${project.artifactId}-${project.version}-bootable.jar</bootable.jar> </systemPropertyVariables> </configuration> <executions> <execution> <id>integration-test</id> <goals> <goal>integration-test</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.wildfly.plugins</groupId> <artifactId>wildfly-jar-maven-plugin</artifactId> <version>6.0.0.Final</version> <configuration> <feature-pack-location>wildfly@maven(org.jboss.universe:community-universe)#25.0.1.Final</feature-pack-location> <layers> <layer>jaxrs-server</layer> <layer>management</layer> </layers> <!-- for development <plugin-options> <jboss-maven-dist/> </plugin-options> --> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>package</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </profile>
依存関係としては、こちらが必要です。
<dependency> <groupId>org.wildfly.arquillian</groupId> <artifactId>wildfly-arquillian-container-bootable</artifactId> <version>4.0.0.Alpha3</version> <scope>test</scope> </dependency>
システムプロパティとしては、arquillian.xml
で定義したwildfly-bootable
を使うようにするのと、Bootable JARの
パスを指定するようにしています。
<configuration> <systemPropertyVariables> <arquillian.launch>wildfly-bootable</arquillian.launch> <bootable.jar>target/${project.artifactId}-${project.version}-bootable.jar</bootable.jar> </systemPropertyVariables> </configuration>
Bootable JARのパスは、arquillian.xml
で定義したこちらに設定されます。
<container qualifier="wildfly-bootable"> <configuration> <property name="jarFile">${bootable.jar}</property> </configuration> </container>
また、Bootable JARを作るためのWildFly JAR Plugin自体の設定も、もちろん必要になります。
<plugin> <groupId>org.wildfly.plugins</groupId> <artifactId>wildfly-jar-maven-plugin</artifactId> <version>6.0.0.Final</version> <!-- 省略 --> </plugin>
あとは、こちらを実行。
$ mvn verify -Pbootable
Bootable JARを作成した後に、テストが実行されます。
今回はBootable JARをフルで作りましたが、あくまでローカル動作確認用なので軽量化したい場合は、以下の
コメントアウトを解除すると良いでしょう。
<!-- for development <plugin-options> <jboss-maven-dist/> </plugin-options> -->
設定は、こちらを参考に。
まとめ
久しぶりに、WildFlyとArquillianを使いつつ、加えてJUnit 5を使ってインテグレーションテストをやってみました。
Jakarta EE以降になってからの情報が全然なくて、またArquillian自体のドキュメントもあまり更新されていない
ようなので、動かせるようになるまでの情報を揃えるのに苦労しました。
まあ、その過程で最近の事情がいろいろわかったので、良しとしましょうか…。