Why DirtiesContext is needed on other test classes to mock bean dependency for class with JMS Listener

Viewed 1090

Context

A Spring Boot application with a Rest endpoint and a JMS AMQ Listener

Test behaviour observed

The tests classes run fine without needing DirtiesContext individually but when the entire suite of test classes are run the following behaviours are observed -

  1. Mocking of a bean dependency for the JMS Consumer test requires the earlier test classes to have a DirtiesContext annotation.
  2. Mocking of bean dependency for RestControllers seem to work differently than a JMS Listener i.e don't need DirtiesContext on the earlier test classes

I've created a simple Spring application to reproduce the Spring context behaviour I need help understanding - https://github.com/ajaydivakaran/spring-dirties-context

1 Answers

The reason this happens is due to the fact that without @DirtiesContext Spring will remain the context for reuse for other tests that share the same setup (read more on Context Caching in the Spring documentation). This is not ideal for your setup as you have a messaging listener, because now multiple Spring Contexts can remain active and steal the message you put into the queue using the JmsTemplate.

Using @DirtiesContext ensures to stop the application context, hence this context is not alive afterward and can't consume a message:

from @DirtiesContext:

Test annotation which indicates that the {@link org.springframework.context.ApplicationContext ApplicationContext} * associated with a test is dirty and should therefore be closed and removed from the context cache.

For performance reasons, I would try to not make use of @DirtiesContext too often and rather ensure that the JMS destination is unique for each context you launch during testing. You can achieve this by outsourcing the destination value to a config file (application.properties) and randomly populate this value e.g. using a ContextInitializer.

A first (simple) implementation could look like the following:

@AllArgsConstructor
@Service
public class Consumer {
    private EnergeticGreeter greeter;
    private MessageRepository repository;
    private ApplicationContext applicationContext;

    @JmsListener(destination = "${consumer.destination}")
    public void consume(
            @Header(name = JmsHeaders.MESSAGE_ID, required = false) String messageId,
            TextMessage textMessage) {

        System.out.println("--- Consumed by context: " + applicationContext.toString());

        if ("Ahem hello!!".equals(greeter.welcome().getContent())) {
            repository.save();
        }
    }
}

the corresponding test:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = DestinationValueInitializer.class)
public class JMSConsumerIntegrationTest {

    @Autowired
    private JmsTemplate jmsTemplate;

    @Value("${consumer.destination}")
    private String destination;

    @Autowired
    private ApplicationContext applicationContext;

    @MockBean
    private EnergeticGreeter greeter;

    @MockBean
    private MessageRepository repository;

    //Todo - To get all tests in this project to pass when entire test suite is run look at Todos added.
    @Test
    public void shouldInvokeRepositoryWhenGreetedWithASpecificMessage() {
        when(greeter.welcome()).thenReturn(new Message("Ahem hello!!"));

        System.out.println("--- Send from context: " + applicationContext.toString());

        jmsTemplate.send(destination, session -> session.createTextMessage("hello world"));

        Awaitility.await().atMost(10, TimeUnit.SECONDS).untilAsserted(
                () -> verify(repository, times(1)).save()
        );
    }
}

and the context initializer:

public class DestinationValueInitializer implements
        ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        TestPropertyValues.of("consumer.destination=" + UUID.randomUUID().toString()).applyTo(applicationContext);
    }
}

I've provided a small PR for your project where you can see this in the logs, that a different application context is consuming your message and hence you can't verify that the repository was called on the application context you write your test in.

Related