iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
💯

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:

SpringBootTest.java
...
@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
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()
}

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
  • AfterEachCallback
    • Defines post-processing equivalent to @AfterEach
  • BeforeAllCallback
    • Defines pre-processing equivalent to @BeforeAll
  • AfterAllCallback
    • Defines post-processing equivalent to @AfterAll
  • 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");
  }
}
SampleParam.java
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.

SampleTest.java (partial)
// 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:

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("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:

Execution Result
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:

  1. beforeAll() of the extension
  2. @BeforeAll of the test class
  3. beforeEach() of the extension
  4. @BeforeEach of the test class
  5. @Test method
  6. @AfterEach of the test class
  7. afterEach() of the extension
  8. @AfterAll of the test class
  9. afterAll() of the extension

For pre-processing, the extension is executed first, and for post-processing, the extension is executed last.

References

Discussion