Lauren
Lauren

Reputation: 1500

Hooking into test lifecycle events with Spring Boot/JUnit 5 outside of test class

JUnit 5 has features like the @BeforeEach, @BeforeAll, @AfterAll, @AfterEach annotations that allow you to execute code during test life-cycles events, for instance to clear state that would otherwise be shared between tests.

I'd like a way to do this automatically for certain test configurations without having to remember to put these life-cycle hooks in each test.

For example:

If I have a Spring test configuration that loads some stub data sources:

interface DataProvider {
    String getData(String id);
}

And a class that depends on this:

@Component
class ClassThatDependsOnDataProvider {
    private final DataProvider dataProvider;

    @Autowired
    ClassThatDependsOnDataProvider(DataProvider dataProvider) {
        this.dataProvider = dataProvider;
    }

    public String process(String id) {
        String data = dataProvider.get(id);
        return some_processing_of(data);
    }
}

Normally this might hit a back-end, but for my tests I will swap it out with an in-memory solution:

@TestComponent
class InMemoryDataProvider implements DataProvider {
    private Map<String, String> data = new HashMap<>();

    public void setData(String id, String data) {
        data.put(id, data);
    }

    public String getData(String id) {
        return data.get(id);
    }

    public void clear() {
        data.clear();
    }
}

In my test I might want to use this to stub out data in my back-end, like:

@SpringBootTest
@Import({InMemoryDataProvider.class})
class ClassThatDependsOnDataProviderTests {
    @Autowired InMemoryDataProvider dataProvider;

    @Autowired ClassThatDependsOnDataProvider classThatDependsOnDataProvider;

    @Test
    public void should_correctly_process_data() {
        // given i have an id with a value
        dataProvider.setData("stub_id", "stub_value");

        // when i process my id
        var result = classThatDependsOnDataProvider.process("stub_id");

        // then i receive expected result
        assertThat(result).isWhatIExpect();
    }

    // more @Tests

    // DON'T WANT TO HAVE TO DO THIS IN EACH TEST CLASS
    @AfterEach
    public void tearDown() {
        dataProvider.clear();
    }
} 

I want to be able to remove the tearDown() method here, which is annotated with @AfterEach, and move that behavior into either the test component/test fake or else a configuration class such that, with a set of such providers, it is easy to maintain a set of tests that will replace them with fake versions without having to manually manage their lifecycle for each test.

Is there some kind of Spring feature, lifecycle event, or JUnit 5 feature which will allow this?

Please excuse the simple example, obviously it is overkill for a test this simple but it's just a reference for discussion. Assume @MockBean or using mocking libs is not an option.

Upvotes: 0

Views: 2514

Answers (1)

Jordi Mart&#237;nez
Jordi Mart&#237;nez

Reputation: 448

You can use JUnit5 extensions: https://junit.org/junit5/docs/current/user-guide/#extensions.

For example, your extensions would be something like the following:

public class InMemoryDataProviderExtension implements AfterEachCallback {

    /**
     * {@inheritDoc}
     */
    @Override
    public void afterEach(final ExtensionContext context) throws Exception {
        final ApplicationContext applicationContext = SpringExtension.getApplicationContext(context);

        final InMemoryDataProvider inMemoryDataProvider = applicationContext.getBean(InMemoryDataProvider.class);
        inMemoryDataProvider.clear();

    }
}

Note that:

  • SpringExtension.getApplicationContext(context); gets the spring application context, so you can retrieve the beans defined in the context
  • The extension implements AfterEachCallback interface, but exist others for the other extension points (i.e. BeforeEachCallback, BeforeAllCallback, etc)

In order to activate the extension in your test:

@SpringBootTest
@ExtendWith(InMemoryDataProviderExtension.class)
public class ClassThatDependsOnDataProviderTests { ... }

Upvotes: 3

Related Questions