Migration from @MockBean to @SpyBean breaks answer chain containing doThrow()
09:57 30 Jul 2026

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

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:

@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:

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:

 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)

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));
doAnswer((new Answer() {
    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) {/*...*/}

What am I doing wrong?

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

java spring mockito