In this post I'll write about how to reduce a bit of boilerplate code when writing tests in Spring Framework in a situation where we don't want to bring up the whole Spring context in order to test if only a subset of components are working together correctly.
Our goal will be to have Spring load only the components that we are interested in testing, mock out everything else and do it in a simple and readable way.
Let's start with an example. Our example application has a `UserService` with a `registerNewUser` method. Whenever a new user is added to the system, `UserService`'s `registerNewUser` method is called. This method adds the user to the system, hashes user's password, stores user data to the database, updates application's user statistics and sends out confirmation e-mail. To accomplish these tasks, `UserService` depends on `PasswordEncoder`, `UserRepository`, `StatisticsService` and `EmailService`.We want to write an integration test that will verify that when registerNewUser
is called, the user is indeed stored correctly to the database and user password is hashed. To do this, we want actual UserService
implementation brought up in our test Spring context, along with PasswordEncoder
and persistence related components and have everything else mocked out (side note: I'm using the term mock here, but since we're not verifying interactions with mock objects in the example tests, some testing vocabularies would refer to these kind of test objects as dummy objects).
To mock out the beans we'll be using Mockito library. Our JUnit test will be run with SpringJUnit4ClassRunner
. Demo application's code is available at GitHub.
Here's the test class:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=AnnotationConfigContextLoader.class)
public class UserServiceImplIntegrationTest {
@Autowired
UserService userService;
@Autowired
UserRepository userRepository;
@Transactional
@Test
public void testRegisterNewUser() {
final String userName = "someone";
final String password = "somepass";
userService.registerNewUser(new User("someone@example.com", userName, password));
User user = userRepository.findByUserName(userName);
Assert.assertNotNull(user);
Assert.assertTrue(SCryptUtil.check(password, user.getPassword()));
}
@Configuration
@Import(TestAppConfig.class)
static class ContextConfiguration {
@Bean
public UserService userService() {
return new UserServiceImpl();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new SCryptPasswordEncoder();
}
@Bean
public EmailService emailService() {
return Mockito.mock(EmailService.class);
}
@Bean
public StatisticsService statisticsService() {
return Mockito.mock(StatisticsService.class);
}
}
}
Let's examine the code:
With @ContextConfiguration(loader=AnnotationConfigContextLoader.class)
we instruct Spring to load test context configuration from the annotated class. We have defined the configuration class as a static inner class of our test class (ContextConfiguration
). We import common configuration that will be shared among different integration tests form TestAppConfig.class
. In the ContextConfiguration
we define the beans whose actual implementation will participate in the integration test (UserServiceImpl
and SCryptPasswordEncoder
) and we mock out their dependencies (EmailService
, StatisticsService
). The inspiration for this pattern of writing integration tests that combine actual and mocked beans originate from great answers found in this stack overflow question: Injecting Mockito mocks into a Spring bean.
The idea is to keep common configuration that we plan to reuse between various tests in TestAppConfig.class
(in our example, TestAppConfig
contains test database, JPA and Spring-data configuration) and add keep per test specific configuration in an inner class of the test class. We also manually define beans that we need to mock out in order for Spring to satisfy all the dependencies of the beans we plan to test.
This works just fine, expect in certain more complex integration tests, there will be a lot of beans to be mocked out, and we have to write lots of boilerplate code to define those. For example, to mock out EmailService
class we write:
@Bean
public EmailService emailService() {
return Mockito.mock(EmailService.class);
}
Characters "EmailService" are typed three times (Type definition, method name and as a parameter to mock method) and we hat to write 4 lines of code. If there's 10 beans to mock out, that's a lot of typing! Is there a way to make things more concise? It turns out there is, using the ImportBeanDefinitionRegistrar
. We will create an annotation that we'll use to specify which classes we plan to mock out, and then implement ImportBeanDefinitionRegistrar
that will look for the annotation and register bean definitions we specified as a mocked beans.
Here's the custom annotation:
@Import(MockImportRegistar.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MockedBeans {
/**
* Types that need to be mocked.
* @return
*/
Class<?>[] value() default {};
}
Here's the code for ImportBeanDefinitionRegistrar
implementation (note that we referenced MockImportRegistar
with @Import
in MockedBean
annotation above):
public class MockImportRegistar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (importingClassMetadata.isAnnotated(MockedBeans.class.getName())) {
Object mockedBeanTypesValue = importingClassMetadata.getAnnotationAttributes(MockedBeans.class.getName()).get("value");
if (mockedBeanTypesValue instanceof Class<?>[]) {
Class<?>[] mockedBeanTypes = (Class<?>[]) mockedBeanTypesValue;
if (mockedBeanTypes != null && mockedBeanTypes.length > 0) {
mockSpecifiedBeanTypes(registry, mockedBeanTypes);
}
}
}
}
private void mockSpecifiedBeanTypes(BeanDefinitionRegistry registry, Class<?>[] mockedBeanTypes) {
for (Class<?> mockedType : mockedBeanTypes) {
registry.registerBeanDefinition("mock" + mockedType.getSimpleName(),
BeanDefinitionBuilder
.rootBeanDefinition(Mockito.class)
.setFactoryMethod("mock")
.addConstructorArgValue(mockedType.getName())
.getBeanDefinition()
);
}
}
}
Now that have defined the annotation and the registrar we can rewrite the ContextConfiguration
like this:
@Configuration
@Import(TestAppConfig.class)
@MockedBeans({EmailService.class, StatisticsService.class})
static class ContextConfiguration {
@Bean
public UserService userService() {
return new UserServiceImpl();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new SCryptPasswordEncoder();
}
}
Beans that we need to be mocked are now defined using our @MockedBeans
annotation. I find this approach more concise, simpler and easier to maintain, especially for more complex integration tests.