きり丸の技術日記

技術・エンジニアのイベント・資格等はこちらにまとめる予定です

【Spring】TestExecutionListnerを継承した自作アノテーションでSpringのテストセットアップを快適にする

テスト設計というのは非常に難しいです。

ちょっとでも見誤ると、すぐにテストができない設計になってしまい、テストをしないという設計になってしまいがちです。
これを避けるためにTDDを行うのですが、粒度の違うUTとE2Eではテストの設計が違ってくるのでやっぱり難しい。


今回の記事では、テスト設計について語るわけではありません。

テストの難易度が高いので、少しでも難易度を下げられる手段をとれるようにする、というのが今回の記事の目標です。

環境

  • Java
  • Spring
  • TestExecutionListener

今回の記事でしない話

  • カスタムバリデーションのための自作アノテーション
    • どこかで検証して、記事にする予定。

想定しているユースケース

  • DBのレコードの削除
  • シーケンスの初期化
    • 指定していない場合はすべてリセット
  • マスタをメモリに読み込ませて高速でリセットさせる
    • 指定していない場合はすべてリセット

テーブルをセットアップするアノテーションはライブラリで用意されています。

// テスト前のセットアップ(CLEAN_INSERT指定することが多い。)
@DatabaseSetup(value = "/test/hogeApi/A01/setup.xml")

ですが、目的のテーブルだけ削除する・特定のシーケンスを初期化する、等のアノテーションは用意されていません。

シーケンスを初期化しない場合は、対象項目を無視しないと複数のテストを動かしたときにシーケンスがずれてしまい、まともな検証ができなくなります。

かといって、テスト設計の不備で、不必要にテストを緩くするのは良くないです。
もちろん、品質が落ちる前提でのテスト設計であれば、トレードオフなので状況次第では採用もありです。


そこで、自作アノテーションを作ることで、痒いところに手を届くようにしましょう。

処理自体は、メソッドとして用意するのと変わりません。
アノテーションで記載すると、テストメソッドの外に記載するので、テスト自体の可読性が上がります。

…たぶん。

まぁ、自作アノテーションで失敗すると、原因が分かりづらくなることはあります…。


あと、自作アノテーションを作ると、アノテーションに対して理解が深まるので、アノテーションのソースも読めるようになります。

…これも、たぶん。

使い方

自作アノテーションを作る

詳細の作り方は、他の方の記事に任せます。

DBSetupというメソッドで動くアノテーションを作成しています。

@Documented // JavaDoc生成時に出力されるようになる
@Retention(RetentionPolicy.RUNTIME) // 実行時に使用する
@Target(ElementType.METHOD) // 付与対象を指定。これはメソッド。
public @interface DBSetup {
  String path(); // 自作アノテーションに必要なパラメータ
}

自作アノテーションに挙動をつける

アノテーションを作っただけでは動きません。
処理を追加してあげる必要があります。

今回の例では、テストの実行前に動いてほしいので、SpringのTestExecutionListenerのbeforeTestMethodを継承しています。

Test実行後に動いてほしい、インスタンス生成時に動いてほしい等もTestExecutionListenerに用意されているので、それを継承すると良いでしょう。


挙動としては、TestExecutionListenerを継承しているので、どのテストが実行されても下記のメソッドが呼ばれます。

呼ばれたうえで、AnnotationUtilsを使用し、対象のテストクラスでDBSetupが付与されているかどうかを取得します。

nullであれば付与されていないので処理を終了する。
null以外であればpathの値をログに出力しています。
この値をもとに具体的な処理を記載しますが、アノテーションの使い方としては蛇足になるので説明を省きます。

@Slf4j
public class DBSetupListener implements TestExecutionListener {
  @Override
  public void beforeTestMethod(TestContext testContext) throws Exception {
    DBSetup annotation = AnnotationUtils.findAnnotation(testContext.getTestMethod(), DBSetup.class);

    if (Objects.isNull(annotation)) {
      return;
    }
    // DBSetupの処理を書く。
    log.info(annotation.path());
  }
}

テストでの使い方

SpringのTestExectuionListnerが必要になるので、SpringJunitConfigを使用しています。

TestExecutionListnersに自作したアノテーションの挙動を書いたクラスを記載し、テスト時に読み込みます。

@SpringJUnitConfig(AnotationTests.Config.class)
@TestExecutionListeners({
    // 自作アノテーションの挙動を記載したクラス
    DBSetupListener.class 
})
class AnotationTests {
  // 何もDIさせたくないので、適当な値を設定
  @ComponentScan({"jp.co.kelly.biz.domain.hogehoge"}) 
  public static class Config {
  }

  // "path"はアノテーションのメソッド名です
  @DBSetup(path = "ああああ") 
  @Test
  void test_01() {
    System.out.println("キリン");
  }

  @Test
  void test_02() {
    System.out.println("コアラ");
  }
}

なお、こちらのテストを実行すると、このような挙動になります。

ああああ // DBsetupでpathの値を出力
キリン // test_01
コアラ // test_02

終わりに

今回の記事で、自作アノテーションについてのイメージが湧けば幸いです。

個人的にアノテーションのことを深く知ろうとしなかったのって、どうやってアノテーションと挙動がどう紐づいているのか分からなかったからですね。
今回のアノテーションも一例でしかないから、いまだに良く分からないアノテーションに出会うことはあります…。

適切に使えば、アノテーション名で何をやっているのかを伝えられるので、非常に強力な武器になります。

ぜひ、使ってみてください。


もしこの記事が役に立ったのであれば、はてぶ、Twitterでの記事の拡散、twitterのフォローもよろしくお願いします。

Githubソース

github.com

表記揺れ吸収のため記載

これってどれが正しいですかね。