#3848·mockito

@Retryable annotation over the method of Spring service breaks stubbing when @SpyBean applied

Author: stsypanovCreated Jul 30, 2026Updated Aug 4, 2026

Originally asked as a SO question at https://stackoverflow.com/questions/79989231/migration-from-mockbean-to-spybean-breaks-answer-chain-containing-dothrow.

In my project I have an integration test targeted to verify behavior when multiple messages are sent to external system and some of them succeed and others fail. Currently it's implemented with @MockBean:

java
class SendMultipleMessagesIntgTest extends TestBase {

    @MockBean
    private ClientService clientService;

    @BeforeEach
    public void setUp() {
        when(clientService.sendMessage(any(MessageRequestDto.class)))
                .thenReturn(new MessageResponseDto()
                        .setFile(new FileDataDto().setId("test-file-id"))
                        .setSwiftId("test-swift-id-" + System.currentTimeMillis())
                );
    }
//...
}

In order to be able to call the real method from a test method of the same test class and to reuse Spring application context I'm rewriting the code above to use @SpyBean inherited from TestBase:

java
@SpringBootTest
abstract class TestBase {
  @SpyBean
  protected ExternalMessagingClientService externalMessagingClientService;
//...
}

class SendMultipleMessagesIntgTest extends TestBase {

    @BeforeEach
    public void setUp() {
        doReturn(new MessageResponseDto()
                .setFile(new FileDataDto().setId("test-file-id"))
                .setSwiftId("test-swift-id-" + System.currentTimeMillis())
        ).when(clientService).sendMessage(any(MessageRequestDto.class));
    }
//...
}

When I rely on the stubbing declared in setUp() method everything is fine. The following stubbing also works fine after migration to @SpyBean:

java
when(clientService.sendMessage(any(MessageRequestDto.class)))
        .thenReturn(new MessageResponseDto()
                .setFile(new FileDataDto().setId("test-file-id-1")))
        .thenReturn(new MessageResponseDto()
                .setFile(new FileDataDto().setId("test-file-id-2")));
-->
 doReturn(new MessageResponseDto().setFile(new FileDataDto().setId("test-file-id-1")),
         new MessageResponseDto().setFile(new FileDataDto().setId("test-file-id-3")))
         .when(clientService).sendMessage(any(MessageRequestDto.class));

But if there's an exception thrown between two return answers, the stubbing is broken and exception in fact is not thrown at all:

java
 when(clientService.sendMessage(any(MessageRequestDto.class)))
         .thenReturn(new MessageResponseDto().setFile(new FileDataDto().setId("test-file-id-1")))
         .thenThrow(new RuntimeException("External messaging service failed"))
         .thenReturn(new MessageResponseDto().setFile(new FileDataDto().setId("test-file-id-3")));

When I use mock and the test calls it 3 times, the mock returns, throws, returns. When I use spy it returns 3 times. This is observed only for cases when there's doThrow() between two doReturn().

I've tried to rewrite this template in two ways:
1)

java
doReturn(new MessageResponseDto()
        .setFile(new FileDataDto().setId("test-file-id-1")))
        .doThrow(new RuntimeException("External messaging service failed"))
        .doReturn(new MessageResponseDto().setFile(new FileDataDto().setId("test-file-id-3")))
        .when(externalMessagingClientService).sendMessage(any(MessageRequestDto.class));
java
doAnswer((new Answer<MessageResponseDto>() {
    int call;
    @Override
    public MessageResponseDto answer(InvocationOnMock invocation) {
        if (call == 0) {
            call++;
            return new MessageResponseDto().setFile(new FileDataDto().setId("test-file-id-1"));
        }
        if (call == 1) {
            call++;
            throw new RuntimeException("External messaging service failed");
        }
        return new MessageResponseDto().setFile(new FileDataDto().setId("test-file-id-3"));
    }
})).when(clientService).sendMessage(any(MessageRequestDto.class));

Calling Mockito.reset() before stubbing doesn't help.

ClientService is a plain Spring bean:

@Service
@RequiredArgsConstructor
public class ClientService implements IExternalMessagingClientService {

    private final RestTemplate restTemplate;
    private final ExternalMessagingServiceProperties clientProperties;
    private final WebClient webClient;

    @Override
    @SneakyThrows
    @Retryable
    public MessageResponseDto sendMessage(MessageRequestDto request) {/*...*/}

This example will work as expected if I remove @Retryable from the signature of ClientService.sendMessage() method.

Java8
Spring Boot Starter Parent 2.5,1
Jupiter 5.7.2
Mockito-inline 3.8.0
Mockito-core 3.9.0