DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Actuator Enhancements: Spring Framework 6.2 and Spring Boot 3.4
  • How Spring Boot Starters Integrate With Your Project
  • A Practical Guide to Creating a Spring Modulith Project
  • Structured Logging in Spring Boot 3.4 for Improved Logs

Trending

  • Key Considerations in Cross-Model Migration
  • Transforming AI-Driven Data Analytics with DeepSeek: A New Era of Intelligent Insights
  • Rust and WebAssembly: Unlocking High-Performance Web Apps
  • Automatic Code Transformation With OpenRewrite
  1. DZone
  2. Coding
  3. Frameworks
  4. How to Mock, Spy, and Fake Spring Beans

How to Mock, Spy, and Fake Spring Beans

Learn how to mock Spring Beans in the current Java ecosystem.

By 
Lubos Krnac user avatar
Lubos Krnac
·
Jan. 08, 16 · Tutorial
Likes (9)
Comment
Save
Tweet
Share
196.8K Views

Join the DZone community and get the full member experience.

Join For Free

EDIT: As of Spring Boot 1.4.0, faking of Spring Beans is supported natively via annotation @MockBean. Read Spring Boot docs for more info.

About a year ago, I wrote a blog post on how to mock Spring Beans. The patterns described there were a little bit invasive to the production code.  As one of the readers Colin correctly pointed out in a comment. Today I'm introducing a better way to spy/mock Spring Beans based on the @Profile annotation. This blog post is going to describe this technique. I used this approach with success at work and also in my side projects.

Note that widespread mocking in your application is often considered as design smell.

Introducing Production Code

First of all, we need code under test to demonstrate mocking. We will use these simple classes:

@Repository
public class AddressDao {
    public String readAddress(String userName) {
        return "3 Dark Corner";
    }
}

@Service
public class AddressService {
    private AddressDao addressDao;

    @Autowired
    public AddressService(AddressDao addressDao) {
        this.addressDao = addressDao;
    }

    public String getAddressForUser(String userName){
        return addressDao.readAddress(userName);
    }
}

@Service
public class UserService {
    private AddressService addressService;

    @Autowired
    public UserService(AddressService addressService) {
        this.addressService = addressService;
    }

    public String getUserDetails(String userName){
        String address = addressService.getAddressForUser(userName);
        return String.format("User %s, %s", userName, address);
    }
}

Of course this code doesn’t make much sense, but it will be good to demonstrate how to mock Spring Beans. AddressDao just returns a string and thus simulates a read from some data source. It is autowired into AddressService. This bean is autowired into UserService, which is used to construct a string with the user name and address.

Notice that we are using constructor injection because field injection is considered a bad practice. If you want to enforce constructor injection for your application, Oliver Gierke (Spring ecosystem developer and Spring Data lead) recently created very nice project called Ninjector.

Configuration to scan all these beans is pretty standard Spring Boot's main class:

@SpringBootApplication
public class SimpleApplication {
    public static void main(String[] args) {
        SpringApplication.run(SimpleApplication.class, args);
    }
}

Mock Spring Beans (Without AOP)

Let’s test the AddressService class where we mock AddressDao. We can create this mock via Spring’s @Profiles and @Primary annotations this way:

@Profile("AddressService-test")
@Configuration
public class AddressDaoTestConfiguration {
    @Bean
    @Primary
    public AddressDao addressDao() {
        return Mockito.mock(AddressDao.class);
    }
}

This test configuration will be applied only when Spring profile's AddressService-test is active. When it’s applied, it registers a Bean with the type AddressDao, which is a mock instance created by Mockito. The @Primary annotation tells Spring to use this instance instead of a real one when somebody autowires the AddressDao Bean.

Test class is using JUnit framework:

@ActiveProfiles("AddressService-test")
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(SimpleApplication.class)
public class AddressServiceITest {
    @Autowired
    private AddressService addressService;

    @Autowired
    private AddressDao addressDao;

    @Test
    public void testGetAddressForUser() {
        // GIVEN
        Mockito.when(addressDao.readAddress("john"))
            .thenReturn("5 Bright Corner");

        // WHEN 
        String actualAddress = addressService.getAddressForUser("john");

        // THEN   
        Assert.assertEquals("5 Bright Corner", actualAddress);
    }
}

We activate the profile AddressService-test to enable AddressDao mocking. The annotation @RunWith is needed for Spring integration tests and @SpringApplicationConfiguration defines which Spring configuration will be used to construct the context for testing. Before the test, we autowire an instance of AddressService under test and the AddressDao mock.

Subsequent testing methods should be clear if you are using Mockito. In the GIVEN phase, we record the desired behavior into a mock instance. In the WHEN phase, we execute testing code, and in the THEN phase, we verify if testing code returned the value we expect.

Spy on Spring Beans (Without AOP)

For a spying example, we will be spying on the AddressService instance:

@Profile("UserService-test")
@Configuration
public class AddressServiceTestConfiguration {
    @Bean
    @Primary
    public AddressService addressServiceSpy(AddressService addressService) {
        return Mockito.spy(addressService);
    }
}

This Spring configuration will be component-scanned only if the profile UserService-test is active. It defines primary Bean of type AddressService. @Primary tells Spring to use this instance in case two Beans of this type are present in the Spring context. During the construction of this Bean, we autowire an existing instance of AddressService from the Spring context and use Mockito’s spying feature. The Bean we are registering is effectively delegating all the calls to the original instance, but Mockito spying allows us to verify interactions on a spied instance.

We will test the behavior of UserService this way:

@ActiveProfiles("UserService-test")
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(SimpleApplication.class)
public class UserServiceITest {
    @Autowired
    private UserService userService;

    @Autowired
    private AddressService addressService;

    @Test
    public void testGetUserDetails() {
        // GIVEN - Spring scanned by SimpleApplication class

        // WHEN
        String actualUserDetails = userService.getUserDetails("john");

        // THEN
        Assert.assertEquals("User john, 3 Dark Corner", actualUserDetails);
        Mockito.verify(addressService).getAddressForUser("john");
    }
}

For testing, we activate the UserService-test profile so our spying configuration will be applied. We autowire UserService, which is under test, and AddressService, which is being spied via Mockito.

We don’t need to prepare any behavior for testing in the GIVEN phase. The WHEN phase is obviously executing code under test. In the THEN phase, we verify if the testing code returned the value we expect and also if the addressService call was executed with correct parameters.

Problems With Mockito and Spring AOP

Let's say that we now want to use the Spring AOP module to handle some cross-cutting concerns. For example, to log calls on our Spring Beans in this way, we use this code:

package net.lkrnac.blog.testing.mockbeanv2.aoptesting;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Aspect
@Component
@Slf4j
@Profile("aop") //only for example purposes
public class AddressLogger {
    @Before("execution(* net.lkrnac.blog.testing.mockbeanv2.beans.*.*(..))")
    public void logAddressCall(JoinPoint jp){
        log.info("Executing method {}", jp.getSignature());
    }
}

This AOP Aspect is applied before the call on Spring Beans from the package net.lkrnac.blog.testing.mockbeanv2. It is using Lombok’s annotation @Slf4j to log the signature of the called method. Notice that this bean is created only when the aop profile is defined. We are using this profile to separate AOP and non-AOP testing examples. In a real application you wouldn’t want to use such a profile.

We also need to enable AspectJ for our application, therefore all the following examples will be using this Spring Boot main class:

@SpringBootApplication
@EnableAspectJAutoProxy
public class AopApplication {
    public static void main(String[] args) {
        SpringApplication.run(AopApplication.class, args);
    }
}

 AOP constructs are enabled by @EnableAspectJAutoProxy.

Such AOP constructs may be problematic if we combine Mockito for mocking with Spring AOP. That's because both use CGLIB to proxy real instances, and when the Mockito proxy is wrapped into the Spring proxy, we can experience type mismatch problems. These can be mitigated by configuring a Bean’s scope with ScopedProxyMode.TARGET_CLASS, but Mockito verify() calls will still fail with NotAMockException. These problems can be seen if we enable the aop profile for UserServiceITest.

Mock Spring Beans Proxied by Spring AOP

To overcome these problems, we will wrap a mock into this Spring bean:

package net.lkrnac.blog.testing.mockbeanv2.aoptesting;

import org.mockito.Mockito;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository;

import lombok.Getter;
import net.lkrnac.blog.testing.mockbeanv2.beans.AddressDao;

@Primary
@Repository
@Profile("AddressService-aop-mock-test")
public class AddressDaoMock extends AddressDao{
    @Getter
    private AddressDao mockDelegate = Mockito.mock(AddressDao.class);

    public String readAddress(String userName) {
        return mockDelegate.readAddress(userName);
    }
}

@Primary makes sure that this Bean will take precedence over the real AddressDao Bean during injection. To make sure it will be applied only for specific tests, we define the profile AddressService-aop-mock-test for this Bean. It inherits the AddressDao class so that it can act as a full replacement of that type.

In order to fake behavior, we define a mock instance of type AddressDao, which is exposed via getters defined by Lombok’s @Getter annotation. We also implement the readAddress() method, which is expected to be called during tests. This method just delegates the call to the mock instance.

The test where this mock is used can look like this:

@ActiveProfiles({"AddressService-aop-mock-test", "aop"})
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(AopApplication.class)
public class AddressServiceAopMockITest {
    @Autowired
    private AddressService addressService; 

    @Autowired
    private AddressDao addressDao;

    @Test
    public void testGetAddressForUser() {
        // GIVEN
        AddressDaoMock addressDaoMock = (AddressDaoMock) addressDao;
        Mockito.when(addressDaoMock.getMockDelegate().readAddress("john"))
            .thenReturn("5 Bright Corner");

        // WHEN 
        String actualAddress = addressService.getAddressForUser("john");

        // THEN  
        Assert.assertEquals("5 Bright Corner", actualAddress);
    }
}

In the test we define the AddressService-aop-mock-test profile to activate AddressDaoMock, the aop profile and the AddressLogger AOP aspect. For testing, we autowire the testing Bean addressService and its faked dependency, addressDao. As we know, addressDao will have a type of AddressDaoMock because this bean was marked as @Primary. Therefore, we can cast it and record its behavior into mockDelegate.

When we call the testing method, recorded behavior should be used because we expect the testing method to use the AddressDao dependency.

Spy on a Spring Bean Proxied by Spring AOP

A similar pattern can be used for spying on the real implementation. This is how our spy can look:

package net.lkrnac.blog.testing.mockbeanv2.aoptesting;

import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;

import lombok.Getter;
import net.lkrnac.blog.testing.mockbeanv2.beans.AddressDao;
import net.lkrnac.blog.testing.mockbeanv2.beans.AddressService;

@Primary
@Service
@Profile("UserService-aop-test")
public class AddressServiceSpy extends AddressService{
    @Getter
    private AddressService spyDelegate;

    @Autowired
    public AddressServiceSpy(AddressDao addressDao) {
        super(null);
        spyDelegate = Mockito.spy(new AddressService(addressDao));
    }

    public String getAddressForUser(String userName){
        return spyDelegate.getAddressForUser(userName);
    }
}

As we can see, this spy is very similar to AddressDaoMock. But in this case, the real Bean is using constructor injection to autowire its dependency. Therefore, we’ll need to define non-default constructors and do constructor injection also. But we won’t pass injected dependencies into parent constructor.

To enable spying on a real object, we construct a new instance with all the dependencies, wrap it into a Mockito spy instance, and store it in the spyDelegate property. We expect to call getAddressForUser() during testing, therefore we delegate this call to spyDelegate. This property can be accessed in tests via the getter defined by Lombok’s @Getter annotation.

The test itself will look like this:

@ActiveProfiles({"UserService-aop-test", "aop"})
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(AopApplication.class)
public class UserServiceAopITest {
    @Autowired
    private UserService userService;

    @Autowired
    private AddressService addressService;

    @Test
    public void testGetUserDetails() {
        // GIVEN
        AddressServiceSpy addressServiceSpy = (AddressServiceSpy) addressService;

        // WHEN
        String actualUserDetails = userService.getUserDetails("john");

        // THEN 
        Assert.assertEquals("User john, 3 Dark Corner", actualUserDetails);
        Mockito.verify(addressServiceSpy.getSpyDelegate()).getAddressForUser("john");
    }
}

It is very straight forward. Profile UserService-aop-test ensures that AddressServiceSpy will be scanned. Profile aop ensures same for the AddressLogger aspect. When we autowire the testing object UserService and its dependency AddressService, we know that we can cast it to AddressServiceSpy and verify the call on its spyDelegate property after calling the testing method.

Faking a Spring Bean Proxied by Spring AOP

It is obvious that delegating calls into Mockito mocks or spies complicates the testing. These patterns are often overkill if we simply need to fake the logic. We can use a fake in that case:

@Primary
@Repository
@Profile("AddressService-aop-fake-test")
public class AddressDaoFake extends AddressDao{
    public String readAddress(String userName) {
        return userName + "'s address";
    }
}

and use it for testing this way:

@ActiveProfiles({"AddressService-aop-fake-test", "aop"})
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(AopApplication.class)
public class AddressServiceAopFakeITest {
    @Autowired
    private AddressService addressService; 

    @Test
    public void testGetAddressForUser() {
        // GIVEN - Spring context

        // WHEN 
        String actualAddress = addressService.getAddressForUser("john");

        // THEN  
        Assert.assertEquals("john's address", actualAddress);
    }
}

I don’t think this test needs explanation.

Source code for these examples is hosted on Github.

Spring Framework Spring Boot

Published at DZone with permission of Lubos Krnac, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Actuator Enhancements: Spring Framework 6.2 and Spring Boot 3.4
  • How Spring Boot Starters Integrate With Your Project
  • A Practical Guide to Creating a Spring Modulith Project
  • Structured Logging in Spring Boot 3.4 for Improved Logs

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!