Enterprise Java

Using @MockitoSpyBean in Spring

Testing is an essential part of building reliable Spring Boot applications. Often, we need to observe or modify the behaviour of specific beans without executing their full logic. This is where spies come in; they allow us to wrap an existing bean, override certain methods, and verify interactions during tests.

With Spring Framework 6.2, a new annotation called @MockitoSpyBean was introduced to simplify this process. It integrates closely with Mockito, giving us more predictable behaviour and finer control when working with spies in the Spring test context.

This article explains what @MockitoSpyBean is, and how it differs from the older @SpyBean.

1. What is @MockitoSpyBean?

@MockitoSpyBean is a Spring test annotation used to replace a real bean in the test ApplicationContext with a Mockito spy. A spy wraps the original bean, allowing real method calls by default while still giving us the ability to override or verify behaviour during testing.

We can apply @MockitoSpyBean on fields or at the class level. It works on test classes, their parent classes, and on enclosing classes for @Nested tests. This makes it flexible and usable in a wide range of Spring test setups.

A spy can only be created for real beans registered in the Spring context, not for components that are only added as resolvable dependencies. @MockitoSpyBean fields also support any visibility level and are inherited by nested test classes. The annotation is repeatable and can be used as a meta-annotation to define reusable spy configurations in a shared annotation.

2. How @MockitoSpyBean Differs from @SpyBean

Before Spring Framework 6.2, @SpyBean was the main annotation used for spying on beans. It works well for many scenarios, but in some cases, it can interfere with how Spring manages beans. @SpyBean creates a brand-new spy instance and replaces the original bean in the application context. When proxies or lifecycle features like transactions are involved, this replacement may affect how the bean behaves during tests.

@MockitoSpyBean takes a different approach. Instead of creating a new bean, it wraps the actual bean that Spring has already initialised. This means the bean keeps its proxy structure, configuration, and lifecycle exactly as it would in production. The result is more stable and predictable behaviour when writing tests that involve AOP, transactions, or other advanced Spring features.

3. Using @MockitoSpyBean in Practice

Suppose we have a service class called PaymentService that processes payments. It depends on a PaymentRepository for storing payment details and a NotificationService for sending notifications after processing.

@Service
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final NotificationService notificationService;

    public PaymentService(PaymentRepository paymentRepository, NotificationService notificationService) {
        this.paymentRepository = paymentRepository;
        this.notificationService = notificationService;
    }

    public Payment processPayment(Payment payment) {
        Payment savedPayment = paymentRepository.save(payment);
        notificationService.notify(savedPayment);
        return savedPayment;
    }
}

The supporting classes:

public record Payment(UUID id, Double amount) {

}
@Repository
public class PaymentRepository {

    private static final Logger logger = Logger.getLogger(PaymentRepository.class.getName());

    private final Map<UUID, Payment> payments = new HashMap<>();

    public Payment save(Payment payment) {
        UUID paymentId = UUID.randomUUID();
        Payment savedPayment = new Payment(paymentId, payment.amount());
        payments.put(paymentId, savedPayment);
        logger.log(Level.INFO, "Payment saved: {0}", payment.id());
        return savedPayment;
    }
}
@Service
public class NotificationService {

    private static final Logger logger = Logger.getLogger(NotificationService.class.getName());

    public void notify(Payment payment) {
        // Logic to send notification
        logger.log(Level.INFO, "Notification sent for payment: {0}", payment.id());
    }
}

Testing with @MockitoSpyBean

We want to spy on PaymentService so we can verify that it triggers the NotificationService during payment processing, and also override its behaviour when testing specific payment scenarios.

@SpringBootTest
class PaymentServiceTest {

    @Autowired
    PaymentRepository paymentRepository;

    @MockitoSpyBean
    NotificationService notificationService;

    @MockitoSpyBean
    PaymentService paymentService;

    @Test
    void testProcessPayment() {

        UUID paymentId = UUID.randomUUID();
        Payment paymentInput = new Payment(paymentId, 250.0);

        Mockito.doNothing().when(notificationService).notify(any(Payment.class));

        Payment savedPayment = paymentService.processPayment(paymentInput);

        Assertions.assertNotNull(savedPayment);
        Assertions.assertNotNull(savedPayment.id());
        Assertions.assertEquals(250.0, savedPayment.amount());

        Mockito.verify(notificationService).notify(any(Payment.class));
    }
}

In this test, NotificationService and PaymentService are wrapped with @MockitoSpyBean, allowing the real beans to execute their logic while giving us the ability to override or verify behaviour. A new Payment is created and passed to processPayment. When processPayment is called, the payment is saved via PaymentRepository, and the notify method is invoked. Mockito.verify confirms that NotificationService.notify() was called.

This demonstrates how @MockitoSpyBean lets us spy on real Spring beans, observe interactions, and override specific behaviour in a controlled, testable way.

4. Migration Considerations

If your project currently uses @SpyBean, migrating to @MockitoSpyBean is generally straightforward. Replace the existing @SpyBean annotations with @MockitoSpyBean and run your tests to ensure that the behaviour remains consistent, particularly for beans with complex proxies or transactional behaviour. In most cases, this migration not only preserves existing functionality but also provides improved stability, tighter integration with modern Mockito features, and greater control over spying and verification in Spring tests.

5. Conclusion

In this article, we explored @MockitoSpyBean and its role in improving testing for Spring applications. We highlighted its advantages over the older @SpyBean, such as tighter integration with Mockito, preservation of bean proxies, and more predictable behaviour in complex test scenarios. Using a practical example, we showed how to spy on real Spring beans, override specific methods, and verify interactions without altering the underlying logic.

Incorporating @MockitoSpyBean in your tests can help create more reliable and maintainable test suites in modern Spring Boot projects.

6. Download the Source Code

This article covered the use of Spring MockitoSpyBean.

Download
You can download the full source code of this example here: spring mockitospybean

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button