@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.
Join the DZone community and get the full member experience.
Join For FreeI 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
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:
@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
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
:
@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("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:
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:
@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";
}
- 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 theTestExecutionListener
.
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 |
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());
}
}
- Simple check to see if a test class is annotated with our annotation. If not - skip further processing. Prefer
AnnotationUtils.findAnnotation()
over simpletestClass().getAnnotations()
if you want to allow your annotation to be a part of different, composed annotation. - Extracted method for setting up our mock.
- Retrieved
Instant
object out of our annotation. Will be used as a mock stub value. 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.- 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()
, andgetZone()
. - 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.
- 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. - After test method execution, revert the mock stub to what was specified globally (per test class).
- 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 :) |
Published at DZone with permission of Grzegorz Poznachowski. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments