これは、なにをしたくて書いたもの?
タイトルどおり、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よりもより細かくライフサイクルに処理を挟み込めるので、やりたいことに応じて使い分ける感じですね。