これは、なにをしたくて書いたもの?
Database RiderとSpring Boot(Spring Framework)を合わせて使っていて、@DirtiesContext
アノテーションを使うと途端にテストが
失敗するようになってハマったのでメモ。
どういうことか?
Mockitoを使った時など、Spring FrameworkのApplicationContext
を破棄したい時に@DirtiesContext
アノテーションを使うことがあります。
Testing / Appendix / Annotations / Spring Testing Annotations / @DirtiesContext
DirtiesContext (Spring Framework 6.0.7 API)
@DirtiesContext
アノテーションを使うと規定のタイミングでApplicationContext
をダーティであるとマークし、破棄することが
できます。
例としては、シングルトンであるSpringで管理されたBeanの状態変更を行った時、組み込みデータベースの状態変更を行った時などが
挙げられています。
@DirtiesContext
アノテーションはクラスまたはメソッドに付与することができ(それぞれクラスモード、メソッドモードとして
扱われます)、ダーティとマークするタイミングを以下のように制御することができます。
- 現在のテストクラスの前(
BEFORE_CLASS
/クラスモード) - 現在のテストクラスの各テストメソッドの実行前(
BEFORE_EACH_TEST_METHOD
/クラスモード) - 現在のテストメソッドの実行前(
BEFORE_METHOD
/メソッドモード) - 現在のテストメソッドの実行後(
AFTER_METHOD
/メソッドモード) - 現在のクラスの各テストメソッドの実行後(
AFTER_EACH_TEST_METHOD
/クラスモード) - 現在のテストクラスの後(
AFTER_CLASS
/クラスモード)
デフォルトは、クラスモードの場合はAFTER_CLASS
、メソッドモードの場合はAFTER_METHOD
です。
これと関連する話題がApplicationContext
のキャッシュなのですが、それはこちらに書かれています。
Testing / Spring TestContext Framework / Context Management / Context Caching
で、これとDatabase Riderがどう関係があるのかということですが、Database Riderはデフォルトでデータベース接続をキャッシュします。
Database Rider Documentation / Configuration / DBUnit configuration
cacheConnection
というプロパティですね。こちらがデフォルトでtrue
になっています。
Database connection will be reused among tests
データベース接続がテストで再利用される、と書かれていますね。
@DirtiesContext
でApplicationContext
を破棄するようにした場合、Database RiderがSpring Framework経由で取得している
データベース接続が無効になり、Database Riderを使ったテストが軒並み失敗していくようになります。
Rider JUnit 5モジュール、Rider Springモジュールのどちらを使っても発生します。
このあたりのissueを見ていて、問題を把握しました。
Issue using @ExpectedDataSet without @DataSet · Issue #136 · database-rider/database-rider · GitHub
この問題にぶつかった場合は、cacheConnection
をfalse
にすることが回避策になります。
このあたりを見ていきたいと思います。
環境
今回の環境は、こちら。
$ 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-69-generic", arch: "amd64", family: "unix"
データベースには、MySQLを使用します。
MySQL localhost:3306 ssl practice SQL > select version(); +-----------+ | version() | +-----------+ | 8.0.32 | +-----------+ 1 row in set (0.0027 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=database-rider-with-dirtiescontext \ -d groupId=org.littlewings \ -d artifactId=database-rider-with-dirtiescontext \ -d version=0.0.1-SNAPSHOT \ -d packageName=org.littlewings.spring.dbrider \ -d dependencies=jdbc,mysql \ -d baseDir=database-rider-with-dirtiescontext | tar zxvf -
ディレクトリ内に移動。
$ cd database-rider-with-dirtiescontext
生成された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/dbrider/DatabaseRiderWithDirtiescontextApplication.java src/test/java/org/littlewings/spring/dbrider/DatabaseRiderWithDirtiescontextApplicationTests.java
ここに、Rider JUnit 5とRider Springの2つのモジュールを追加。
<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> <dependency> <groupId>com.github.database-rider</groupId> <artifactId>rider-junit5</artifactId> <version>1.36.0</version> <scope>test</scope> </dependency> <dependency> <groupId>com.github.database-rider</groupId> <artifactId>rider-spring</artifactId> <version>1.36.0</version> <scope>test</scope> </dependency> </dependencies>
続いて、テーブル定義やソースコードを作成していきます。
今回使うテーブルは、以下のように定義。お題は書籍にしました。
src/main/resources/schema.sql
drop table if exists book; create table 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
エンティティ的なクラス。
src/main/java/org/littlewings/spring/dbrider/Book.java
package org.littlewings.spring.dbrider; public class Book { String isbn; String title; 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は省略 }
@SpringBootApplication
アノテーションを付与したクラス。
src/main/java/org/littlewings/spring/dbrider/App.java
package org.littlewings.spring.dbrider; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { }
Database Riderを使ってテストを書く
では、Database Riderを使ってテストを書いていきます。
Database Rider(というかDbUnit)の設定。
src/test/resources/dbunit.yml
cacheConnection: true properties: caseSensitiveTableNames: true
cacheConnection
はまずはfalse
にしておきます。
テストは、Spring JDBCを使ったデータ登録とDatabase Riderの@ExpectedDataSet
アノテーションでのアサーションを行う簡単なものに
します。
Rider Springモジュールを使ったパターン。
src/test/java/org/littlewings/spring/dbrider/DatabaseRiderSpringTest.java
package org.littlewings.spring.dbrider; import com.github.database.rider.core.api.dataset.ExpectedDataSet; import com.github.database.rider.spring.api.DBRider; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.test.annotation.DirtiesContext; import org.springframework.transaction.annotation.Transactional; import java.util.List; @SpringBootTest @DBRider public class DatabaseRiderSpringTest { @Autowired NamedParameterJdbcTemplate jdbcTemplate; @Test @Transactional //@DirtiesContext @ExpectedDataSet(value = "datasets/DatabaseRiderSpringTest/expect_addBook1.yml", orderBy = "price") void addBook1() { List<Book> books = List.of( Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860), Book.create("978-4621303252", "Effective Java 第3版", 4400), Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278) ); books.forEach(book -> { System.out.println(book); jdbcTemplate.update(""" insert into book(isbn, title, price) values(:isbn, :title, :price)""", new BeanPropertySqlParameterSource(book)); } ); } @Test @Transactional //@DirtiesContext @ExpectedDataSet(value = "datasets/DatabaseRiderSpringTest/expect_addBook2.yml", orderBy = "price") void addBook2() { List<Book> books = List.of( Book.create("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180), Book.create("978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960) ); books.forEach(book -> { System.out.println(book); jdbcTemplate.update(""" insert into book(isbn, title, price) values(:isbn, :title, :price)""", new BeanPropertySqlParameterSource(book)); } ); } }
Rider Springの場合は@Transactional
アノテーションと連動できます。
@Test @Transactional //@DirtiesContext @ExpectedDataSet(value = "datasets/DatabaseRiderSpringTest/expect_addBook1.yml", orderBy = "price") void addBook1() {
@DirtiesContext
アノテーションは後で有効にします。
src/test/resources/datasets/DatabaseRiderSpringTest/expect_addBook1.yml
book: - isbn: "978-4295008477" title: "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]" price: 2860 - isbn: "978-4774189093" title: "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで" price: 3278 - isbn: "978-4621303252" title: "Effective Java 第3版" price: 4400
src/test/resources/datasets/DatabaseRiderSpringTest/expect_addBook2.yml
book: - isbn: "978-4798147406" title: "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド" price: 3960 - isbn: "978-4798161488" title: "MySQL徹底入門 第4版 MySQL 8.0対応" price: 4180
続いて、Rider JUnit 5を使った場合。
src/test/java/org/littlewings/spring/dbrider/DatabaseRiderJUnit5Test.java
package org.littlewings.spring.dbrider; import com.github.database.rider.core.api.dataset.DataSet; import com.github.database.rider.core.api.dataset.ExpectedDataSet; import com.github.database.rider.junit5.api.DBRider; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.test.annotation.DirtiesContext; import org.springframework.transaction.annotation.Transactional; import java.util.List; @SpringBootTest @DBRider public class DatabaseRiderJUnit5Test { @Autowired NamedParameterJdbcTemplate jdbcTemplate; @Test //@Transactional // Rider JUnit5の場合は@Transactinalは無効 //@DirtiesContext @DataSet(cleanBefore = true) @ExpectedDataSet(value = "datasets/DatabaseRiderJUnit5Test/expect_addBook1.yml", orderBy = "price") void addBook1() { List<Book> books = List.of( Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860), Book.create("978-4621303252", "Effective Java 第3版", 4400), Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278) ); books.forEach(book -> { System.out.println(book); jdbcTemplate.update(""" insert into book(isbn, title, price) values(:isbn, :title, :price)""", new BeanPropertySqlParameterSource(book)); } ); } @Test //@Transactional // Rider JUnit5の場合は@Transactinalは無効 //@DirtiesContext @DataSet(cleanBefore = true) @ExpectedDataSet(value = "datasets/DatabaseRiderJUnit5Test/expect_addBook2.yml", orderBy = "price") void addBook2() { List<Book> books = List.of( Book.create("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180), Book.create("978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960) ); books.forEach(book -> { System.out.println(book); jdbcTemplate.update(""" insert into book(isbn, title, price) values(:isbn, :title, :price)""", new BeanPropertySqlParameterSource(book)); } ); } }
Rider JUnit 5の場合、@Transactional
アノテーションとは連動しないので@DataSet
で先にデータを削除するようにしています。
@Test //@Transactional // Rider JUnit5の場合は@Transactinalは無効 //@DirtiesContext @DataSet(cleanBefore = true) @ExpectedDataSet(value = "datasets/DatabaseRiderJUnit5Test/expect_addBook1.yml", orderBy = "price") void addBook1() {
アサーションで使うデータセットの中身は、Rider Springのものと同じです。
src/test/resources/datasets/DatabaseRiderJUnit5Test/expect_addBook1.yml
book: - isbn: "978-4295008477" title: "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]" price: 2860 - isbn: "978-4774189093" title: "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで" price: 3278 - isbn: "978-4621303252" title: "Effective Java 第3版" price: 4400
src/test/resources/datasets/DatabaseRiderJUnit5Test/expect_addBook2.yml
book: - isbn: "978-4798147406" title: "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド" price: 3960 - isbn: "978-4798161488" title: "MySQL徹底入門 第4版 MySQL 8.0対応" price: 4180
テストを行って動作確認。
$ mvn test
すべてパスします。
[INFO] Results: [INFO] [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
@DirtiesContextアノテーションを加える
この状態で、@DirtiesContext
アノテーションをテストメソッドに追加します(というか、コメントアウトの解除ですね)。
Rider Spring版。
@Test @Transactional @DirtiesContext @ExpectedDataSet(value = "datasets/DatabaseRiderSpringTest/expect_addBook1.yml", orderBy = "price") void addBook1() { List<Book> books = List.of( Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860), Book.create("978-4621303252", "Effective Java 第3版", 4400), Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278) ); books.forEach(book -> { System.out.println(book); jdbcTemplate.update(""" insert into book(isbn, title, price) values(:isbn, :title, :price)""", new BeanPropertySqlParameterSource(book)); } ); } @Test @Transactional @DirtiesContext @ExpectedDataSet(value = "datasets/DatabaseRiderSpringTest/expect_addBook2.yml", orderBy = "price") void addBook2() { List<Book> books = List.of( Book.create("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180), Book.create("978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960) ); books.forEach(book -> { System.out.println(book); jdbcTemplate.update(""" insert into book(isbn, title, price) values(:isbn, :title, :price)""", new BeanPropertySqlParameterSource(book)); } ); }
@DirtiesContext
アノテーションをメソッドに付与しているので、テストメソッドの実行後ごとにダーティとマークされ、
ApplicationContext
が破棄されることになります。
Rider JUnit 5版。
@Test //@Transactional // Rider JUnit5の場合は@Transactinalは無効 @DirtiesContext @DataSet(cleanBefore = true) @ExpectedDataSet(value = "datasets/DatabaseRiderJUnit5Test/expect_addBook1.yml", orderBy = "price") void addBook1() { List<Book> books = List.of( Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860), Book.create("978-4621303252", "Effective Java 第3版", 4400), Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278) ); books.forEach(book -> { System.out.println(book); jdbcTemplate.update(""" insert into book(isbn, title, price) values(:isbn, :title, :price)""", new BeanPropertySqlParameterSource(book)); } ); } @Test //@Transactional // Rider JUnit5の場合は@Transactinalは無効 @DirtiesContext @DataSet(cleanBefore = true) @ExpectedDataSet(value = "datasets/DatabaseRiderJUnit5Test/expect_addBook2.yml", orderBy = "price") void addBook2() { List<Book> books = List.of( Book.create("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180), Book.create("978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960) ); books.forEach(book -> { System.out.println(book); jdbcTemplate.update(""" insert into book(isbn, title, price) values(:isbn, :title, :price)""", new BeanPropertySqlParameterSource(book)); } ); }
これでテストを実行。
$ mvn test
すると、テストの半分が失敗します。
[INFO] [INFO] Results: [INFO] [ERROR] Errors: [ERROR] DatabaseRiderJUnit5Test.addBook2 » Runtime DataSet comparison failed due to fo... [ERROR] DatabaseRiderSpringTest.addBook2 » Runtime Could not get driver information fr... [INFO] [ERROR] Tests run: 4, Failures: 0, Errors: 2, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------
どちらも、同じテストクラス内のテストメソッドの片方が失敗しています。
この時、Rider Springの方は以下のようなスタックトレースが出力されていました。
2023-04-16T23:57:11.724+09:00 WARN 38092 --- [ main] com.zaxxer.hikari.pool.ProxyConnection : HikariPool-3 - Connection com.mysql.cj.jdbc.ConnectionImpl@32dcfeea marked as broken because of SQLSTATE(08003), ErrorCode(0) java.sql.SQLNonTransientConnectionException: No operations allowed after connection closed. at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:110) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:89) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:63) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:73) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:73) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.ConnectionImpl.createStatement(ConnectionImpl.java:1089) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.ConnectionImpl.createStatement(ConnectionImpl.java:1079) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.zaxxer.hikari.pool.ProxyConnection.createStatement(ProxyConnection.java:284) ~[HikariCP-5.0.1.jar:na] at com.zaxxer.hikari.pool.HikariProxyConnection.createStatement(HikariProxyConnection.java) ~[HikariCP-5.0.1.jar:na] at com.github.database.rider.core.dataset.DataSetExecutorImpl.handleConstraints(DataSetExecutorImpl.java:344) ~[rider-core-1.36.0.jar:na] at com.github.database.rider.core.dataset.DataSetExecutorImpl.disableConstraints(DataSetExecutorImpl.java:332) ~[rider-core-1.36.0.jar:na] at com.github.database.rider.core.dataset.DataSetExecutorImpl.clearDatabase(DataSetExecutorImpl.java:576) ~[rider-core-1.36.0.jar:na] at com.github.database.rider.core.dataset.DataSetExecutorImpl.createDataSet(DataSetExecutorImpl.java:108) ~[rider-core-1.36.0.jar:na] at com.github.database.rider.core.RiderRunner.runBeforeTest(RiderRunner.java:44) ~[rider-core-1.36.0.jar:na] at com.github.database.rider.junit5.DBUnitExtension.beforeTestExecution(DBUnitExtension.java:69) ~[rider-junit5-1.36.0.jar:na] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeTestExecutionCallbacks$5(TestMethodTestDescriptor.java:191) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeMethodsOrCallbacksUntilExceptionOccurs$6(TestMethodTestDescriptor.java:202) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(TestMethodTestDescriptor.java:202) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeTestExecutionCallbacks(TestMethodTestDescriptor.java:190) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:136) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) ~[na:na] at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) ~[na:na] at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:220) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:188) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:202) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:181) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:150) ~[surefire-junit-platform-2.22.2.jar:2.22.2] at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:124) ~[surefire-junit-platform-2.22.2.jar:2.22.2] at org.apache.maven.surefire.booter.ForkedBooter.invokeProviderInSameClassLoader(ForkedBooter.java:384) ~[surefire-booter-2.22.2.jar:2.22.2] at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:345) ~[surefire-booter-2.22.2.jar:2.22.2] at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:126) ~[surefire-booter-2.22.2.jar:2.22.2] at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:418) ~[surefire-booter-2.22.2.jar:2.22.2] Caused by: com.mysql.cj.exceptions.ConnectionIsClosedException: No operations allowed after connection closed. at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:na] at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77) ~[na:na] at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:na] at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) ~[na:na] at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) ~[na:na] at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:151) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.NativeSession.checkClosed(NativeSession.java:761) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:564) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.ConnectionImpl.createStatement(ConnectionImpl.java) ~[mysql-connector-j-8.0.32.jar:8.0.32] ... 58 common frames omitted
データベース接続はすでに閉じています、ということですね。
Caused by: com.mysql.cj.exceptions.ConnectionIsClosedException: No operations allowed after connection closed.
Rider JUnit 5の場合は@DataSet
アノテーションを使った処理で失敗しているようでした。
2023-04-16T23:57:11.725+09:00 WARN 38092 --- [ main] c.g.d.r.c.dataset.DataSetExecutorImpl : Could not clean database before test. java.sql.SQLNonTransientConnectionException: No operations allowed after connection closed. at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:110) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:89) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:63) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:73) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:73) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.ConnectionImpl.createStatement(ConnectionImpl.java:1089) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.ConnectionImpl.createStatement(ConnectionImpl.java:1079) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.zaxxer.hikari.pool.ProxyConnection.createStatement(ProxyConnection.java:284) ~[HikariCP-5.0.1.jar:na] at com.zaxxer.hikari.pool.HikariProxyConnection.createStatement(HikariProxyConnection.java) ~[HikariCP-5.0.1.jar:na] at com.github.database.rider.core.dataset.DataSetExecutorImpl.handleConstraints(DataSetExecutorImpl.java:344) ~[rider-core-1.36.0.jar:na] at com.github.database.rider.core.dataset.DataSetExecutorImpl.disableConstraints(DataSetExecutorImpl.java:332) ~[rider-core-1.36.0.jar:na] at com.github.database.rider.core.dataset.DataSetExecutorImpl.clearDatabase(DataSetExecutorImpl.java:576) ~[rider-core-1.36.0.jar:na] at com.github.database.rider.core.dataset.DataSetExecutorImpl.createDataSet(DataSetExecutorImpl.java:108) ~[rider-core-1.36.0.jar:na] at com.github.database.rider.core.RiderRunner.runBeforeTest(RiderRunner.java:44) ~[rider-core-1.36.0.jar:na] at com.github.database.rider.junit5.DBUnitExtension.beforeTestExecution(DBUnitExtension.java:69) ~[rider-junit5-1.36.0.jar:na] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeTestExecutionCallbacks$5(TestMethodTestDescriptor.java:191) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeMethodsOrCallbacksUntilExceptionOccurs$6(TestMethodTestDescriptor.java:202) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(TestMethodTestDescriptor.java:202) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeTestExecutionCallbacks(TestMethodTestDescriptor.java:190) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:136) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) ~[na:na] at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) ~[na:na] at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:220) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:188) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:202) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:181) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:150) ~[surefire-junit-platform-2.22.2.jar:2.22.2] at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:124) ~[surefire-junit-platform-2.22.2.jar:2.22.2] at org.apache.maven.surefire.booter.ForkedBooter.invokeProviderInSameClassLoader(ForkedBooter.java:384) ~[surefire-booter-2.22.2.jar:2.22.2] at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:345) ~[surefire-booter-2.22.2.jar:2.22.2] at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:126) ~[surefire-booter-2.22.2.jar:2.22.2] at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:418) ~[surefire-booter-2.22.2.jar:2.22.2] Caused by: com.mysql.cj.exceptions.ConnectionIsClosedException: No operations allowed after connection closed. at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:na] at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77) ~[na:na] at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:na] at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) ~[na:na] at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) ~[na:na] at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:151) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.NativeSession.checkClosed(NativeSession.java:761) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:564) ~[mysql-connector-j-8.0.32.jar:8.0.32] at com.mysql.cj.jdbc.ConnectionImpl.createStatement(ConnectionImpl.java) ~[mysql-connector-j-8.0.32.jar:8.0.32] ... 58 common frames omitted
どっちにしても、データベース接続が使えなくなったというのは同じですね。
Caused by: com.mysql.cj.exceptions.ConnectionIsClosedException: No operations allowed after connection closed.
実行例はmvn test
で一括実行していますが、今回の構成だとテストクラス単位でも失敗します。
というわけで、@DirtiesContext
アノテーションを使っただけでデフォルトだとDatabase RiderとSpring Frameworkを使ったテストが
不安定になります。
cacheConnectionを無効にする
この対処方法ですが、最初に書いたようにcacheConnection
をfalse
にしてデータベース接続を再利用することをやめます。
src/test/resources/dbunit.yml
cacheConnection: false properties: caseSensitiveTableNames: true
確認。
$ mvn test
こうすると、@DirtiesContext
アノテーションを付与したままでもテストに成功するようになります。
[INFO] Results: [INFO] [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
まとめ
というわけで、Database RiderとSpring Frameworkを合わせて使った時に、@DirtiesContext
アノテーションを使うと設定によっては
ハマるという話でした。
最初、データベース接続を使いまわしているのに気づいてなかったので、解決するのにだいぶ苦労しました…。
設定項目はちゃんと見ておかないといけないですね。