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

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 -
Mocking of a bean dependency for the JMS Consumer test requires the earlier test classes to have a DirtiesContext annotation.
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

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

Test #EventListener with custom events Spring Kotlin

I have created a set of custom events for my application
sealed class myEvent(open val id: Int) {
data class myBigEvent(override val id : Int) : myEvent(id)
data class myIntermediateEvent(override val id: Int): myEvent(id)
}
I have a service that has a method for listening my custom events
#Service
class MailService(
private val otherService: OtherService
) {
#EventListener(myEvent.myBigEvent::class, myEvent.myIntermediateEvent::class)
#Async
fun handleProcessEvent(event: myEvent) {
if (event.id != 10 && otherService.hasCompleted) {
sendMail(event)
}
}
The interesting point is that IntellIj annotates with the eventlistener icon next to the method defintion. When clicking on it I get redirected to all my invocations of publisher.publishEvent(myBigEvent) or publisher.publishEvent(myIntermediateEvent)
The issue is when I try to test it. I have the following setup
#TestExecutionListeners
class myEventTest {
#Autowired
private lateinit var publisher: ApplicationEventPublisher
#Mock
private lateinit var mailService: MailService
#BeforeClass
fun start() {
publisher = ApplicationEventPublisher {}
MockitoAnnotations.initMocks(this)
}
#Test
fun `should receive cmlaProcessEvent published event`() {
val cmlaProcessEvent = JobProcessEvent.CmlaProcessSimulationProcessEvent(mlaSimulationRun)
this.publisher.publishEvent(cmlaProcessEvent)
verify(mailService, times(1)).handleProcessEvent(cmlaProcessEvent)
}
}
I get 'Wanted but not invoked' ... 'Actually, there were zero interactions with this mock.'
I think my issue is with the ApplicationEventPublisher that is not sending the events to my MailService under this test context ?
Remove #TestExecutionListeners, since the default listeners should suffice.
If you're using Spring Boot, you should use #MockBean instead of #Mock.
In any case, you have to actually instruct JUnit to use Spring's testing support.
I assume that you are using JUnit 4, since I see #BeforeClass in the example. So I'm basing the following on that assumption (and using Java syntax instead of Kotlin).
You can instruct JUnit 4 to use Spring's testing support via #RunWith(SpringRunner.class). Note, however, that you'll also need to specify where your ApplicationContext configuration is.
For Spring Framework, you can do that with #ContextConfiguration. For Spring Boot, you'll likely want to use #SpringBootTest. Consult the documentation for testing support in Spring Framework and Spring Boot for details.
As a side note, you can also test ApplicationEvent publication without using a mock by using Spring Framework's built-in testing support for application events.

Spring does not load Constructor with jUnit parematerised tests for each test

I use Redis keyspace notifications. In my Spring application I have below class:
#Service
public class KeyExpiredPublisher {
private final JedisPool jedisPool;
#Autowired
private KeyExpiredSubscriber keyExpiredSubscriber;
public KeyExpiredPublisher(JedisPool defaultJedisPool) {
jedisPool = defaultJedisPool;
}
#PostConstruct
public void enablePublishExpire() {
enablePublish().subscribe();
}
public Mono<Void> enablePublish() {
Mono<Void> mono = Mono.fromRunnable(() -> {
Jedis jedis = jedisPool.getResource();
String subscribedEventPattern = "__keyevent#0__:expired";
jedis.psubscribe(keyExpiredSubscriber, subscribedEventPattern);
});
return mono.subscribeOn(Schedulers.boundedElastic());
}
}
All mono stuff there is just for multithreading.
I have a parameterised unit test, which starts redis before each test and stops it after each test. However before each test, the constructors are not injected. Test with first parameter is passing because it gets to constructor (#PostConstruct method above) and subscription happens. However, after the first run, #BeforeEach kills the redis and although other tests start redis again, the code never goes into #PostSonstruct again so subscription never happens for other tests.
Is there any way to make sure Spring loads application before each parameter?
Depending on what you precisely do in your tests (you didn't show us), spring might be reusing the application context for performance reasons, thus only one instance of the bean is created. Usually a DirtiesContext Annotation in the test setup helps with this.

Actually, there were zero interactions with this mock. Embedded Kafka Spring test

I'm trying to see if a method from Service class is invoked when the consumer consumes a message from Kafka topic but i'm getting the error that there is no zero interactions with the Mock. When the test is running it consumes the message and I can see on terminal that the service method is actually invoked (i tried with prints), but it is not passing the test.
My Consumer class:
#Component
public class Consumer {
#Autowired
private Service service;
#KafkaListener(topics = "topic")
public void consume(String message) {
service.add();
}
}
The test:
#SpringBootTest
#RunWith(MockitoJUnitRunner.class)
#DirtiesContext
#EmbeddedKafka(partitions = 1, brokerProperties = { "listeners=PLAINTEXT://localhost:9092", "port=9092" })
class ConsumerTest {
#Mock( lenient = true)
private Service service;
#Autowired
private KafkaTemplate<String, String> kafkaTemplate;
#InjectMocks
private Consumer consumer;
#Test
public void givenEmbeddedKafkaBroker_whenExistsTemperatureMessageInTopic_thenMessageReceivedByConsumerAndServiceInvoked()
throws Exception {
String message = "Hello";
kafkaTemplate.send("topic", message);
Mockito.verify(service, times(1)).add();
}
}
TLDR: Don't use #Mock with #SpringBootTest. Use #MockBean instead.
The components you created in your test don't take part in the message processing:
consumer
service
This stems from the fact that you used #SpringBootTest annotation, which brings up entire application context. This means that Spring creates all services itself, and happily ignores the ones created in the test.
To replace a bean from the test, use #MockBean
To inject a bean created by Spring to your test, use #Autowired

Test, if I don't want to trigger the whole thing

A Spring Boot application
#SpringBootApplication
#EnableScheduling
#Slf4j
public class MyApplication {
#Autowired
private ApplicationEventPublisher publisher;
...
#Bean
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
...
// read data from a file and publishing an event
}
}
For an integration test, I have something typical.
#SpringBootTest
public class TestingMyApplicationTests{
...
}
After I start a test case in the class, the whole chain events occurs, that is reading a file, publishing events and an event listener acts accordingly.
What is the best approach to avoid such chain events occur during running a test?
If you want to avoid that the whole Spring Context is started for all of your integration tests, you can take a look at other test annotations that create a sliced context:
#WebMvcTest creates a Spring Context with only MVC related beans
#DataJpaTest creates a Spring Context with only JPA/JDBC related beans
etc.
In addition to this, I also would remove your CommandLineRunner from your main entry Spring Boot entry point class. Otherwise also the annotations above would trigger the logic.
Therefore you can outsource it to another #Component class:
#Component
public class WhateverInitializer implements CommandLineRunner{
#Autowired
private ApplicationEventPublisher publisher;
// ...
#Override
public void run(String... args) throws Exception {
...
// read data from a file and publishing an event
}
}
Apart from this you can also use #Profile("production") on your Spring beans to only populate them when a specific profile is active. This way you can either include or exluce them for all your integration tests if you don't want e.g. this startup logic always.

Nested configuration not working as expected

I'm working on a project using spring boot, and we've just upgraded to version 1.4.0.RELEASE. As part of the version upgrade, we've started using the #SpringBootTest annotation on our abstract integration test class. After reading the documentation, it sounds like we should be able to use a nested #TestConfiguration-annotated config class to override bean definitions during specific tests. This is not working for us, and instead the non-test bean we are trying to override is still being used.
Interestingly, it seems like usages of the mock test bean and the production bean are actually intertwined within the same test, as if both beans exist side by side in the application context. Also, it seems like the order in which the integration tests run somehow affects this behaviour. I'm wondering if this is something we've misconfigured, or if there is something else going on.
edit:
The abstract class that the integration tests inherit from looks like this:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#ActiveProfiles({"local", "test"})
public abstract class BaseIntegrationTest {
#Value("${local.server.port}")
protected int port;
}
The integration test that we are seeing the strange behaviour in looks like this:
public class WebhookProcessorIT extends BaseIntegrationTest {
#TestConfiguration
public static class Config {
#Bean
#Primary
public WebhookTask webhookTask() {
return mock(WebhookTask.class);
}
}
// sometimes the mock above is used and sometimes
// the actual production bean is used
#Autowired
private WebhookTask task;
#Before
public void setup() {
when(task.process(any())).thenReturn(true);
}
// tests ...
}
And this is what the root application context class looks like:
#EnableAutoConfiguration(exclude = ErrorMvcAutoConfiguration.class)
#SpringBootApplication
public class Application {
private static final Logger log = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
final SpringApplication app = new SpringApplication(Application.class);
app.setBannerMode(Banner.Mode.OFF);
app.run(args);
}
}
edit:
I have also tried using #MockBean, like so:
public class WebhookProcessorIT extends BaseIntegrationTest {
#MockBean
private WebhookTask task;
but I get the same result when the tests run. I can see that Spring is trying to override the production bean with the mock I am providing when I look at the logs during the test setup:
build 15-Sep-2016 09:09:24 2016-09-15 09:09:24 [34mINFO [0;39m [36m[DefaultListableBeanFactory][0;39m (main) Overriding bean definition for bean 'productionWebhookTask' with a different definition
however when it comes to test execution, i can still see the production bean being used:
build 15-Sep-2016 09:09:29 2016-09-15 09:09:29 [39mDEBUG[0;39m [36m[WebhookSupplier][0;39m (WebhookProcessor) Received webhook with ID '1234' from queue.
build 15-Sep-2016 09:09:30 2016-09-15 09:09:30 [39mDEBUG[0;39m [36m[WebhookSupplier][0;39m (WebhookProcessor) Received webhook with ID '5678' from queue.
build 15-Sep-2016 09:09:30 2016-09-15 09:09:30 [39mDEBUG[0;39m [36m[ProductionWebhookTask][0;39m (WebhookProcessor) Received webhook with ID '1234' for processing // production webhook task bean still being used for webhook '1234'
build 15-Sep-2016 09:09:30 2016-09-15 09:09:30 [39mDEBUG[0;39m [36m[WebhookSupplier][0;39m (WebhookProcessor) Deleting webhook with id '5678' from queue. // mock bean causes production logic to be skipped and we just delete webhook '5678'
// More logs from production webhook task operating on webhook with id '1234' and causing the test to fail
Anyway, you can annotate your test beans with #Profile("test") and your real ones with #Profile("production")
and in your properties file put the property spring.profiles.active=test
from documentation
Unlike regular #Configuration classes the use of #TestConfiguration
does not prevent auto-detection of #SpringBootConfiguration.
Unlike a nested #Configuration class which would be used instead of a
your application’s primary configuration, a nested #TestConfiguration
class will be used in addition to your application’s primary
configuration.
Since you use Spring Boot version 1.4.0, you can go on use the new introduced annotation #MockBean instead of using different configuration classes to mock your original beans. It's straight forward and very suitable to your use case.
Here you go with an example from the documentation

Resources