LoginSignup
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2020-12-11

小ネタ…小ネタってなんだ…と小ネタの定義を考えはじめてはや数日、あっという間に〆切の日になってしまいました:joy:
ちょうどカレンダーの概要を見て、先日タイムリーにも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だけじゃなくてトランザクション境界を明示的に宣言しないとダメかな :sweat:

やってみた③ (間違い)

ぱっと @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はされていない…ということでしょうか? :thinking:

やっと成功

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


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

結果: テスト成功 :raised_hands:

解説のようなもの

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も実行せずに、テスト実行で同テーブルを参照しに行っていたので、デッドロックのような状態になっていた…のかな? :thinking:

残る疑問点

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

わかったこと

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1