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.