iTranslated by AI
Introduction to JUnit Extensions: Understanding @SpringBootTest
What is this article?
When testing with Spring Boot, you add the @SpringBootTest annotation to your test classes.
The source code for this annotation is as follows:
...
@ExtendWith(SpringExtension.class)
public @interface SpringBootTest {
...
The @ExtendWith annotation that appears here is used to register extensions in JUnit.
In this article, I will explain what JUnit extensions are.
Environment
- JDK 21
- JUnit Jupiter 5.13.4
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()
}
What are Extensions?
Extensions are, as the name implies, expansion features for JUnit. Specifically, you can define pre- and post-test processing similar to @BeforeEach, @AfterEach, @BeforeAll, and @AfterAll. While these annotations require writing setup and teardown for each test class individually, extensions allow you to create common pre- and post-processing logic that can be shared across multiple test classes.
How to Create Extensions
Extensions are created as classes. In doing so, you implement one or more of the following interfaces as needed:
-
BeforeEachCallback- Defines pre-processing equivalent to
@BeforeEach
- Defines pre-processing equivalent to
-
AfterEachCallback- Defines post-processing equivalent to
@AfterEach
- Defines post-processing equivalent to
-
BeforeAllCallback- Defines pre-processing equivalent to
@BeforeAll
- Defines pre-processing equivalent to
-
AfterAllCallback- Defines post-processing equivalent to
@AfterAll
- Defines post-processing equivalent to
-
ParameterResolver- Allows arguments to be set for test methods (refer to the comments in the source code)
In this example, we will create an extension class that simply outputs logs.
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("Constructor: {}", 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());
// OK if the test method argument is of type SampleParam
return parameterContext.getParameter().getType().equals(SampleParam.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
logger.info("resolveParameter(): {}", parameterContext.getParameter().getName());
// If supportsParameter() returns true, return the value to be assigned to the argument
return new SampleParam("Sample Value");
}
}
package com.example;
public record SampleParam(String value) {
}
How to Use Extensions
To register the created extension in a test class, use the @ExtendWith annotation.
// Register the extension
@ExtendWith(SampleExtension.class)
public class SampleTest {
...
Execution Order of Extensions
Let's see what the execution order looks like when using extensions together with @BeforeEach and similar annotations.
Describe the test class as follows:
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("Constructor: {}", 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);
}
}
The execution result is as follows:
xx:xx:xx.186 [Test worker] INFO com.example.SampleExtension -- Constructor: 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 -- Constructor: 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 -- Constructor: 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
In other words, the order is as follows:
-
beforeAll()of the extension -
@BeforeAllof the test class -
beforeEach()of the extension -
@BeforeEachof the test class -
@Testmethod -
@AfterEachof the test class -
afterEach()of the extension -
@AfterAllof the test class -
afterAll()of the extension
For pre-processing, the extension is executed first, and for post-processing, the extension is executed last.
Discussion