CLOVER🍀

That was when it all began.

Spring TestのTestExecutionListenerを試す

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

タイトルどおり、Spring TestのTestExecutionListenerというものを試してみようかなと思いまして。

Spring TestのTestExecutionListener

TestExecutionListenerはSpring TestContext Frameworkの一部で、テスト実行時の規定のタイミングで動作するリスナーを設定することが
できます。

Testing / Spring TestContext Framework / TestExecutionListener Configuration

TestExecutionListener自体はインターフェースです。

TestExecutionListener (Spring Framework 6.0.7 API)

TestExecutionListenerインターフェースを実装したクラスを作成し、@TestExecutionListenersアノテーションを使って
テストクラスに対してリスナーを構成します。

Testing / Spring TestContext Framework / TestExecutionListener Configuration / Registering TestExecutionListener Implementations

TestExecutionListeners (Spring Framework 6.0.7 API)

TestExecutionListenerには適用順があり、getOrderメソッドをオーバーライドすることで制御します。

Testing / Spring TestContext Framework / TestExecutionListener Configuration / Ordering TestExecutionListener Implementations

順序の値が大きい方がより後で適用される、ということになります。

TestExecutionListenerインターフェースの実装は、デフォルトのものが用意されており、以下が適用済みになっています。

リスナークラス名 説明 order
ServletTestExecutionListener WebApplicationContext向けにServlet APIのモックを構成する 1000
DirtiesContextBeforeModesTestExecutionListener @DirtiesContextアノテーションの"before"モードを扱う 1500
ApplicationEventsTestExecutionListener ApplicationEventsのサポートを提供する 1800
DependencyInjectionTestExecutionListener テストインスタンスのDIサポートを提供する 2000
DirtiesContextTestExecutionListener @DirtiesContextアノテーションの"after"モードを扱う 3000
TransactionalTestExecutionListener @Transactionalアノテーションを付与したテストを、デフォルトでロールバックさせる 4000
SqlScriptsTestExecutionListener @Sqlアノテーションで設定されたSQLスクリプトを実行する 5000
EventPublishingTestExecutionListener テスト実行イベントをテスト用のApplicationContextに発行する 10000

Spring Testで提供される機能の中には、これらのTestExecutionListenerの実装により提供されるものもあるため、ドキュメント内に
上記のリスナー名が時々出てきます。

TestExecutionListenerの実装を作成したら、以下のように@TestExecutionListenersアノテーションで指定するのですが。

@SpringBootTest
@TestExecutionListeners(
        listeners = XxxTestExecutionListener.class
)
public class XxxTest {

これだとデフォルトで提供されているリスナーがすべて外れてしまうので、デフォルトのものも含めて全部列挙するか、デフォルトの
リスナー群とマージするかを選ぶことになります。

大抵の場合は、デフォルトのリスナー群をマージすることになるのではと思います。その方法については、以下に書いてあります。

Testing / Spring TestContext Framework / TestExecutionListener Configuration / Merging TestExecutionListener Implementations

以下のように、MergeMode#MERGE_WITH_DEFAULTSを指定します。

@SpringBootTest
@TestExecutionListeners(
    listeners = MyCustomTestExecutionListener.class,
    mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
)
class MyTest {
    // class body...
}

なお、デフォルトはREPLACE_DEFAULTSなので、デフォルト実装とマージする場合は明示的に指定する必要があります。

ここまでTestExecutionListenerの外観について見てきましたが、どんなタイミングに処理を挟めるかを見ていませんでしたね。
これについてはJavadocを読むしかなさそうです。

TestExecutionListener (Spring Framework 6.0.7 API)

以下のメソッドがあります。引数はいずれもTestContextです。

  • beforeTestClass … テストを実行する前(テストクラスのインスタンス化の前、@BeforeAllがある場合はその前)に処理を行う
  • prepareTestInstance … テストクラスのインスタンス化後に処理を行う
  • beforeTestMethod … テストメソッドの実行前のライフサイクルのコールバックの実行前(@BeforeEachがある場合はその前)に処理を行う
  • beforeTestExecution … テストメソッドの実行前に処理を行う
  • afterTestExecution … テストメソッドの実行後に処理を行う
  • afterTestMethod … テストメソッドの実行後のライフサイクルのコールバックの実行後(@AfterEachがある場合はその後)に処理を行う
  • afterTestClass … クラス内のすべてのテストの実行後(@AfterAllがある場合はその前)に処理を行う

ここまでドキュメントを読んでみましたが、実際に動かした方が早い気がするので自分でTestExecutionListenerを作ってみましょう。

まずは各動作タイミングを確認するだけのTestExecutionListenerの実装と、テストメソッドに@Transactionalを付与してロールバック
される前に処理を行うTestExecutionListenerの実装を作ってみることにします。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.6 2023-01-17
OpenJDK Runtime Environment (build 17.0.6+10-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.6+10-Ubuntu-0ubuntu122.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.1 (2e178502fcdbffc201671fb2537d0cb4b4cc58f8)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.6, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-67-generic", arch: "amd64", family: "unix"

データベースには、MySQLを使用します。

 MySQL  localhost:3306 ssl  practice  SQL > select version();
+-----------+
| version() |
+-----------+
| 8.0.32    |
+-----------+
1 row in set (0.0285 sec)

MySQLは172.17.0.2で動作しているものとし、データベースpractice、アカウントはkazuhira/passwordで接続できるものと
します。

Spring Bootプロジェクトを作成する

まずはSpring Bootプロジェクトを作成します。依存関係にはjdbcとmysqlを加えます。

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=3.0.5 \
  -d javaVersion=17 \
  -d type=maven-project \
  -d name=test-execution-listener-example \
  -d groupId=org.littlewings \
  -d artifactId=test-execution-listener-example \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=org.littlewings.spring.test \
  -d dependencies=jdbc,mysql \
  -d baseDir=test-execution-listener-example | tar zxvf -

ディレクトリ内に移動。

$ cd test-execution-listener-example

Maven依存関係など。

        <properties>
                <java.version>17</java.version>
        </properties>
        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-jdbc</artifactId>
                </dependency>

                <dependency>
                        <groupId>com.mysql</groupId>
                        <artifactId>mysql-connector-j</artifactId>
                        <scope>runtime</scope>
                </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
        </dependencies>

        <build>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>

生成されたソースコードは、削除しておきます。

$ rm src/main/java/org/littlewings/spring/test/TestExecutionListenerExampleApplication.java src/test/java/org/littlewings/spring/test/TestExecutionListenerExampleApplicationTests.java

mainメソッドを持ったクラスは再定義しておきました。

src/main/java/org/littlewings/spring/test/App.java

package org.littlewings.spring.test;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

はじめてのTestExecutionListener

最初は簡単なTestExecutionListenerの実装を作成してみましょう。

依存関係にSpring JDBCがあるままだとデータベース接続設定が必要になるので、ここではいったん外してspring-boot-starterを
追加しておきます。

     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!--
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-jdbc</artifactId>
       </dependency>

       <dependency>
           <groupId>com.mysql</groupId>
           <artifactId>mysql-connector-j</artifactId>
           <scope>runtime</scope>
       </dependency>
       -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

TestExecutionListenerの実装を作成するには、AbstractTestExecutionListenerクラスを継承して作るのがよいみたいです。

AbstractTestExecutionListenerクラスは、すべてのメソッドが空実装で、getOrderは最も低い値(Integer#MIN_VALUE)で実装された
クラスです。

AbstractTestExecutionListener (Spring Framework 6.0.7 API)

今回はすべてのメソッドを実装し、呼び出されたログ出力するTestExecutionListenerの実装を作成しました。

src/test/java/org/littlewings/spring/test/listener/LoggingTestExecutionListener.java

package org.littlewings.spring.test.listener;

import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;

public class LoggingTestExecutionListener extends AbstractTestExecutionListener {
    @Override
    public int getOrder() {
        return 20000;
    }

    @Override
    public void beforeTestClass(TestContext testContext) throws Exception {
        log(
                testContext.getTestClass().getSimpleName(),
                "none",
                "none",
                "call, beforeTestClasses"
        );
    }

    @Override
    public void prepareTestInstance(TestContext testContext) throws Exception {
        log(
                testContext.getTestClass().getSimpleName(),
                testContext.getTestInstance().toString(),
                "none",
                "call, prepareTestInstance"
        );
    }

    @Override
    public void beforeTestMethod(TestContext testContext) throws Exception {
        log(
                testContext.getTestClass().getSimpleName(),
                testContext.getTestInstance().toString(),
                testContext.getTestMethod().getName(),
                "call, beforeTestMethod"
        );
    }

    @Override
    public void beforeTestExecution(TestContext testContext) throws Exception {
        log(
                testContext.getTestClass().getSimpleName(),
                testContext.getTestInstance().toString(),
                testContext.getTestMethod().getName(),
                "call, beforeTestExecution"
        );
    }

    @Override
    public void afterTestExecution(TestContext testContext) throws Exception {
        log(
                testContext.getTestClass().getSimpleName(),
                testContext.getTestInstance().toString(),
                testContext.getTestMethod().getName(),
                "call, afterTestExecution"
        );
    }

    @Override
    public void afterTestMethod(TestContext testContext) throws Exception {
        log(
                testContext.getTestClass().getSimpleName(),
                testContext.getTestInstance().toString(),
                testContext.getTestMethod().getName(),
                "call, afterTestMethod"
        );
    }

    @Override
    public void afterTestClass(TestContext testContext) throws Exception {
        log(
                testContext.getTestClass().getSimpleName(),
                "none",
                "none",
                "call, afterTestClass"
        );
    }

    void log(String testClassSimpleName, String testInstanceToString, String testMethodName, String message) {
        System.out.printf(
                "[%s/%s:%s] %s%n",
                testClassSimpleName,
                testInstanceToString,
                testMethodName,
                message
        );
    }
}

どのメソッドの引数もTestContextで、テスト対象のクラスやメソッド、テスト用のApplicationContextの取得ができます。

TestContext (Spring Framework 6.0.7 API)

今回はこちらを使って、テストクラスやメソッドの情報をログ出力しています。ただ、タイミングによっては取得できないものも
あるので(たとえば、beforeTestClassのタイミングだとテストクラスのインスタンスがなく、テストメソッドも決まらない)、
そういったケースでは固定値noneを出力するようにしています。

順番は、デフォルトの実装よりも後ろにくるようにしておきました。

    @Override
    public int getOrder() {
        return 20000;
    }

こちらを適用したテストクラスを作成。

src/test/java/org/littlewings/spring/test/SampleTest.java

package org.littlewings.spring.test;

import org.junit.jupiter.api.*;
import org.littlewings.spring.test.listener.LoggingTestExecutionListener;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestExecutionListeners;

@SpringBootTest
@TestExecutionListeners(
        listeners = {LoggingTestExecutionListener.class},
        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
)
public class SampleTest {
    public SampleTest() {
        System.out.println("new test instance");
    }

    @BeforeAll
    static void setUpAll() {
        System.out.println("before all");
    }

    @AfterAll
    static void tearDownAll() {
        System.out.println("after all");
    }

    @BeforeEach
    void setUp() {
        System.out.println("before each");
    }

    @AfterEach
    void tearDown() {
        System.out.println("after each");
    }

    @Test
    void test1() {
        System.out.println("test1");
    }

    @Test
    void test2() {
        System.out.println("test2");
    }
}

作成したTestExecutionListenerを@TestExecutionListenersアノテーションに指定し、デフォルトのTestExecutionListenerリスナー
実装とマージするようにしています。

@SpringBootTest
@TestExecutionListeners(
        listeners = {LoggingTestExecutionListener.class},
        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
)
public class SampleTest {

あとは、各種コールバックメソッドを実装し、テストメソッドを2つ実装しています。

この状態でテストを実行すると、以下のような出力になります。
※関係のある部分のみの抜粋

最初のテストメソッドの実行。

[MyTest/none:none] call, beforeTestClasses
before all
new test instance
22:45:54.406 [main] DEBUG org.springframework.test.context.support.DependencyInjectionTestExecutionListener -- Performing dependency injection for test class org.littlewings.spring.test.MyTest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.5)


〜省略〜

[MyTest/org.littlewings.spring.test.MyTest@5d235104:none] call, prepareTestInstance
[MyTest/org.littlewings.spring.test.MyTest@5d235104:test1] call, beforeTestMethod
before each
[MyTest/org.littlewings.spring.test.MyTest@5d235104:test1] call, beforeTestExecution
test1
[MyTest/org.littlewings.spring.test.MyTest@5d235104:test1] call, afterTestExecution
after each
[MyTest/org.littlewings.spring.test.MyTest@5d235104:test1] call, afterTestMethod
new test instance
[MyTest/org.littlewings.spring.test.MyTest@c755b2:none] call, prepareTestInstance
[MyTest/org.littlewings.spring.test.MyTest@c755b2:test2] call, beforeTestMethod
before each
[MyTest/org.littlewings.spring.test.MyTest@c755b2:test2] call, beforeTestExecution
test2
[MyTest/org.littlewings.spring.test.MyTest@c755b2:test2] call, afterTestExecution
after each
[MyTest/org.littlewings.spring.test.MyTest@c755b2:test2] call, afterTestMethod
after all
[MyTest/none:none] call, afterTestClass

2つ目のテストメソッドの実行。

[SampleTest/none:none] call, beforeTestClasses
before all
new test instance
[SampleTest/org.littlewings.spring.test.SampleTest@142213d5:none] call, prepareTestInstance
[SampleTest/org.littlewings.spring.test.SampleTest@142213d5:test1] call, beforeTestMethod
before each
[SampleTest/org.littlewings.spring.test.SampleTest@142213d5:test1] call, beforeTestExecution
test1
[SampleTest/org.littlewings.spring.test.SampleTest@142213d5:test1] call, afterTestExecution
after each
[SampleTest/org.littlewings.spring.test.SampleTest@142213d5:test1] call, afterTestMethod
new test instance
[SampleTest/org.littlewings.spring.test.SampleTest@934b52f:none] call, prepareTestInstance
[SampleTest/org.littlewings.spring.test.SampleTest@934b52f:test2] call, beforeTestMethod
before each
[SampleTest/org.littlewings.spring.test.SampleTest@934b52f:test2] call, beforeTestExecution
test2
[SampleTest/org.littlewings.spring.test.SampleTest@934b52f:test2] call, afterTestExecution
after each
[SampleTest/org.littlewings.spring.test.SampleTest@934b52f:test2] call, afterTestMethod
after all
[SampleTest/none:none] call, afterTestClass

これで、実行順が確認できましたね。

@Transactionalが付与されていることを前提にしたTestExecutionListenerを作成する

次は、テストメソッドに@Transactionalアノテーションが付与されていることを前提に、トランザクションがロールバックされる前に
処理を行うTestExecutionListenerの実装を作成してみようと思います。

spring-boot-starter-jdbcとmysqlの依存関係を再び追加します。

 <dependencies>
        <!--
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter</artifactId>
       </dependency>
       -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

テーブルは、schema.sqlで作成することにします。お題は書籍で。

src/main/resources/schema.sql

create table if not exists book(
  isbn varchar(14),
  title varchar(100),
  price int,
  primary key(isbn)
);

データベースの接続、SQLの実行、ログレベル設定。

src/main/resources/application.properties

spring.datasource.url=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin
spring.datasource.username=kazuhira
spring.datasource.password=password

spring.sql.init.mode=always

logging.level.org.springframework.jdbc=DEBUG

エンティティ的なクラス。

src/main/java/org/littlewings/spring/test/Book.java

package org.littlewings.spring.test;

public class Book {
    private String isbn;
    private String title;
    private Integer price;

    public static Book create(String isbn, String title, Integer price) {
        Book book = new Book();
        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);

        return book;
    }

    // getter/setterは省略
}

Serviceクラスも作成。

src/main/java/org/littlewings/spring/test/BookService.java

package org.littlewings.spring.test;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Map;

@Service
@Transactional
public class BookService {
    NamedParameterJdbcTemplate jdbcTemplate;

    public BookService(NamedParameterJdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void add(Book book) {
        jdbcTemplate.update(
                """
                        insert into book(isbn, title, price)
                        values(:isbn, :title, :price)""",
                new BeanPropertySqlParameterSource(book)
        );
    }

    public Book findByIsbn(String isbn) {
        return jdbcTemplate.queryForObject(
                """
                        select isbn, title, price
                        from book
                        where isbn = :isbn""",
                Map.of("isbn", isbn),
                new BeanPropertyRowMapper<>(Book.class)
        );
    }
}

1件登録、1件取得。今回最低限使うものだけです。

TestExecutionListenerを作成します。

src/test/java/org/littlewings/spring/test/listener/WithTestTransactionExecutionListener.java

package org.littlewings.spring.test.listener;

import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

public class WithTestTransactionExecutionListener extends AbstractTestExecutionListener {
    @Override
    public int getOrder() {
        return 30000;
    }

    @Override
    public void afterTestMethod(TestContext testContext) throws Exception {
        System.out.println("WithTestTransactionExecutionListener:afterTestMethod");

        NamedParameterJdbcTemplate jdbcTemplate =
                testContext.getApplicationContext().getBean(NamedParameterJdbcTemplate.class);

        assertThat(jdbcTemplate.queryForObject(
                """
                        select title
                        from book
                        where isbn = :isbn""",
                Map.of("isbn", "978-4798142470"),
                String.class))
                .isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発");
        assertThat(jdbcTemplate.queryForObject(
                """
                        select title
                        from book
                        where isbn = :isbn""",
                Map.of("isbn", "978-4774182179"),
                String.class))
                .isEqualTo("[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ");
    }
}

順番は、少なくともTransactionalTestExecutionListenerの4000より大きい必要があります。だいぶ大きくしましたが。

    @Override
    public int getOrder() {
        return 30000;
    }

オーバーライドするメソッドは、afterTestMethodにしました。これは、TransactionalTestExecutionListenerがロールバックを行うのが
afterTestMethodなのでこれに合わせています。

    @Override
    public void afterTestMethod(TestContext testContext) throws Exception {

実装としては、呼び出されていることの確認と、アサーション。

    @Override
    public void afterTestMethod(TestContext testContext) throws Exception {
        System.out.println("WithTestTransactionExecutionListener:afterTestMethod");

        NamedParameterJdbcTemplate jdbcTemplate =
                testContext.getApplicationContext().getBean(NamedParameterJdbcTemplate.class);

        assertThat(jdbcTemplate.queryForObject(
                """
                        select title
                        from book
                        where isbn = :isbn""",
                Map.of("isbn", "978-4798142470"),
                String.class))
                .isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発");
        assertThat(jdbcTemplate.queryForObject(
                """
                        select title
                        from book
                        where isbn = :isbn""",
                Map.of("isbn", "978-4774182179"),
                String.class))
                .isEqualTo("[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ");
    }

テストメソッド終了時にロールバックされる前であれば、コミット前のデータが見えるはずです。

テストコード。

src/test/java/org/littlewings/spring/test/TransactionalListenerTest.java

package org.littlewings.spring.test;

import org.junit.jupiter.api.Test;
import org.littlewings.spring.test.listener.WithTestTransactionExecutionListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.transaction.annotation.Transactional;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@TestExecutionListeners(
        listeners = {WithTestTransactionExecutionListener.class},
        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
)
public class TransactionalListenerTest {
    @Autowired
    BookService bookService;

    @Test
    @Transactional
    void withTransaction() {
        bookService.add(Book.create("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4400));
        bookService.add(Book.create("978-4774182179", "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", 4180));

        assertThat(bookService.findByIsbn("978-4798142470").getTitle())
                .isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発");
        assertThat(bookService.findByIsbn("978-4774182179").getTitle())
                .isEqualTo("[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ");
    }
}

@Transactionalアノテーションを付与しているので、テストメソッド終了時にロールバックします。

    @Test
    @Transactional
    void withTransaction() {

この状態で、テストを実行してみましょう。

作成したTestExecutionListenerが適用されているログが出力されます。

00:49:04.546 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener -- Before test class: class [TransactionalListenerTest], class annotated with @DirtiesContext [false] with mode [null]

Spring JDBCのログレベルをDEBUGにしているので、今回作成したTestExecutionListenerの後にロールバックされていることが
確認できます。

WithTestTransactionExecutionListener:afterTestMethod
2023-03-25T00:49:07.497+09:00 DEBUG 95328 --- [           main] o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL query
2023-03-25T00:49:07.497+09:00 DEBUG 95328 --- [           main] o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [select title
from book
where isbn = ?]
2023-03-25T00:49:07.499+09:00 DEBUG 95328 --- [           main] o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL query
2023-03-25T00:49:07.499+09:00 DEBUG 95328 --- [           main] o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [select title
from book
where isbn = ?]
2023-03-25T00:49:07.501+09:00 DEBUG 95328 --- [           main] o.s.jdbc.support.JdbcTransactionManager  : Initiating transaction rollback
2023-03-25T00:49:07.501+09:00 DEBUG 95328 --- [           main] o.s.jdbc.support.JdbcTransactionManager  : Rolling back JDBC transaction on Connection [HikariProxyConnection@773301025 wrapping com.mysql.cj.jdbc.ConnectionImpl@66b59b7d]
2023-03-25T00:49:07.521+09:00 DEBUG 95328 --- [           main] o.s.jdbc.support.JdbcTransactionManager  : Releasing JDBC Connection [HikariProxyConnection@773301025 wrapping com.mysql.cj.jdbc.ConnectionImpl@66b59b7d] after transaction

データも残っていませんね。

 MySQL  localhost:3306 ssl  practice  SQL > select * from book;
Empty set (0.0760 sec)

ここで試しに、TransactionalTestExecutionListenerよりも先に適用されるようにしてみましょう。

public class WithTestTransactionExecutionListener extends AbstractTestExecutionListener {
    @Override
    public int getOrder() {
        //return 30000;
        return 300;
    }

この状態でテストを実行すると、ロールバックが先に行われるためTestExecutionListener内でのアサーションに失敗します。

2023-03-25T00:52:42.730+09:00 DEBUG 95575 --- [           main] o.s.jdbc.support.JdbcTransactionManager  : Initiating transaction rollback
2023-03-25T00:52:42.730+09:00 DEBUG 95575 --- [           main] o.s.jdbc.support.JdbcTransactionManager  : Rolling back JDBC transaction on Connection [HikariProxyConnection@1972628089 wrapping com.mysql.cj.jdbc.ConnectionImpl@31c628e7]
2023-03-25T00:52:42.743+09:00 DEBUG 95575 --- [           main] o.s.jdbc.support.JdbcTransactionManager  : Releasing JDBC Connection [HikariProxyConnection@1972628089 wrapping com.mysql.cj.jdbc.ConnectionImpl@31c628e7] after transaction
WithTestTransactionExecutionListener:afterTestMethod
2023-03-25T00:52:42.745+09:00 DEBUG 95575 --- [           main] o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL query
2023-03-25T00:52:42.745+09:00 DEBUG 95575 --- [           main] o.s.jdbc.core.JdbcTemplate               : Executing prepared SQL statement [select title
from book
where isbn = ?]
2023-03-25T00:52:42.745+09:00 DEBUG 95575 --- [           main] o.s.jdbc.datasource.DataSourceUtils      : Fetching JDBC Connection from DataSource
2023-03-25T00:52:42.747+09:00  WARN 95575 --- [           main] o.s.test.context.TestContextManager      : Caught exception while invoking 'afterTestMethod' callback on TestExecutionListener [org.littlewings.spring.test.listener.WithTestTransactionExecutionListener] for test method [void org.littlewings.spring.test.TransactionalListenerTest.withTransaction()] and test instance [org.littlewings.spring.test.TransactionalListenerTest@58687fb7]

org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
        at org.springframework.dao.support.DataAccessUtils.nullableSingleResult(DataAccessUtils.java:97) ~[spring-tx-6.0.7.jar:6.0.7]
        at org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.queryForObject(NamedParameterJdbcTemplate.java:244) ~[spring-jdbc-6.0.7.jar:6.0.7]
        at org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.queryForObject(NamedParameterJdbcTemplate.java:252) ~[spring-jdbc-6.0.7.jar:6.0.7]
        at org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.queryForObject(NamedParameterJdbcTemplate.java:268) ~[spring-jdbc-6.0.7.jar:6.0.7]
        at org.littlewings.spring.test.listener.WithTestTransactionExecutionListener.afterTestMethod(WithTestTransactionExecutionListener.java:25) ~[test-classes/:na]
        at org.springframework.test.context.TestContextManager.afterTestMethod(TestContextManager.java:440) ~[spring-test-6.0.7.jar:6.0.7]
        at org.springframework.test.context.junit.jupiter.SpringExtension.afterEach(SpringExtension.java:206) ~[spring-test-6.0.7.jar:6.0.7]

〜省略〜

これで、確認したいことはできたかなと思います。

ちなみに、実は今回の例だとオーバーライドするメソッドをafterTestExecutionにすると、TransactionalTestExecutionListenerより前に
適用されるようにしてもテストに失敗しなくなったりします。
afterTestExecutionは、afterTestMethodよりも前に動作するからですね。

今回は、適用順の確認ということで。

まとめ

Spring TestのTestExecutionListenerを試してみました。

存在を知らなかったので、Spring Frameworkでのテストの拡張方法のひとつを知る良い機会になりました。Spring Framework用には
なりますが、JUnit 5よりもより細かくライフサイクルに処理を挟み込めるので、やりたいことに応じて使い分ける感じですね。