Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

In this tutorial, we aim to address the difference between @Spy and @SpyBean, explaining their functionalities and providing guidance on when to employ each one.

2. Basic Application

For this article, we’ll use a simple order application that includes an order service to create orders and that calls a notification service to notify when processing an order.

OrderService has a save() method that takes in an Order object, saves it using OrderRepository, and invokes the NotificationService:

@Service
public class OrderService {

    public final OrderRepository orderRepository;

    public final NotificationService notificationService;

    public OrderService(OrderRepository orderRepository, NotificationService notificationService) {
        this.orderRepository = orderRepository;
        this.notificationService = notificationService;
    }
    
    public Order save(Order order) {
        order = orderRepository.save(order);
        notificationService.notify(order);
        if(!notificationService.raiseAlert(order)){
           throw new RuntimeException("Alert not raised");
        }
        return order;
    }
}

For simplicity, let’s assume that the notify() method logs the order. In reality, it can involve more complex actions, such as sending emails or messages to downstream applications via a queue.

Let’s also assume that every order created must receive an alert by calling an ExternalAlertService, which returns true if the alert is successful, and the OrderService will fail if it doesn’t raise the alert:

@Component
public class NotificationService {

    private ExternalAlertService externalAlertService;
    
    public void notify(Order order){
        System.out.println(order);
    }

    public boolean raiseAlert(Order order){
        return externalAlertService.alert(order);
    }

}

The save() method in OrderRepository saves the order object in memory using a HashMap:

public Order save(Order order) {
    UUID orderId = UUID.randomUUID();
    order.setId(orderId);
    orders.put(UUID.randomUUID(), order);
    return order;
}

3. @Spy and @SpyBean Annotations in Action

Now that we have a basic application in place, let’s see how to test different aspects of it with @Spy and @SpyBean annotations.

3.1. Mockito’s @Spy Annotation

The @Spy annotation, part of the Mockito testing framework, creates a spy (partial mock) of a real object and is commonly used for Unit Testing.

A spy allows us to track and optionally stub or verify specific methods of a real object while still executing the real implementation for other methods.

Let’s understand this by writing a unit test for the OrderService and annotating the NotificationService with @Spy:

@Spy
OrderRepository orderRepository;
@Spy
NotificationService notificationService;
@InjectMocks
OrderService orderService;

@Test
void givenNotificationServiceIsUsingSpy_whenOrderServiceIsCalled_thenNotificationServiceSpyShouldBeInvoked() {
    UUID orderId = UUID.randomUUID();
    Order orderInput = new Order(orderId, "Test", 1.0, "17 St Andrews Croft, Leeds ,LS17 7TP");
    doReturn(orderInput).when(orderRepository)
        .save(any());
    doReturn(true).when(notificationService)
        .raiseAlert(any(Order.class));
    Order order = orderService.save(orderInput);
    Assertions.assertNotNull(order);
    Assertions.assertEquals(orderId, order.getId());
    verify(notificationService).notify(any(Order.class));
}

In this case, the NotificationService acts as a spy object and invokes the real notify() method when no mock is defined. Furthermore, because we define a mock for the raiseAlert() method, the NotificationService behaves as a partial mock

3.2. Spring Boot’s @SpyBean Annotation

On the other hand, the @SpyBean annotation is specific to Spring Boot and is used for integration testing with Spring’s dependency injection.

It allows us to create a spy (partial mock) of a Spring bean while still using the actual bean definition from our application context.

Let’s add an integration test using @SpyBean for NotificationService:

@Autowired
OrderRepository orderRepository;
@SpyBean
NotificationService notificationService;
@SpyBean
OrderService orderService;

@Test
void givenNotificationServiceIsUsingSpyBean_whenOrderServiceIsCalled_thenNotificationServiceSpyBeanShouldBeInvoked() {

    Order orderInput = new Order(null, "Test", 1.0, "17 St Andrews Croft, Leeds ,LS17 7TP");
    doReturn(true).when(notificationService)
        .raiseAlert(any(Order.class));
    Order order = orderService.save(orderInput);
    Assertions.assertNotNull(order);
    Assertions.assertNotNull(order.getId());
    verify(notificationService).notify(any(Order.class));
}

In this case, the Spring application context manages the NotificationService and injects it into the OrderService. Invoking notify() within NotificationService triggers the execution of the real method, and invoking raiseAlert() triggers the execution of the mock.

4. Differences Between @Spy and @SpyBean

Let’s understand the difference between @Spy and @SpyBean in detail.

In unit testing, we utilize @Spy, whereas in integration testing, we employ @SpyBean.

If the @Spy annotated component contains other dependencies, we can declare them during initialization. If they’re not provided during initialization, the system will use a zero-argument constructor if available. In the case of the @SpyBean test, we must use the @Autowired annotation to inject the dependent component. Otherwise, during runtime, Spring Boot creates a new instance.

If we use @SpyBean in the unit test example, the test will fail with a NullPointerException when NotificationService gets invoked because OrderService expects a mock/spy NotificationService.

Likewise, if @Spy is used in the example of the integration test, the test will fail with the error message ‘Wanted but not invoked: notificationService.notify(<any com.baeldung.spytest.Order>),’ because the Spring application context is not aware of the @Spy annotated class. Instead, it creates a new instance of NotificationService and injects it into OrderService.

5. Conclusion

In this article, we explored @Spy and @SpyBean annotations and when to use them.

As always, the source code for the examples is available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.