以下の内容はhttps://kazuhira-r.hatenablog.com/entry/2025/02/01/224521より取得しました。


Weld Testing ExtensionsとJUnit 5を使って、Jakarta Contexts and Dependency Injection管理Beanのテストをする

これは、なにをしたくて書いたもの?

Weld Testing Extensionsを使うと、Jakarta Contexts and Dependency Injection(以降CDI)管理Beanを使ってJUnitのテストを
書けるようなので試してみようかなと。

Weld Testing Extensions

Weld Testing Extensionsは、CDI管理BeanをテストするためのJUnit、SpockのExtensionです。

GitHubリポジトリーはこちら。

GitHub - weld/weld-testing: Set of test framework extensions (JUnit 4, JUnit 5, Spock) to enhance the testing of CDI components via Weld. Supports Weld 5.

こちらを使うと、テスト中に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にはWeldJunit5ExtensionWeldJunit5AutoExtensionの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なら対象にできそうです。

https://github.com/weld/weld-testing/blob/4.0.3.Final/junit5/src/test/java/org/jboss/weld/junit5/bean/AddBeanTest.java#L102

最後に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を使うことになるかもですが。




以上の内容はhttps://kazuhira-r.hatenablog.com/entry/2025/02/01/224521より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14