Springアプリケーションのテストコードでトランザクションを制御したかった

小ネタ…小ネタってなんだ…と小ネタの定義を考えはじめてはや数日、あっという間に〆切の日になってしまいました:joy:
ちょうどカレンダーの概要を見て、先日タイムリーにもSpringアプリケーションの改修時に華麗にハマってしまったことがあったことを思い出し、せっかくですのでちょっと恥ずかしい試行錯誤や推測した内容も含め、備忘として残しておくことにしました。

## 困った
`autoCommit=false` の設定を有効にしたところ、DBを参照する処理のテストがFailするようになってしまった

### 使用ライブラリ
- Spring Boot(v2.1.6.RELEASE 諸事情により古いです…)
- Spring Data JDBC
- H2 Database(In-memory)

### 詳細
元々 `autoCommit=true` （デフォルト）」で組んでいたアプリケーションでしたが、諸事情で `false` にする必要があったため、そちらを修正してテストをぶん回してみたところ発生しました。

元のテストコード（一部抜粋）はこちら。

```java
    @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はどっちを使うか迷った場合はだいたい起動時にエラーになるのになあ、と思いつつ明示してみることに。

```java
    @Test
    @Sql( script = "classpath:sql/testdata.sql",
        config = @SqlConfig(dataSource = "db1DataSource",
                            transactionManager = "db1TransactionManager"))
    public void 指定した条件の検索結果が返ること() throws Exception {
```

結果： 変化なし

ふーむ、やっぱり `autoCommit=false` なのでTransactionManagerだけじゃなくてトランザクション境界を明示的に宣言しないとダメかな :sweat:

### やってみた③　（間違い）
ぱっと `@Sql` でトランザクション境界の設定をする方法が思いつかなかったので、とりあえず `@Before` で `@Transactional` を設定してテストデータの登録処理を呼び出すようにしてみました。単純に、publicメソッドに `@Transactional` を指定すればいいんじゃね？という気持ちで。

```java
    @Before
    @Transactional("db1TransactionManager")
    public void before() {
        ScriptUtils.executeSqlScript(db1DataSource.getConnection(),
            new ClassPathResource("sql/testdata.sql"));
    }

    @Test
    public void 指定した条件の検索結果が返ること() throws Exception {
```

結果: テスト実行時、検索処理（ `testee.search()` ) で検索クエリがタイムアウトしてしまう

はてなんでだろう、 現象的にはデッドロックされているような状態に似ています。つまり、 `@Before` のメソッドで登録処理自体は実行されていそうだけれど、commitはされていない…ということでしょうか？ :thinking:

## やっと成功
ふと以前やっていたテストメソッドに対する `@Transactional` の仕様を思い出し、テストメソッドに `@Transactional` を設定してみました。

```java

    @Test
    @Sql( script = "classpath:sql/testdata.sql",
        config = @SqlConfig(dataSource = "db1DataSource",
                            transactionManager = "db1TransactionManager"))
    @Transactional("db1TransactionManager")
    public void 指定した条件の検索結果が返ること() throws Exception {
```

結果: **テスト成功** :raised_hands:

## 解説のようなもの
[Springでは、`@Transactional` を指定したテストメソッドは、処理が終わるとデフォルトでロールバックが実行されます。](https://spring.pleiades.io/spring-framework/docs/current/reference/html/testing.html#testcontext-tx-enabling-transactions)
H2（In-memory）のように使い捨てのDBを使っている場合はあまり関係がないのでしばらく使っていませんでした（のですっかり忘れていた）。

これは **DBにテスト実行時のデータを残さない** ための仕様なのだろうと思います。と、いうことは、 **テストデータを事前に登録するための `@Sql` アノテーションも同じ扱いになっていないとおかしい** と考えました。
[ドキュメント](https://spring.pleiades.io/spring-framework/docs/current/reference/html/testing.html#testcontext-executing-sql-declaratively-tx)にも以下の通り記載がありました。

> SQL スクリプトは、transactionMode 属性の設定値に応じて、トランザクションなしで、既存の Spring 管理トランザクション（たとえば、@Transactional アノテーションが付けられたテスト用の TransactionalTestExecutionListener によって管理されるトランザクション）、または分離されたトランザクション内で実行されます。

つまり、今回の場合は `@Sql` で指定したテストデータの登録からテストメソッドが終了するまで、１つのトランザクション内で処理が行われている状態になっているわけですね。 調べてみると `@SqlConfig` のオプションに `transactionMode` なども指定できるようになっているので、こちらもあわせて別のトランザクションで実行するなどの制御もできそうです。

ちなみに、 「やってみた③」の `@Before` で `@Transactional` を設定したときに発生した現象については[同ドキュメント内](https://spring.pleiades.io/spring-framework/docs/current/reference/html/testing.html#testcontext-tx-enabling-transactions)に記載があるのを見つけました。

> @Transactional は、テストライフサイクルメソッドではサポートされていないことに注意してください。たとえば、JUnit Jupiter の @BeforeAll、@BeforeEach でアノテーションが付けられたメソッドなどです。

これは確かに、言われてみればなんとなくですが推測はできるかもしれません。 テストライフサイクルメソッドはJUnitの仕組みで呼び出されているメソッドですから、Springがトランザクション管理などを付加する余地は確かになさそうに見えます。

つまり、「やってみた③」では `@Transactional` が有効にならなかったため、トランザクション管理なしでDB編集を行うクエリを発行していた…つまりテーブル編集後commitもrollbackも実行せずに、テスト実行で同テーブルを参照しに行っていたので、デッドロックのような状態になっていた…のかな？ :thinking: 

## 残る疑問点
そうすると、1点解せないのは「やってみた②」の結果です。 
`@Transactional` をテストメソッドに宣言していない場合、状態としては「やってみた③」と同等なのでは？と思ったのですが、実際はDBの編集内容こそ反映されないものの、検索クエリがタイムアウトになることなくテストが終了しています。
とは言え、申し訳ないのですが、こちらは少し調べてもすぐにはわからなかったので、もう少し調べてみようと思います。


## わかったこと
- テストメソッドで `@Transactional` を指定すると、処理が成功してもデフォルトではロールバックされる
- `@Sql` は、デフォルトでテストメソッドと同じトランザクション管理下で実行される
    - したがって、テストメソッドに `@Transactional` が設定されている場合、（デフォルトだと）テスト終了時に `@Sql` でのDB編集内容も同様にロールバックされる
- テストライフサイクルメソッドでは `@Transactional` は使えない（設定はできるが無効？）