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

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

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

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

  • Frequently Used Annotations in Spring Boot Applications
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements
  • Migration of Microservice Applications From WebLogic to Openshift
  • Micronaut With Relation Database and...Tests

Trending

  • Performance Optimization Techniques for Snowflake on AWS
  • AI’s Role in Everyday Development
  • Fixing Common Oracle Database Problems
  • Teradata Performance and Skew Prevention Tips
  1. DZone
  2. Coding
  3. Frameworks
  4. @MockBean—Spring Boot's Missing Ingredient

@MockBean—Spring Boot's Missing Ingredient

Testing support in Spring Boot is getting better, but it's far from perfect. The example here shows how to use Spring Boot in tandem with your mock testing.

By 
Grzegorz Poznachowski user avatar
Grzegorz Poznachowski
·
Jun. 28, 16 · Tutorial
Likes (22)
Comment
Save
Tweet
Share
87.1K Views

Join the DZone community and get the full member experience.

Join For Free
The problem

I really liked Spring Boot’s concept, since I first saw it. The only thing, I felt, it was missing was better support for testing in general.

The Problem

It all started when I wanted to have a way to test 'current date' logic in my application. It was supposed to be a reusable, easy-to-use feature (via an annotation) in a custom Spring Boot Starter. The starter is based on Java 8, hence JSR-310 Date / Time API is a natural pick. Current date is only one of several things I want to make "mockable" in integration tests. There are other areas of functionality that are good candidates for mocking out. Keeping that in mind, I will use the ZonedDateTime class as a mocking example across the article.

Serving Current Date in JSR-310

Java 8 comes with a new, redesigned API for handling Date and Time. It is designed after Joda-Time, but with many improvements and changes. Here is a nice StackOverflow post pointing out the most important ones. One of the nice features Joda-Time has is DateTimeUtils.setCurrentMillisFixed(long fixedMillis). With that method I was able to set fixed current time easily and reset to system time by invoking DateTimeUtils.setCurrentMillisSystem() after test execution. However, it did not make it through to the Java 8 API, probably due to the fact that Joda-Time implementation of the functionality was kind of hackish. A shared, static variable is used there for keeping the MillisProvider instance.

How do we achieve the same with JSR-310? The easiest and most basic way is to invoke ZonedDateTime.now(). However, this would be bad from a testing perspective. How would you test that? There are two more overloaded now methods, and we are mostly interested in now(Clock clock). Its Javadoc confirms that we are in the right place:

Obtains the current date-time from the specified clock.

This will query the specified clock to obtain the current date-time.
The zone and offset will be set based on the time-zone in the clock.

Using this method allows the use of an alternate clock for testing.
The alternate clock may be introduced using dependency injection.

@param clock the clock to use, not null
@return the current date-time, not null

— ZonedDateTime.now(Clock clock) Javadoc

Ok, so now we need a Clock instance to pass in. How do we get it? The second part of the Javadoc highlighted text has the answer. Treat it as a normal dependency and use dependency injection for that. Hence, the easiest way is to declare a Clock bean in your application:

Specyfing Clock bean:
@Bean
public Clock clock() {
    return Clock.systemDefaultZone();
}

and use it as a regular dependency.

Tip

Since Spring 4.3.RC1 you don’t need to put @Autowired annotation with a single constructor class. So, if you use Lombok and constructor injection (you always should!), you can reduce your initial code to:

@Service
@AllArgsConstructor
public class ClockService {
    private final Clock clock;
}

Spring Boot supports this since 1.4.0.M2.

Before Spring Boot 1.4.0.M2

It’s time for the main course. How are we going to test current date backed up with the injected Clock?

The simplest solution I thought about was to use @Primary:

Specifying primary bean:
@Bean
@Primary
public Clock clock() {
    return Clock.fixed(Instant.parse("2010-01-10T10:00:00Z"), ZoneId.of("UTC"));
}

Primary beans are always preferred and picked in a situation where 2 or more beans of the same type are found. It works just fine for a single test case, but if you wanted to reuse it among your test classes, you would need to copy this definition over and over. One can extract it to a superclass and use inheritance. I find such a solution to be a design smell. If there are more candidates to mock, several artificial classes have to be created along with an inheritance tree, which would obscure the test code.

As stated before, the solution is going to be a part of a custom Spring Boot starter. Because of that, I was determined to make the Clock mocking mechanism annotation based. My first idea was to use @ContextConfiguration in my custom annotations, which would call the configuration of the mocked Clock with @Primary. After a few attempts my lack of Spring knowledge came out. Fortunately, Sam Brannen answered my question and explained it nicely. My next approach was to incorporate Spring profiles. In my test starter I have added profile specific configuration with @Primary mocked out bean dependencies.

Profile specific auto-configuration:
@Profile("fixedClock")
@Configuration
public class FixedClockAutoConfiguration {
}

To use them I had to run my integration tests with corresponding profiles via @ActiveProfiles. There were two major drawbacks I didn’t like with the solution:

  • Active profile names had to be provided explicitly, which would mean remembering all available profile names.
  • Mocked clock has some default value. I wanted to provide an option to override this value. That was possible via @TestPropertySource, but again, the property name had to be remembered.

Was there anything else I could do, but I’m just not aware of? Eventually, Phil Webb clarified that Spring Boot 1.3 does not have the tools I’m looking for. With the bad news he brought, he brought also hope… Spring Boot 1.4 is supposed to bring heavy testing enhancements. Simplified annotation naming, focused testing (on JSON, MVC or JPA slices), and desired mocking support.

I had to wait… and for the time being, I did a slight improvement over my profiles. I created a simple implementation of ActiveProfilesResolver, which enables picking proper profile via an annotation:

Example active profile resolver
public class TestProfilesResolver implements ActiveProfilesResolver {

    ImmutableMap<Class<? extends Annotation>, String> PROFILES = ImmutableMap.<Class<? extends Annotation>, String>builder()
            .put(Wiremock.class, "wireMock")
            .put(FixedClock.class, "fixedClock")
            .build();

    @Override
    public String[] resolve(Class<?> testClass) {
        List<String> profiles = Lists.newArrayList();
        Arrays.stream(testClass.getAnnotations()).forEach(annotation -> {
            Class<? extends Annotation> annotationType = annotation.annotationType();
            if (PROFILES.containsKey(annotationType)) {
                profiles.add(PROFILES.get(annotationType));
            }
        });
        return profiles.toArray(new String[profiles.size()]);
    }
}

To use it, annotate your integration test with @ActiveProfiles(resolver = TestProfilesResolver.class).

It worked, but it had to be a part of the application and not a part of the starters I was preparing. The reason for that is the mock candidates, which are located in many different starters. Without hardcoding all of them in a single one, it wouldn’t be particularly easy to accomplish.

Do It Like a Boss

Ok, so all the goodies are in place - Spring Boot 1.4.0.M2 is out for some time now. Mockito support included. All tutorials and documentation show, however, only the basic usage of the @MockBean annotation, which is specifying the annotation on a class field and using Mockito methods directly in the test class:

Example @MockBean usage:
@RunWith(SpringRunner.class)
@SpringBootTest
public class MockBeanIntegrationTest {

    @MockBean
    private SomeService someService;

    @Before
    public void setupMock() {
        when(someService.getResult())
            .thenReturn("success");
    }
}

With this in place, the SomeService dependency is mocked out and set up to return a "success" String anytime getResult() is invoked. Mocks are reset after each test method by default. There is also analogical support for spying beans via the @SpyBean annotation. It all works great, but there is more to that!

@MockBean, implemented as meta-annotation, combined with TestExecutionListener is something I was looking for the whole time. The idea is simple—create an annotation that would indicate mocking a particular dependency and handle mocking internals in the execution listener:

First thing we can do is define our annotation.

@Documented
@Inherited
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@MockBean(value = Clock.class, reset = MockReset.NONE) (1)
public @interface FixedClock {
    String value() default "2010-01-10T10:00:00Z";
}
  1. This is the only interesting part here. Anytime you annotate your test with @FixedClock, it substitutes the application context’s Clock-type bean with a mock. We are disabling mock reset deliberately—it will be handled by the TestExecutionListener.

TestExecutionListener is a feature in the Spring-test component, that allows you to plug 'Spring context'-aware custom code in JUnit test lifecycle phases. As explained in documentation, there are a couple of default listeners registered. You can use your own by using @TestExecutionListeners annotation on a given test, but a better way is to register it automatically via META-INF/spring.factories properties (under org.springframework.test.context.TestExecutionListener key). If the order of your listeners is important you can easily assign the order value by implementing Ordered or by annotating your listener with @Order.

TestExecutionListener interface has couple of methods:

public interface TestExecutionListener {
    void beforeTestClass(TestContext testContext) throws Exception;
    void prepareTestInstance(TestContext testContext) throws Exception;
    void beforeTestMethod(TestContext testContext) throws Exception;
    void afterTestMethod(TestContext testContext) throws Exception;
    void afterTestClass(TestContext testContext) throws Exception;
}
Tip

If you don’t want to implement all of them, you can help yourself with AbstractTestExecutionListener class, which provides empty method stubs.

Let’s see how our FixedClockListener can implement these methods:

public class FixedClockListener extends AbstractTestExecutionListener {

    @Override
    public void beforeTestClass(TestContext testContext) throws Exception {
        FixedClock classFixedClock = AnnotationUtils.findAnnotation(testContext.getTestClass(), FixedClock.class); (1)
        if (classFixedClock == null) {
            return;
        }
        mockClock(testContext, classFixedClock); (2)
    }

    @Override
    public void beforeTestMethod(TestContext testContext) throws Exception {
        FixedClock methodFixedClock = AnnotationUtils.findAnnotation(testContext.getTestMethod(), FixedClock.class); (6)
        if (methodFixedClock == null) {
            return;
        }
        verifyClassAnnotation(testContext); (7)
        mockClock(testContext, methodFixedClock);
    }

    @Override
    public void afterTestMethod(TestContext testContext) throws Exception {
        FixedClock methodFixedClock = AnnotationUtils.findAnnotation(testContext.getTestMethod(), FixedClock.class);
        if (methodFixedClock == null) {
            return;
        }
        verifyClassAnnotation(testContext);

        FixedClock classFixedClock = AnnotationUtils.findAnnotation(testContext.getTestClass(), FixedClock.class); (8)
        mockClock(testContext, classFixedClock);
    }

    @Override
    public void afterTestClass(TestContext testContext) throws Exception {
        FixedClock annotation = AnnotationUtils.findAnnotation(testContext.getTestClass(), FixedClock.class);
        if (annotation == null) {
            return;
        }
        reset(testContext.getApplicationContext().getBean(Clock.class)); (9)
    }

    private void verifyClassAnnotation(TestContext testContext) {
        FixedClock classAnnotation = AnnotationUtils.findAnnotation(testContext.getTestClass(), FixedClock.class);
        if (classAnnotation == null) {
            throw new IllegalStateException("@FixedClock class level annotation is missing.");
        }
    }

    private void mockClock(TestContext testContext, FixedClock fixedClock) {
        Instant instant = Instant.parse(fixedClock.value()); (3)
        Clock mockedClock = testContext.getApplicationContext().getBean(Clock.class); (4)
        when(mockedClock.instant()).thenReturn(instant); (5)
        when(mockedClock.getZone()).thenReturn(TimeZone.getDefault().toZoneId());
    }
}
  1. Simple check to see if a test class is annotated with our annotation. If not - skip further processing. Prefer AnnotationUtils.findAnnotation() over simple testClass().getAnnotations() if you want to allow your annotation to be a part of different, composed annotation.
  2. Extracted method for setting up our mock.
  3. Retrieved Instant object out of our annotation. Will be used as a mock stub value.
  4. Clock bean is mocked by @FixedClock annotation and here we are fetching the mock from the application context, so we can provide mocking stubs on the mock instance.
  5. Here we provide the stubs. As they have to be declared on method calls, we cannot simply declare Clock.fixed() here. Fortunately, there are only two methods to stub: instant(), and getZone().
  6. With test execution listener approach, we can handle overriden fixed Clock values per test method. Here again, a simple check if the method is in fact annotated. If not - skip processing.
  7. For test methods, we need to implement additional verification step. We need to check if the test class was annotated as well. @MockBean will work only if the test class was marked with it.
  8. After test method execution, revert the mock stub to what was specified globally (per test class).
  9. Eventually, when all tests ran, reset the mock.

With all we did so far, we can easily test code below:

@RestController("/api/time")
@AllArgsConstructor
public class TimeEndpoint {
    private final Clock clock;

    @GetMapping
    public ZonedDateTime getTime() {
        return ZonedDateTime.now(clock);
    }
}

and provide fixed current time in integration test:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@FixedClock
public class FixedClockTest {

    @LocalServerPort
    int port;

    @Before
    public void setUp() {
        RestAssured.port = this.port;
    }

    @Test
    public void testClock() throws Exception {
        get("/api/time")
        .then()
            .body(containsString("2010-01-10T10:00:00Z")); // default @FixedClock value

    }

    @Test
    @FixedClock("2011-11-11T11:00:00Z")
    public void testClockOverridden() throws Exception {
        get("/api/time")
        .then()
            .body(containsString("2011-11-11T11:00:00Z"));

    }
}


Summary

At last, it seems, that Spring Boot with 1.4.0.M2 release, received the last, missing piece in its testing toolbox.

The proposed solution will not only suit in a custom starter (but it’s a great fit). You can implement a similar solution in the actual application and you don’t have to limit yourself to mocking the current date. This approach let you mock anything you find appropriate, keeping your tests clean.

Note

Presented code samples are part of Neo-Starters - even more opinionated way of developing REST services :)

Spring Framework Spring Boot integration test Annotation Clock (cryptography) Dependency injection application Profile (engineering)

Published at DZone with permission of Grzegorz Poznachowski. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Frequently Used Annotations in Spring Boot Applications
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements
  • Migration of Microservice Applications From WebLogic to Openshift
  • Micronaut With Relation Database and...Tests

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!