これは、なにをしたくて書いたもの?
タイトルどおり、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
アノテーションを使って
テストクラスに対してリスナーを構成します。
TestExecutionListeners (Spring Framework 6.0.7 API)
TestExecutionListener
には適用順があり、getOrder
メソッドをオーバーライドすることで制御します。
順序の値が大きい方がより後で適用される、ということになります。
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 {
これだとデフォルトで提供されているリスナーがすべて外れてしまうので、デフォルトのものも含めて全部列挙するか、デフォルトの
リスナー群とマージするかを選ぶことになります。
大抵の場合は、デフォルトのリスナー群をマージすることになるのではと思います。その方法については、以下に書いてあります。
以下のように、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よりもより細かくライフサイクルに処理を挟み込めるので、やりたいことに応じて使い分ける感じですね。