💯
入門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
つまりこのような順序になります。
- エクステンションの
beforeAll()
- テストクラスの
@BeforeAll
- エクステンションの
beforeEach()
- テストクラスの
@BeforeEach
- テストメソッド
@Test
- テストクラスの
@AfterEach
- エクステンションの
afterEach()
- テストクラスの
@AfterAll
- エクステンションの
afterAll()
前処理ではエクステンションが先に実行され、後処理ではエクステンションが後に実行されます。
Discussion