これは、なにをしたくて書いたもの?
Weld Testing Extensionsを使うと、Jakarta Contexts and Dependency Injection(以降CDI)管理Beanを使ってJUnitのテストを
書けるようなので試してみようかなと。
Weld Testing Extensions
Weld Testing Extensionsは、CDI管理BeanをテストするためのJUnit、SpockのExtensionです。
こちらを使うと、テスト中にWeld SEコンテナを起動してCDIの機能を使ったテストができます。
概要にある通り、JUnit 4、JUnit 5、Spock向けのExtensionが提供されています。
JUnit 5向けのものは、Weld JUnit 5 (Jupiter) Extensionsという名称のようです。
また似た位置づけのものとしてはCDI-Unitというものもあるようです。
CDI-Unit user guide | cdi-unit
GitHub - cdi-unit/cdi-unit: Unit testing for CDI applications
こちらはWeld Testing Extensionsよりも広い機能を持っているようで、Jakarta RESTful Web ServicesやJakarta Enterprise Bean
などへのサポートもあるようです。
Weld JUnit 5 (Jupiter) Extensions
Weld JUnit 5 (Jupiter) Extensionsについて、少しみておきましょう。
Weld JUnit 5 (Jupiter) ExtensionsはJUnit 5向けのWeld Testing Extensionsです。ドキュメントはこちら。
weld-testing/junit5/README.md at 4.0.3.Final · weld/weld-testing · GitHub
Weld JUnit 5 (Jupiter) Extensionsは、その名の通りJUnit 5向けのExtensionを提供しています。そしてWeldJunit5Extensionと
WeldJunit5AutoExtensionという、2種類のExtensionがあります。
There are two extension here, both of which follow the extension mechanism introduced in JUnit 5. Therefore, in order to use this extension in your test, you have to annotate your test class with @ExtendWith(WeldJunit5Extension.class) or @ExtendWith(WeldJunit5AutoExtension.class) respectively.
これらのExtensionは、以下の機能を持ちます。
- Weld SEコンテナーの自動的な起動/停止
- CDI管理Beanを
@Injectが指定されたフィールドや、メソッドのパラメーターにインジェクションする - (ApplicationScoped以外の)スコープのアクティブ化、インターセプターなどをサポートする
またBeanのモック化の機能も備えます。
今回はこちらを使ってみようと思います。
環境
今回の環境はこちら。
$ java --version openjdk 21.0.5 2024-10-15 OpenJDK Runtime Environment (build 21.0.5+11-Ubuntu-1ubuntu124.04) OpenJDK 64-Bit Server VM (build 21.0.5+11-Ubuntu-1ubuntu124.04, mixed mode, sharing) $ mvn --version Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 21.0.5, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "6.8.0-52-generic", arch: "amd64", family: "unix"
準備
まずはテスト対象のアプリケーションを用意します。
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>weld-testing-example</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.wildfly.bom</groupId> <artifactId>wildfly-ee-with-tools</artifactId> <version>35.0.0.Final</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>jakarta.ws.rs</groupId> <artifactId>jakarta.ws.rs-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>jakarta.enterprise</groupId> <artifactId>jakarta.enterprise.cdi-api</artifactId> <scope>provided</scope> </dependency> <!-- あとで --> </dependencies> <build> <finalName>ROOT</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.4.0</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> <plugin> <groupId>org.wildfly.plugins</groupId> <artifactId>wildfly-maven-plugin</artifactId> <version>5.1.1.Final</version> <executions> <execution> <id>package</id> <goals> <goal>package</goal> </goals> </execution> </executions> <configuration> <overwrite-provisioned-server>true</overwrite-provisioned-server> <discover-provisioning-info> <version>35.0.0.Final</version> </discover-provisioning-info> </configuration> </plugin> </plugins> </build> </project>
CDI管理Beanはいくつか用意しましょう。
SessionScoped。
src/main/java/org/littlewings/weld/SessionScopedBean.java
package org.littlewings.weld; import java.io.Serializable; import java.time.LocalDateTime; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.SessionScoped; @SessionScoped public class SessionScopedBean implements Serializable { private static final long serialVersionUID = 1L; private LocalDateTime firstAccessDateTime; private String name; @PostConstruct void init() { firstAccessDateTime = LocalDateTime.now(); } public LocalDateTime getFirstAccessDateTime() { return firstAccessDateTime; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
ApplicationScoped。
src/main/java/org/littlewings/weld/MessageService.java
package org.littlewings.weld; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class MessageService { public String decorate(String value) { return String.format("★★★%s★★★", value); } }
こちらはSessionScopedなCDI管理Beanを使います。
src/main/java/org/littlewings/weld/SessionScopedService.java
package org.littlewings.weld; import java.time.format.DateTimeFormatter; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @ApplicationScoped public class SessionScopedService { @Inject private SessionScopedBean sessionScopedBean; public void setName(String name) { sessionScopedBean.setName(name); } public String getName() { return sessionScopedBean.getName(); } public String getFirstAccessDateTime() { return sessionScopedBean.getFirstAccessDateTime().format(DateTimeFormatter.ISO_DATE_TIME); } }
SessionScopedを加えたことで、とても話がややこしくなりました…。
JAX-RSリソースクラス。
src/main/java/org/littlewings/weld/HelloResource.java
package org.littlewings.weld; import java.util.Map; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; @Path("/hello") @ApplicationScoped public class HelloResource { @Inject private SessionScopedService sessionScopedService; @Inject private MessageService messageService; @GET @Path("/name") @Produces(MediaType.APPLICATION_JSON) public Map<String, String> name(@QueryParam("value") String value) { sessionScopedService.setName(value); return Map.of( "name", sessionScopedService.getName(), "firstAccessDateTime", sessionScopedService.getFirstAccessDateTime() ); } @GET @Path("/get") @Produces(MediaType.APPLICATION_JSON) public Map<String, String> get() { return Map.of( "name", sessionScopedService.getName(), "firstAccessDateTime", sessionScopedService.getFirstAccessDateTime() ); } @GET @Path("/star") @Produces(MediaType.APPLICATION_JSON) public Map<String, String> star(@QueryParam("value") String value) { return Map.of("message", messageService.decorate(value)); } }
JAX-RSの有効化。
src/main/java/org/littlewings/weld/RestApplication.java
package org.littlewings.weld; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; @ApplicationPath("/") public class RestApplication extends Application { }
かなり適当なアプリケーションですが、ひとまず動作確認します。
$ mvn wildfly:run
確認。
$ curl -b cookie.txt -c cookie.txt localhost:8080/hello/name?value=Hello
{"firstAccessDateTime":"2025-02-01T22:09:11.435740571","name":"Hello"}
$ curl -b cookie.txt -c cookie.txt localhost:8080/hello/get
{"firstAccessDateTime":"2025-02-01T22:09:11.435740571","name":"Hello"}
$ curl -b cookie.txt -c cookie.txt localhost:8080/hello/star?value=Hello
{"message":"★★★Hello★★★"}
SessionScopedなCDI管理Beanにはインスタンスが作成された後にその日時を記録しているので、セッションを継続していれば
その時の日時が出力され続けます。
これで準備はOKです。
Weld Testing Extensionsを使ってみる
それでは、Weld Testing Extensionsを使ってみましょう。
ドキュメントはこちらでした。
weld-testing/junit5/README.md at 4.0.3.Final · weld/weld-testing · GitHub
Maven依存関係を追加。
<dependency> <groupId>org.jboss.weld</groupId> <artifactId>weld-junit5</artifactId> <version>4.0.3.Final</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.27.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.15.2</version> <scope>test</scope> </dependency>
最低限必用なのは、weld-junit5ですね。weld-junit5にはJUnit 5への依存関係も含まれています。
Weld JUnit 5 (Jupiter) Extensions / Maven Artifact
ExtensionにはWeldJunit5ExtensionとWeldJunit5AutoExtensionの2種類があるという話でした。
Weld JUnit 5 (Jupiter) Extensions / Configuration Versus Automagic
WeldJunit5ExtensionはWeld SEを起動し、Weldコンテナーはビルダーでカスタマイズできます。このビルダーはテストクラスの
フィールドに定義します。
WeldJunit5AutoExtensionはアノテーションベースのアプローチで、ある仮定のもとにWeldコンテナーを自動構成しようとします。
自動構成がテストにうまくフィットするとは限らないので、カスタマイズができるアノテーションが多数あります。
両方の拡張機能を同時に使うことはできません。
まずはWeldJunit5Extensionから使ってみます。
Weld JUnit 5 (Jupiter) Extensions / WeldJunit5Extension
このExtensionは、以下の機能を持ちます。
- 各テストの実行前にWeld SEコンテナーを開始する
- テストクラスの
@Injectアノテーションが付与されたフィールドにCDI管理Beanをインジェクションする - テストメソッドの引数にCDI管理Beanをインジェクションする
- 引数の型が解決可能なCDI管理Beanと一致する場合
- デフォルトでは貪欲にインジェクションしようとするため、特にパラメタライズドテストと競合するようなら動作を変更できる
- テストが完了したら、Weld SEコンテナーを停止する
ちなみに、@ExtendWith(WeldJunit5AutoExtension.class)と@EnableWeldは同じ意味を持ちます。
もっとも単純な例。
src/test/java/org/littlewings/weld/MessageServiceTest.java
package org.littlewings.weld; import jakarta.inject.Inject; import org.jboss.weld.junit5.auto.WeldJunit5AutoExtension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(WeldJunit5AutoExtension.class) // または // @EnableWeld class MessageServiceTest { @Inject private MessageService messageService; @Test void test() { assertThat(messageService.decorate("hello")) .isEqualTo("★★★hello★★★"); } @Test void inject(MessageService service) { assertThat(service.decorate("hello")) .isEqualTo("★★★hello★★★"); } }
@ExtendWith(WeldJunit5AutoExtension.class)または@EnableWeldが付与されていることがポイントですね。
@ExtendWith(WeldJunit5AutoExtension.class) // または // @EnableWeld class MessageServiceTest {
以降は@EnableWeldを使っていきます。
WeldInitiatorを使ったカスタマイズ。
Weld JUnit 5 (Jupiter) Extensions / WeldJunit5Extension / WeldInitiator
たとえば、テスト用にCDI管理Beanを作成。
src/test/java/org/littlewings/weld/TestMessageService.java
package org.littlewings.weld; import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped public class TestMessageService extends MessageService { @Override public String decorate(String value) { return "override method"; } }
src/test/java/org/littlewings/weld/TestBeanMessageServiceTest.java
package org.littlewings.weld; import jakarta.inject.Inject; import org.jboss.weld.junit5.EnableWeld; import org.jboss.weld.junit5.WeldInitiator; import org.jboss.weld.junit5.WeldSetup; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @EnableWeld public class TestBeanMessageServiceTest { @WeldSetup private WeldInitiator weld = WeldInitiator.ofTestPackage(); //WeldInitiator.from(TestMessageService.class).build(); @Inject private MessageService messageService; @Test void test() { assertThat(messageService.decorate("foo")).isEqualTo("override method"); } }
@WeldSetupアノテーションを付与したWeldInitiatorをフィールドに定義し、Weld SEコンテナーのカスタマイズを行います。
これで、テストコードにあるCDI管理Beanも検出するようになります。特定のCDI管理Beanを指定することもできます。
@WeldSetup private WeldInitiator weld = WeldInitiator.ofTestPackage(); //WeldInitiator.from(TestMessageService.class).build();
次は、スコープのアクティブ化を見ていきます。
デフォルトではApplicationScopedとDependentのみが有効なようなので、以下のようなテストを書くと
src/test/java/org/littlewings/weld/SessionScopedServiceTest.java
package org.littlewings.weld; import jakarta.inject.Inject; import org.jboss.weld.junit5.EnableWeld; import org.junit.jupiter.api.Test; @EnableWeld class SessionScopedServiceTest { @Inject private SessionScopedService sessionScopedService; @Test void test() { } }
SessionScopedに依存しているCDI管理Beanを使っているため、そのままでは実行できません。
org.jboss.weld.exceptions.IllegalArgumentException: WELD-001408: Unsatisfied dependencies for type SessionScopedService with qualifiers @Default at injection point [BackedAnnotatedField] @Inject private org.littlewings.weld.SessionScopedServiceTest.sessionScopedService at org.littlewings.weld.SessionScopedServiceTest.sessionScopedService(SessionScopedServiceTest.java:0)
このような時も、WeldInitiatorを使います。
src/test/java/org/littlewings/weld/SessionScopedServiceWithCustomScopeTest.java
package org.littlewings.weld; import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.context.SessionScoped; import jakarta.inject.Inject; import org.jboss.weld.junit5.EnableWeld; import org.jboss.weld.junit5.WeldInitiator; import org.jboss.weld.junit5.WeldSetup; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @EnableWeld class SessionScopedServiceWithCustomScopeTest { @WeldSetup private WeldInitiator weld = WeldInitiator .from(SessionScopedService.class, SessionScopedBean.class) .activate(RequestScoped.class, SessionScoped.class) .build(); @Inject private SessionScopedService sessionScopedService; @Inject private SessionScopedBean sessionScopedBean; @Test void test() { sessionScopedBean.setName("foo"); assertThat(sessionScopedService.getFirstAccessDateTime()).isNotNull(); assertThat(sessionScopedService.getName()).isEqualTo("foo"); } }
これで、指定したCDI管理Beanに対してRequestScopedとSessionScopedが有効になります。
@WeldSetup private WeldInitiator weld = WeldInitiator .from(SessionScopedService.class, SessionScopedBean.class) .activate(RequestScoped.class, SessionScoped.class) .build();
MockBeanを使ったMockitoとの組み合わせ。
src/test/java/org/littlewings/weld/WithMockitTest.java
package org.littlewings.weld; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import jakarta.enterprise.context.SessionScoped; import jakarta.enterprise.inject.spi.Bean; import org.jboss.weld.junit.MockBean; import org.jboss.weld.junit5.EnableWeld; import org.jboss.weld.junit5.WeldInitiator; import org.jboss.weld.junit5.WeldSetup; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; @EnableWeld public class WithMockitTest { @WeldSetup private WeldInitiator weld = WeldInitiator .from(SessionScopedService.class) .addBeans(createMockBean()) .activate(SessionScoped.class) .build(); @Test void test(SessionScopedService sessionScopedService) { assertThat(sessionScopedService.getName()).isEqualTo("testName"); assertThat(sessionScopedService.getFirstAccessDateTime()).isEqualTo("2025-02-01T21:30:05"); /* SessionScopedの場合はverifyはできなさそう SessionScopedBean sessionScopedBean = weld.select(SessionScopedBean.class).get(); verify(sessionScopedBean, times(1)).getName(); verify(sessionScopedBean, times(1)).getFirstAccessDateTime(); */ } static Bean<?> createMockBean() { SessionScopedBean mockBean = Mockito.mock(SessionScopedBean.class); when(mockBean.getName()).thenReturn("testName"); when(mockBean.getFirstAccessDateTime()).thenReturn(LocalDateTime.of(LocalDate.of(2025, 2, 1), LocalTime.of(21, 30, 5))); return MockBean.builder() .types(SessionScopedBean.class) .scope(SessionScoped.class) .creating(mockBean) .build(); } }
verifyはApplicationScopedなCDI管理Beanなら対象にできそうです。
最後にWeldJunit5AutoExtensionを使ってみます。
Weld JUnit 5 (Jupiter) Extensions / WeldJunit5AutoExtension
先ほどSessionScopedなCDI管理Beanを使う時にはWeldInitiatorでカスタマイズを行いましたが、
@ExtendWith(WeldJunit5AutoExtension.class)または@EnableAutoWeldを使うと以下のように置き換えられます。
src/test/java/org/littlewings/weld/SessionScopedServiceWithCustomScope2Test.java
package org.littlewings.weld; import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.context.SessionScoped; import jakarta.inject.Inject; import org.jboss.weld.junit5.auto.ActivateScopes; import org.jboss.weld.junit5.auto.EnableAutoWeld; import org.jboss.weld.junit5.auto.WeldJunit5AutoExtension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(WeldJunit5AutoExtension.class) // または // @EnableAutoWeld @ActivateScopes({RequestScoped.class, SessionScoped.class}) class SessionScopedServiceWithCustomScope2Test { @Inject private SessionScopedService sessionScopedService; @Inject private SessionScopedBean sessionScopedBean; @Test void test() { sessionScopedBean.setName("foo"); assertThat(sessionScopedService.getFirstAccessDateTime()).isNotNull(); assertThat(sessionScopedService.getName()).isEqualTo("foo"); } }
WeldInitiatorは使いません。デフォルトの動作でうまくいかない場合は、アノテーションでカスタマイズします。
@ExtendWith(WeldJunit5AutoExtension.class) // または // @EnableAutoWeld @ActivateScopes({RequestScoped.class, SessionScoped.class}) class SessionScopedServiceWithCustomScope2Test {
こんなところでしょうか。
おわりに
Weld Testing Extensionsを使って、JUnitのテストコードでWeld SEコンテナーを統合してみました。
CDI管理Beanを使ったテストはこちらを使うと便利だなとは思うのですが、実際に使おうと思うとCDI管理Beanの先には
データベースに依存した機能があったりでそのあたりの統合には苦労するんでしょうかね…。
こういうところも、いずれ確認してみたいなと想います。
なんだかんだで、ちゃんとやろうとするとArquillianを使うことになるかもですが。