@MockBean uses different instance in JMS listener when running multiple Spring Boot tests

Viewed 540

I've written a Spring Boot Test, that writes into a JMS queue and is expecting some processing via an JMS listener. In the listener, I'm trying to read an object from S3. The AmazonS3 class should be replaced by a MockBean. In my test I set up the mock like this:

@SpringBootTest
public class MyTest {

    @Autowired
    MyJmsPublisher jmsPlublisher;
    @MockBean
    AmazonS3 amazonS3;

    @Test
    public void test() {
        final S3Object s3Object = mock(S3Object.class);
        when(s3Object.getObjectContent()).thenReturn(mock(S3ObjectInputStream.class));
        when(amazonS3.getObject(anyString(), anyString())).thenReturn(s3Object);
        
        jmsPlublisher.publishMessage("mymessage");
        Awaitility.await().untilAsserted(() -> {
            //wait for something here
        });
    }
}


@Component
@RequiredArgsConstructor
public class MyJmsPublisher {

    private final JmsTemplate jmsTemplate;

    public void publishMessage(String message) {
        jmsTemplate.convertAndSend("destination", message);
    }
}

@Component
@RequiredArgsConstructor
public class MyJmsListener {

    private final AmazonS3 amazonS3;

    @JmsListener(destination = "destination")
    public void onMessageReceived(String message) {
        final S3ObjectInputStream objectContent = amazonS3.getObject("a", "b").getObjectContent();
        // some logic here
    }
}

But the issue is that when running multiple Spring Boot tests , the MyJmsListener class contains a mock that is different from the one created in the Test. It's a mock, but for example the getObjectContent() returns null. But when I run the test alone, everything works fine.

I've tried to inject the AmazonS3 bean into the MyJmsPublisher and call the mocked method there and it worked. So I suspect, that it's because the JMS listener operates in a different thread.

I've found this thread and also set the reset to all available options, but does not make any difference. I also tried this OP's approach that worked for them, where I create a mock via the @Bean annotation like this:

@Configuration
public class MyConfig {
   @Bean
   @Primary
   public AmazonS3 amazonS3() {
      return Mockito.mock(AmazonS3.class);
   }
}

But this just has the same behavior as mentioned above.

So can you actually use the @MockBean annotation when using different Threads like using a @JMSListener? Or am I missing something?

2 Answers

Spring Beans with methods annotated with @JmsListener are injecting beans leaked from previous test executions when activated by a secondary thread. A practical workaround is to configure the test executor to use an isolated VM for each class to avoid this issue.

For Maven executions you can configure the maven-failsafe-plugin or maven-surefire-plugin by setting the reuseForks option. e.g:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <configuration>
        <reuseForks>false</reuseForks>
    </configuration>
</plugin>

You can also easily change the Fork mode to Class in JUnit IDE tools for multiple tests execution. Example for IntelliJ:

IntelliJ JUnit Fork mode

Using @DirtiesContext does not work, and unfortunately, I still couldn't find the root cause for this - My hunch is that it could be something related to using an in-memory instance of the ActiveMQ broker, which lives in the VM instance shared by the executions.

We had a similar issue when using the @JmsListener annotation in combination with @MockBean/@SpyBean. In our case using a separate destination for each test class solved this problem:

@JmsListener(destination = "${my.mq.topic.name}")
void consume(TextMessage message){
...
}

@SpringBootTest
@TestPropertySource(properties = "my.mq.topic.name=UniqueTopicName"})
class MyConsumerIT{
...
}

As far as I understand Spring has to create a separate JMS Consumer for each topic/queue. This configuration forces Spring to create a separate JMS Consumer for this class and Inject it correctly into the TestContext. In comparison without this configuration, Spring reuses the once created Consumer for all test classes.

Related