💯

入門JUnitエクステンション: @SpringBootTestの正体

に公開

この記事は何?

Spring Bootを使ってテストする場合、テストクラスに@SpringBootTestアノテーションを付加します。

このアノテーションのソースコードは次のようになっています。

SpringBootTest.java
...
@ExtendWith(SpringExtension.class)
public @interface SpringBootTest {
    ...

ここで出てきた @ExtendWith アノテーションは、JUnitでエクステンションを登録するためのものです。

この記事では、JUnitのエクステンションとは何かを解説していきます。

環境

  • JDK 21
  • JUnit Jupiter 5.13.4
build.gradle.kts
dependencies {
  implementation("ch.qos.logback:logback-classic:1.5.18")
  testImplementation(platform("org.junit:junit-bom:5.13.4"))
  testImplementation("org.junit.jupiter:junit-jupiter")
  testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
  useJUnitPlatform()
}

エクステンションとは?

エクステンションとは、その名の通りJUnitの拡張機能です。具体的には @BeforeEach@AfterEach@BeforeAll@AfterAll などと同様のテストの前処理・後処理などを定義することができます。これらのアノテーションでは各テストクラス・後処理を個別に書くことになる一方で、エクステンションでは複数のテストクラスに共通の前処理・後処理を作成できます。

エクステンションの作り方

エクステンションはクラスとして作成します。その際、次のインタフェースを1つ以上、必要に応じて実装します。

  • BeforeEachCallback
    • @BeforeEach相当の前処理を定義
  • AfterEachCallback
    • @AfterEach相当の後処理を定義
  • BeforeAllCallback
    • @BeforeAll相当の前処理を定義
  • AfterAllCallback
    • @AfterAll相当の後処理を定義
  • ParameterResolver
    • テストメソッドに引数を設定できるようにするもの(ソースコード内のコメント参照)

今回はログを出力するだけのエクステンションクラスを作成します。

package com.example;

import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SampleExtension implements
    BeforeEachCallback,
    AfterEachCallback,
    BeforeAllCallback,
    AfterAllCallback,
    ParameterResolver {

  private static final Logger logger = LoggerFactory.getLogger(SampleExtension.class);

  public SampleExtension() {
    logger.info("コンストラクタ: {}", this);
  }

  @Override
  public void afterEach(ExtensionContext extensionContext) throws Exception {
    logger.info("afterEach(): {}", extensionContext.getTestMethod().get().getName());
  }

  @Override
  public void beforeEach(ExtensionContext extensionContext) throws Exception {
    logger.info("beforeEach(): {}", extensionContext.getTestMethod().get().getName());
  }

  @Override
  public void afterAll(ExtensionContext extensionContext) throws Exception {
    logger.info("afterAll(): {}", extensionContext.getTestClass().get().getName());
  }

  @Override
  public void beforeAll(ExtensionContext extensionContext) throws Exception {
    logger.info("beforeAll(): {}", extensionContext.getTestClass().get().getName());
  }

  @Override
  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
      throws ParameterResolutionException {
    logger.info("supportsParameter(): {}", parameterContext.getParameter().getName());
    // テストメソッドの引数がSampleParam型であればOK
    return parameterContext.getParameter().getType().equals(SampleParam.class);
  }

  @Override
  public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
      throws ParameterResolutionException {
    logger.info("resolveParameter(): {}", parameterContext.getParameter().getName());
    // supportsParameter()がtrueを返す場合、引数に代入する値を返す
    return new SampleParam("Sample Value");
  }
}
SampleParam.java
package com.example;

public record SampleParam(String value) {
}

エクステンションの使い方

作成したエクステンションをテストクラスに登録するには @ExtendWith アノテーションを利用します。

SampleTest.java(一部)
// エクステンションを登録
@ExtendWith(SampleExtension.class)
public class SampleTest {
    ...

エクステンションの実行順序

エクステンションと @BeforeEach などを併用した場合、実行順序がどうなるのかを見てみましょう。

テストクラスを次のように記述します。

SampleTest.java
package com.example;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ExtendWith(SampleExtension.class)
public class SampleTest {
  private static final Logger logger = LoggerFactory.getLogger(SampleTest.class);

  public SampleTest() {
    logger.info("コンストラクタ: {}", this);
  }

  @BeforeEach
  void beforeEach() {
    logger.info("@BeforeEach");
  }

  @AfterEach
  void afterEach() {
    logger.info("@AfterEach");
  }

  @BeforeAll
  static void beforeAll() {
    logger.info("@BeforeAll");
  }

  @AfterAll
  static void afterAll() {
    logger.info("@AfterAll");
  }

  @Test
  void test01(SampleParam param) {
    logger.info("test01(): {}", param);
  }

  @Test
  void test02(SampleParam param) {
    logger.info("test02(): {}", param);
  }
}

実行結果は次の通りです。

実行結果
xx:xx:xx.186 [Test worker] INFO com.example.SampleExtension -- コンストラクタ: com.example.SampleExtension@50dfbc58
xx:xx:xx.196 [Test worker] INFO com.example.SampleExtension -- beforeAll(): com.example.SampleTest
xx:xx:xx.198 [Test worker] INFO com.example.SampleTest -- @BeforeAll
xx:xx:xx.200 [Test worker] INFO com.example.SampleTest -- コンストラクタ: com.example.SampleTest@27216cd
xx:xx:xx.201 [Test worker] INFO com.example.SampleExtension -- beforeEach(): test01
xx:xx:xx.201 [Test worker] INFO com.example.SampleTest -- @BeforeEach
xx:xx:xx.202 [Test worker] INFO com.example.SampleExtension -- supportsParameter(): arg0
xx:xx:xx.202 [Test worker] INFO com.example.SampleExtension -- resolveParameter(): arg0
xx:xx:xx.202 [Test worker] INFO com.example.SampleTest -- test01(): SampleParam[value=Sample Value]
xx:xx:xx.206 [Test worker] INFO com.example.SampleTest -- @AfterEach
xx:xx:xx.206 [Test worker] INFO com.example.SampleExtension -- afterEach(): test01
xx:xx:xx.209 [Test worker] INFO com.example.SampleTest -- コンストラクタ: com.example.SampleTest@1b84f475
xx:xx:xx.210 [Test worker] INFO com.example.SampleExtension -- beforeEach(): test02
xx:xx:xx.211 [Test worker] INFO com.example.SampleTest -- @BeforeEach
xx:xx:xx.211 [Test worker] INFO com.example.SampleExtension -- supportsParameter(): arg0
xx:xx:xx.211 [Test worker] INFO com.example.SampleExtension -- resolveParameter(): arg0
xx:xx:xx.211 [Test worker] INFO com.example.SampleTest -- test02(): SampleParam[value=Sample Value]
xx:xx:xx.211 [Test worker] INFO com.example.SampleTest -- @AfterEach
xx:xx:xx.211 [Test worker] INFO com.example.SampleExtension -- afterEach(): test02
xx:xx:xx.212 [Test worker] INFO com.example.SampleTest -- @AfterAll
xx:xx:xx.213 [Test worker] INFO com.example.SampleExtension -- afterAll(): com.example.SampleTest

つまりこのような順序になります。

  1. エクステンションの beforeAll()
  2. テストクラスの @BeforeAll
  3. エクステンションの beforeEach()
  4. テストクラスの @BeforeEach
  5. テストメソッド @Test
  6. テストクラスの @AfterEach
  7. エクステンションの afterEach()
  8. テストクラスの @AfterAll
  9. エクステンションの afterAll()

前処理ではエクステンションが先に実行され、後処理ではエクステンションが後に実行されます。

参考文献

Discussion