Spring Boot - @SQSListener and @SpyBean are not working together?

Viewed 776

I have a simple spring boot application with AWS SQS integration. In integration tests I tried to spy on method annotated with @SqsListener annotation and got a behavior where bean sometimes wasn't spied.

You can take a look at sample project here: https://github.com/Sanych/aws-sqs-spybean-troubleshooting

run mvn clean verify

In this log one may notice, that 2 out of 3 test method were spied successfully:

2020-09-06 15:39:23.597  INFO 18372 --- [           main] com.aws.test.demo.SQSListenerIT          : Started SQSListenerIT in 0.813 seconds (JVM running for 24.024)
2020-09-06 15:39:23.604  WARN 18372 --- [           main] com.aws.test.demo.SQSListenerIT          : calling class com.aws.test.demo.SQSListener$MockitoMock$1506819983
2020-09-06 15:39:23.604  WARN 18372 --- [           main] com.aws.test.demo.SQSListenerIT          : sendMessage to queue: test 1
2020-09-06 15:39:23.810  WARN 18372 --- [enerContainer-2] com.aws.test.demo.SQSListener            : id: class com.aws.test.demo.SQSListener$MockitoMock$1506819983, received: 'test 1'
2020-09-06 15:39:23.929  WARN 18372 --- [           main] com.aws.test.demo.SQSListenerIT          : calling class com.aws.test.demo.SQSListener$MockitoMock$1506819983
2020-09-06 15:39:23.929  WARN 18372 --- [           main] com.aws.test.demo.SQSListenerIT          : sendMessage to queue: test 2
2020-09-06 15:39:24.052  WARN 18372 --- [enerContainer-2] com.aws.test.demo.SQSListener            : id: class com.aws.test.demo.SQSListener, received: 'test 2'
2020-09-06 15:39:29.148  WARN 18372 --- [           main] com.aws.test.demo.SQSListenerIT          : calling class com.aws.test.demo.SQSListener$MockitoMock$1506819983
2020-09-06 15:39:29.148  WARN 18372 --- [           main] com.aws.test.demo.SQSListenerIT          : sendMessage to queue: test 3
2020-09-06 15:39:29.175  WARN 18372 --- [enerContainer-2] com.aws.test.demo.SQSListener            : id: class com.aws.test.demo.SQSListener$MockitoMock$1506819983, received: 'test 3'
[ERROR] Tests run: 3, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 6.468 s <<< FAILURE! - in com.aws.test.demo.SQSListenerIT
[ERROR] test2  Time elapsed: 5.218 s  <<< ERROR!
org.awaitility.core.ConditionTimeoutException:
Assertion condition defined as a lambda expression in com.aws.test.demo.SQSListenerIT
Wanted but not invoked:
SQSListener.listen(<any string>);
-> at com.aws.test.demo.SQSListenerIT.lambda$sendMessageFromFileAndWaitForListenerToRead$0(SQSListenerIT.java:57)
Actually, there were zero interactions with this mock.
 within 5 seconds.
        at com.aws.test.demo.SQSListenerIT.sendMessageFromFileAndWaitForListenerToRead(SQSListenerIT.java:57)
        at com.aws.test.demo.SQSListenerIT.test2(SQSListenerIT.java:42)
Caused by: org.mockito.exceptions.verification.WantedButNotInvoked:

Wanted but not invoked:
SQSListener.listen(<any string>);
-> at com.aws.test.demo.SQSListenerIT.lambda$sendMessageFromFileAndWaitForListenerToRead$0(SQSListenerIT.java:57)
Actually, there were zero interactions with this mock.
        at com.aws.test.demo.SQSListenerIT.lambda$sendMessageFromFileAndWaitForListenerToRead$0(SQSListenerIT.java:57)

Am I missing something?

1 Answers

Ran into exactly the same problem.

From the SpyBeandocumentation:

All beans in the context of the same type will be wrapped with the spy.

After thorough investigation, the only reasonable explanation for me is that the SqsListener method / bean is registered before the MockitoPostProcessor actually wraps the existing Bean with a spy proxy.

As a result, the "real" bean will actually be invoked but this invocation cannot be validated with verify(...) because the invocation is not triggered through the Spy proxy.

Might be good to know: the @SqsListener methods are registered via the QueueMessageHandler, which is constructed by the SqsAutoConfiguration. Hence if the SqsAutoConfiguration runs before the MockitoPostProcessor wrapped the spy bean, the invocation does not go through the Spy proxy.

Possible solution

I used following approach to work around this problem..


public class MyService {

    @Autowired
    private OtherService otherService;

    @SqsListener("queue")
    public void listen(@NotificationMessage Event event) {
        oterService.doWork();
    }

}
  • Split unit tests (testing MyService functionally) from integration tests (only testing that MyService#listen is actually invoked)
  • In integration tests (@SpringBootTest): don't @SpyBean the listener bean itself, but a bean used by the listener. (OtherService in my example)
  • verify(otherService, timeout(500)).doWork()

Although this answer is rather abstract, I hope this can be of any help.

EDIT: Just mocking OtherService using MockBean doesn't suffice. I had to explicitly create a OtherService mock (mock(OtherService.class) and set it in my MyService using a package private setter. I admit, not the cleanest solution, however so far this is the only solution that worked for me.

EDIT (2022-02-16): As of Spring Cloud AWS 2.4, an @SqsTest annotation is available for exactly this purpose: https://github.com/awspring/spring-cloud-aws/releases/tag/v2.4.0 https://github.com/awspring/spring-cloud-aws/blob/v2.4.0/spring-cloud-aws-samples/spring-cloud-aws-sqs-sample/src/test/java/io/awspring/cloud/sqs/sample/SqsSampleApplicationTests.java

Related