Avoiding Spring context issues when parallelising @Nested Spring integration tests

Featured image for sharing metadata for article

I write a lot of integration tests with Spring (Boot) and enjoy using nested test classes while using JUnit 5 to provide some structure.

For instance, I may have the following test hierarchy:

V1AlphaFetcherIntegrationTest { // top-level class
  Fetch { // method under test
    InGeneral { // regardless of the happy/sad path testing
      delegatesToMapperFromMethod()
      itAddsRequestCorrelationIdToMdcFromMdc()
    }

    WhenNotFound {
      // ...
    }

    WhenBadRequest {
      // ...
    }
  }
}

This structure gives better breakdown of where tests are applied, allowing a more targeted test setup/teardown for each set of specific cases, and generally helps visibility of tests and failures, too.

On the flipside as I default to parallelisation with my tests, this leads to the need to juggle the Spring context.

I've hit a number of issues with this parallelisation in the past, with i.e. MockBeans sometimes having more invocations than expected, or where we've got setup for mocks being crossed over.

Before

Previously, or where not managing Spring context, I'd reach to the following hierarchy for tests, where we have our overall integration test class, which has beans set up and any other helper methods, which can then be consumed inside the nested classes:

@ExtendWith(SpringExtension.class)
@Import({IntegrationTestBase.Config.class, JacksonAutoConfiguration.class})
class V1AlphaFetcherIntegrationTest {
  protected final TestLogger logger = TestLoggerFactory.getTestLogger(V1AlphaFetcher.class);

  @MockBean protected Mdc mdc;

  @Autowired protected ObjectMapper objectMapper;
  @Autowired protected MockWebServer server;

  @Autowired protected V1AlphaFetcher fetcher;

  @TestConfiguration
  static class Config {

    @Bean
    public MockWebServer webServer() {
      return new MockWebServer();
    }

    @Bean
    public WebClient webClient() {
      return WebClient.builder().build();
    }

   // other beans
  }

  @AfterEach
  void tearDown() {
    logger.clear();
  }

  // shared methods


  @Nested
  class Fetch {
    @Nested
    class InGeneral {
      // tests, using the shared beans like `mdc`
    }
  }
}

This would then lead to some awkward state management, possibly requiring a @BeforeEach in each test to reset things, which isn't ideal, and overcomplicates our tests.

After

The solution I find works is to instead create a base class for each nested test case, i.e. IntegrationTestBase:

class V1AlphaFetcherIntegrationTest {
  @ExtendWith(SpringExtension.class)
  @Import({IntegrationTestBase.Config.class, JacksonAutoConfiguration.class})
  @DirtiesContext
  @Nested
  abstract class IntegrationTestBase {
    protected final TestLogger logger = TestLoggerFactory.getTestLogger(V1AlphaFetcher.class);

    @MockBean protected Mdc mdc;

    @Autowired protected ObjectMapper objectMapper;
    @Autowired protected MockWebServer server;

    @Autowired protected V1AlphaFetcher fetcher;

    @TestConfiguration
    static class Config {

      @Bean
      public MockWebServer webServer() {
        return new MockWebServer();
      }

      @Bean
      public WebClient webClient() {
        return WebClient.builder().build();
      }

     // other beans
    }

    @AfterEach
    void tearDown() {
      logger.clear();
    }

    // shared methods
  }

  @Nested
  class Fetch {
    @Nested
    class InGeneral extends IntegrationTestBase {
      // tests
    }
  }
}

This allows us to put common things into a central place, making a clear set of common setup, and allowing us to access shared beans using protected fields. By leaving state out of the overall test class, we also make it much clearer how the state is being managed.

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #java #spring #spring-boot #testing #tdd.

Also on:

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.