👋

JJUG CCC 2023 Spring で話せなかったロールバック周りについて

2023/07/03に公開3

先日、JJUG CCC 2023 Spring に登壇してきました!

https://speakerdeck.com/yukana/repository-test-strategy-speeds-up-unit-test

発表時間的に端折ったロールバック周りについて、この記事で補足しておきます。

Transactional を使用しなかった理由

ユニットテストが遅い問題の一つとして、DirtiesContext を使用してロールバックを行なっていることを挙げました。

テストでロールバックを簡単に行う方法として、Spring の Transactional を使う方法があります。下記のように、テストクラスもしくはテストメソッドに @Transactional をつけて、テスト中に行われた変更をテスト終了時にロールバックすることができます。

テスト対象のクラス
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    @Transactional
    public void register() {
        // ユーザ生成処理

        this.userRepository.save(user);
    }
}
テストクラス
@SpringBootTest
@Transactional
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    public void register() {
        // テストデータの準備

        // テスト対象メソッドの実行
        this.userService.register();

        // 検証
    }
}

しかし、今回は Transactional を使用しませんでした。
本番環境とテスト環境で行うことを同じにするためです。

テスト対象のアプリケーションサービスのメソッドには、自身でトランザクション管理を行うものと、そうでないものがあります。Transactional を使用すると、自身でトランザクション管理を行うメソッドをテストする時に問題があります。Transactional のデフォルトの挙動 Propagation.REQUIRED ではテスト側で開始したトランザクションを使い回すからです。
先ほどの例で言うと、テストデータの準備、テスト対象メソッドの実行、検証が全て同じトランザクション上で実行されることになります。
しかし、本番環境ではこのようなことは起こらないので、本番環境とテスト環境で行うことが異なり、正しくテストが行われることを保証できません。

検証していないのでわからないですが、Transactional のオプションをうまく使って、本番環境と合わせられるかもしれないです。
ただ、仮にそれができたとしても、テストを書くたびに意識するのは難しいですし、うっかり デフォルトの Transactional を使ってしまうかもしれません。

また、ロールバックの問題とは別に、リポジトリを過剰にテストしていて、アプリケーションサービスとリポジトリでテストが重複している問題もあったので、結果的にテストの責務を分離することで高速化を図りました。

ちなみに、単体テストの考え方/使い方 の10章によると、テストデータの後始末は、各テストケースの実行前に行うのがよいみたいです。

JJUG CCC の感想

社内の輪読会で読んだ現場で役立つシステム設計の原則の著者の増田さんのセッション 「これだけは知っておきたいクラス設計の基礎知識」を聞けてよかったです。
「体で覚える」というのが印象的で、インプットするだけでなく、もっと手を動かしていこうと思いました!

BABY JOB  テックブログ

Discussion

Masatoshi TadaMasatoshi Tada

JJUG幹事の多田です。この度はご登壇ありがとうございました!当日その場にはいられなかったのですが、この記事と資料は拝見しました😆
いくつかお聞きしたいことがあるのですが、よろしいでしょうか?

  • テスト環境と本番環境の差異を無くすことを重視してらっしゃるとのことでしたが、そうなるとテストでのDBはH2ではなく本番と同じDB(MySQLでしょうか?)を使ったほうがいいのかなと思いました。その点はどうお考えですか?(僕の読み違いだったらごめんなさい🙇‍♀️)
  • @SqlにSqlConfig.TransactionMode.ISOLATEDを指定すると、データ投入がテストメソッドとは別のトランザクションで実行されます。これだと要件には合わないでしょうか?(検証がテスト対象メソッドと同一トランザクションになってしまいますが、それで特に問題は生じないかなと思いました)
  • 結合テストやE2Eテストは行ってらっしゃいますか?資料には書かれていないのでどうなのかなーと思ってお聞きしました。
yukanayukana

多田さん、コメントありがとうございます!
こちらこそ登壇の機会をいただきありがとうございました。

以下ご質問への回答です。

テスト環境と本番環境の差異を無くすことを重視してらっしゃるとのことでしたが、そうなるとテストでのDBはH2ではなく本番と同じDB(MySQLでしょうか?)を使ったほうがいいのかなと思いました。その点はどうお考えですか?

おっしゃる通り、本番と同じDBをコンテナ等で用意できるとよいのですが、以下を考慮してH2を使用しています。

  • 今回のユニットテスト高速化の話以前からH2を使っている
  • テスト実行時のDBのセットアップコスト
  • テストの責務を分離することを高速化のスコープとしたかった

@SqlにSqlConfig.TransactionMode.ISOLATEDを指定すると、データ投入がテストメソッドとは別のトランザクションで実行されます。これだと要件には合わないでしょうか?(検証がテスト対象メソッドと同一トランザクションになってしまいますが、それで特に問題は生じないかなと思いました)

そんな設定ができたんですね。知りませんでした!
テスト環境と本番環境を合わせる点で要件を満たしそうですし、知ってれば高速化の選択肢の一つになってたと思います。

結合テストやE2Eテストは行ってらっしゃいますか?資料には書かれていないのでどうなのかなーと思ってお聞きしました。

現時点で自動化された結合テスト、E2Eテストはなく手動でカバーしている状況です😅
(自動化は検討しています)

Masatoshi TadaMasatoshi Tada

ご返信ありがとうございますー!
理解しました。
またのご参加orご登壇、楽しみにしておりますー😆