小ネタ…小ネタってなんだ…と小ネタの定義を考えはじめてはや数日、あっという間に〆切の日になってしまいました
ちょうどカレンダーの概要を見て、先日タイムリーにもSpringアプリケーションの改修時に華麗にハマってしまったことがあったことを思い出し、せっかくですのでちょっと恥ずかしい試行錯誤や推測した内容も含め、備忘として残しておくことにしました。
困った
autoCommit=false
の設定を有効にしたところ、DBを参照する処理のテストがFailするようになってしまった
使用ライブラリ
- Spring Boot(v2.1.6.RELEASE 諸事情により古いです…)
- Spring Data JDBC
- H2 Database(In-memory)
詳細
元々 autoCommit=true
(デフォルト)」で組んでいたアプリケーションでしたが、諸事情で false
にする必要があったため、そちらを修正してテストをぶん回してみたところ発生しました。
元のテストコード(一部抜粋)はこちら。
@Test
@Sql( script = "classpath:sql/testdata.sql",
config = @SqlConfig(dataSource = "db1DataSource"))
public void 指定した条件の検索結果が返ること() throws Exception {
// あれこれテストの準備(省略)
// テスト実行
List<Map<String, Object>> actual = testee.search("aaa", "bbb");
// 検索結果が3件あることを検証
assertThat(actual).hasSize(3); // --- (1)
@Sql
で指定した classpath:sql/testdata.sql
には、検索処理で参照されるデータを登録するSQLが定義されています。また、DBを2種類参照していたのでDataSourceを明示しています。
この内容のままテストを実行すると、上記テストコードの (1)
の箇所で以下のアサーションエラーが出力されました。
java.lang.AssertionError:
Expected size:<3> but was:<0> in:
<[]>
あるぇ。3件ヒットするはずのデータが1件もヒットしていません。
ログを確認してみると、どうやらテストデータが1件も登録されていない模様。そりゃ検索結果0件だわ。
推測
どうやら @Sql
で指定したクエリの内容が autoCommit=false
に変更したことによってcommitされていないようです。さてどうやったらコミットされるのかしら。
やってみた① (間違い)
sqlファイルの最後に commit;
を書いてみた。
結果: 変化なし(そりゃそうだ)
やってみた② (間違い)
とりあえず、@Sql
にてTransactionManagerを指定してみました。
DBが2つあるのでTransactionManagerの定義も2つあるのですが、いつもSpringはどっちを使うか迷った場合はだいたい起動時にエラーになるのになあ、と思いつつ明示してみることに。
@Test
@Sql( script = "classpath:sql/testdata.sql",
config = @SqlConfig(dataSource = "db1DataSource",
transactionManager = "db1TransactionManager"))
public void 指定した条件の検索結果が返ること() throws Exception {
結果: 変化なし
ふーむ、やっぱり autoCommit=false
なのでTransactionManagerだけじゃなくてトランザクション境界を明示的に宣言しないとダメかな
やってみた③ (間違い)
ぱっと @Sql
でトランザクション境界の設定をする方法が思いつかなかったので、とりあえず @Before
で @Transactional
を設定してテストデータの登録処理を呼び出すようにしてみました。単純に、publicメソッドに @Transactional
を指定すればいいんじゃね?という気持ちで。
@Before
@Transactional("db1TransactionManager")
public void before() {
ScriptUtils.executeSqlScript(db1DataSource.getConnection(),
new ClassPathResource("sql/testdata.sql"));
}
@Test
public void 指定した条件の検索結果が返ること() throws Exception {
結果: テスト実行時、検索処理( testee.search()
) で検索クエリがタイムアウトしてしまう
はてなんでだろう、 現象的にはデッドロックされているような状態に似ています。つまり、 @Before
のメソッドで登録処理自体は実行されていそうだけれど、commitはされていない…ということでしょうか?
やっと成功
ふと以前やっていたテストメソッドに対する @Transactional
の仕様を思い出し、テストメソッドに @Transactional
を設定してみました。
@Test
@Sql( script = "classpath:sql/testdata.sql",
config = @SqlConfig(dataSource = "db1DataSource",
transactionManager = "db1TransactionManager"))
@Transactional("db1TransactionManager")
public void 指定した条件の検索結果が返ること() throws Exception {
結果: テスト成功
解説のようなもの
Springでは、@Transactional
を指定したテストメソッドは、処理が終わるとデフォルトでロールバックが実行されます。
H2(In-memory)のように使い捨てのDBを使っている場合はあまり関係がないのでしばらく使っていませんでした(のですっかり忘れていた)。
これは DBにテスト実行時のデータを残さない ための仕様なのだろうと思います。と、いうことは、 テストデータを事前に登録するための @Sql
アノテーションも同じ扱いになっていないとおかしい と考えました。
ドキュメントにも以下の通り記載がありました。
SQL スクリプトは、transactionMode 属性の設定値に応じて、トランザクションなしで、既存の Spring 管理トランザクション(たとえば、@Transactional アノテーションが付けられたテスト用の TransactionalTestExecutionListener によって管理されるトランザクション)、または分離されたトランザクション内で実行されます。
つまり、今回の場合は @Sql
で指定したテストデータの登録からテストメソッドが終了するまで、1つのトランザクション内で処理が行われている状態になっているわけですね。 調べてみると @SqlConfig
のオプションに transactionMode
なども指定できるようになっているので、こちらもあわせて別のトランザクションで実行するなどの制御もできそうです。
ちなみに、 「やってみた③」の @Before
で @Transactional
を設定したときに発生した現象については同ドキュメント内に記載があるのを見つけました。
@Transactional は、テストライフサイクルメソッドではサポートされていないことに注意してください。たとえば、JUnit Jupiter の @BeforeAll、@BeforeEach でアノテーションが付けられたメソッドなどです。
これは確かに、言われてみればなんとなくですが推測はできるかもしれません。 テストライフサイクルメソッドはJUnitの仕組みで呼び出されているメソッドですから、Springがトランザクション管理などを付加する余地は確かになさそうに見えます。
つまり、「やってみた③」では @Transactional
が有効にならなかったため、トランザクション管理なしでDB編集を行うクエリを発行していた…つまりテーブル編集後commitもrollbackも実行せずに、テスト実行で同テーブルを参照しに行っていたので、デッドロックのような状態になっていた…のかな?
残る疑問点
そうすると、1点解せないのは「やってみた②」の結果です。
@Transactional
をテストメソッドに宣言していない場合、状態としては「やってみた③」と同等なのでは?と思ったのですが、実際はDBの編集内容こそ反映されないものの、検索クエリがタイムアウトになることなくテストが終了しています。
とは言え、申し訳ないのですが、こちらは少し調べてもすぐにはわからなかったので、もう少し調べてみようと思います。
わかったこと
- テストメソッドで
@Transactional
を指定すると、処理が成功してもデフォルトではロールバックされる -
@Sql
は、デフォルトでテストメソッドと同じトランザクション管理下で実行される- したがって、テストメソッドに
@Transactional
が設定されている場合、(デフォルトだと)テスト終了時に@Sql
でのDB編集内容も同様にロールバックされる
- したがって、テストメソッドに
- テストライフサイクルメソッドでは
@Transactional
は使えない(設定はできるが無効?)