19

tl;dr: How can I instantiate a custom data provider as a Spring component before all tests run?

Is there a smart way to inject Spring components into a custom JUnit Jupiter extension that implements BeforeAllCallback? The beforeAll method should trigger a complex process before MyTestClass is executed with @ExtendWith(OncePerTestRunExtension.class).

I created a Spring Boot Application (src/main/java) that provides my test (src/test/java) with the necessary data. The data can take up to a few hours to be prepared for the tests. It also gives me abstracted access to some rest-endpoints.

The data does not change in between the process of all test classes. So I just want to pull the data once.

Writing all tests in just one class would work, but I think the separation into different classes gives a better overview.

Sam Brannen
  • 29,611
  • 5
  • 104
  • 136
froehli
  • 904
  • 1
  • 11
  • 35

2 Answers2

30

In the beforeAll(ExtensionContext) method of your custom BeforeAllCallback, you can access the Spring ApplicationContext for the current test class via SpringExtension.getApplicationContext(extensionContext).

If you configure your custom data provider as a Spring component in that ApplicationContext, you can then retrieve the component from the ApplicationContext within your extension -- for example, via applicationContext.getBean(MyDataProvider.class).

If you need to process the data and store that processed data between tests, you can store that in the root ExtensionContext.Store in JUnit Jupiter. See ExtensionContext.getRoot() and the getOrComputeIfAbsent(...) variants in ExtensionContext.Store for details.

Sam Brannen
  • 29,611
  • 5
  • 104
  • 136
  • 1
    Thanks for your feedback. It works so far for me. But something strange happened when using `@Nested` Classes in my Test. It thorws a `NoSuchBeanDefinitionException` of the Bean that I get via `SpringExtension.getApplicationContext(context).getBean(myBean.class)` inside `class SetupExtension implements BeforeAllCallback, AfterAllCallback`. This happens only when putting the test in a nested class. If i spare the use of nested Tests (and therfore some more structure), my tests run though just fine. – froehli Aug 02 '19 at 13:59
  • About the ExtensionContext.Store: this just works between Extensions, right? I could not access any data that I stored there from my normal tests. Since the BeforAllCallback Class shoud gather the Data, I would need access to them. My solution was the use of a static method inside the callback class. But I would prefere a way without static classes. – froehli Aug 02 '19 at 14:04
  • 1
    Well, if you have some kind of "data holder" that your extension saves in the `ExtensionContext.Store`, your extension could make that "data holder" available to test classes -- for example, by dependency injection via the `ParameterResolver` extension API. – Sam Brannen Aug 02 '19 at 19:49
  • 2
    Regarding the `@Nested` tests, please note that a `@Nested` test class does not (yet) inherit Spring test configuration from its enclosing class. So the behavior you experienced is likely to be expected. See also: https://github.com/spring-projects/spring-framework/issues/19930 – Sam Brannen Aug 02 '19 at 19:51
  • This link was very helpful, thanks! I annotated the nested classes with `@SpringBootTest` and it worked. SurprisinglyI I recognized, that the `beforeAll(ExtensionContext context)` method from the class that implements `BeforeAllCallback` is called before every testclass. I wanted to archive, that this method is only called once. Therefore I created a `static boolean started = false` to check wheter it neeeds to be executed again. But this is not how it is intendet, right? Are there any good examples for structuring big JUnit TestSuites? – froehli Aug 05 '19 at 13:17
  • The `afterAll` method is even called twice per class. – froehli Aug 05 '19 at 13:23
  • If the `afterAll` method is called twice for the same test class, that would be a bug. If you can confirm that to be the case, please open an issue to report it here: https://github.com/junit-team/junit5/issues/new/choose – Sam Brannen Aug 05 '19 at 15:18
  • I could reproduce this in a minimal example. It seems to be related to the `@Nested` Tests. So I expected the methods to be called once per execution of all classes. But instead there are called for each TestClass. Even for the nested classes. Maybe this works as expected? Should I still create an issue? What is about my original quastion: could the `BeforeAllCallback` be the wrong tool? I wanted a "Do this once before every other Test-Class is executed" semantic. – froehli Aug 06 '19 at 10:32
  • @SamBrannen Is there also a way to get at the `ConfigurableApplicationContext` to do what I can do with `@DynamicPropertySource` but in an extension? – johanneslink Dec 21 '20 at 09:36
  • `SpringExtension.getApplicationContext(extensionContext)` will give you the `ApplicationContext`, which you can cast to `ConfigurableApplicationContext`, but the `ApplicationContext` is already loaded (i.e., 'refreshed') by that point. The only way to modify the `ApplicationContext` before it is refreshed is via a `ContextCustomizer` or `ApplicationContextInitializer` (or custom `SmartContextLoader` which you probably don't want). – Sam Brannen Dec 22 '20 at 12:33
11

Here's how I implemented this to setup some test data in my database using a dataSource Spring bean, by combining Sam's answer above with this answer: https://stackoverflow.com/a/51556718/278800

import javax.sql.DataSource;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit.jupiter.SpringExtension;

public class TestDataSetup implements BeforeAllCallback, ExtensionContext.Store.CloseableResource {

  private static boolean started = false;

  private DataSource dataSource;

  @Override
  public void beforeAll(ExtensionContext extensionContext) {
    synchronized (TestDataSetup.class) {
      if (!started) {
        started = true;

        // get the dataSource bean from the spring context
        ApplicationContext springContext = SpringExtension.getApplicationContext(extensionContext);
        this.dataSource = springContext.getBean(DataSource.class);

        // TODO: put your one-time db initialization code here

        // register a callback hook for when the root test context is shut down
        extensionContext
            .getRoot()
            .getStore(ExtensionContext.Namespace.GLOBAL)
            .put("TestDataSetup-started", this);
      }
    }
  }

  @Override
  public void close() {
    synchronized (TestDataSetup.class) {
      // TODO: put your db cleanup code here
    }
  }

(I'm not 100% sure on the thread safety of this, so I added the synchronized block just to be safe.)

To enable this extension, you just need to add this annotation to your test classes which need it:

@ExtendWith(TestDataSetup.class)

The nice thing is that Junit 5 allows multiple extensions, so this works even if your tests are already annotated with @ExtendWith(SpringExtension.class).

Steve K
  • 2,044
  • 3
  • 24
  • 37