Advanced unit testing (with) your Spring configuration


If you drop XML and configure your Spring web application in Java, you’ll still want to test your application. In fact, you can now also test your configuration.
A previous blog post describes how.
This post goes further: it describes how you can use the @Profile annotation to go beyond simple unit tests to component tests — testing our components as taken from the Spring application context, connected to mocked or to a stubbed objects (for example an in-memory database).


Introduction




You test your applications. Usually, you’ll do more than just ad hoc testing, and automate some (if not all) tests.
Tutorials on unit tests and integration tests are legion, but we’ve found that unit testing persistence is very difficult and often does not yield the correct results at reasonable cost.
We use a component test1 for database access instead, with a stubbed (i.e. in-memory) database.



These component tests greatly reduce the cost of writing tests for our data access code, including our service layer.
Not that we test it in bulk — on the contrary: we still test each unit individually.
But by connecting to an in-memory database we can focus more on the interface/API of the code to test than on the JPA Criteria API, setting up a data structure for the test, etc.



There is only one downside to this: testing our controllers is more like an integration test than a unit test.
In the past we’ve alleviated this problem by overriding Spring beans with mocked dependencies, but the required rewiring and reloading of the Spring context (using @DirtiesContext) is a slow hack.
Our current solution, as outlined in this post, is to mock the services for controller tests (only).




A component test tests connected components at the code level, as opposed to a unit test where all dependencies are mocked.
For a more detailed explanation, see: What are unit, component and integration testing?



Configuration



The configuration required for our setup is as follows:



  • As many beans for all configurations as possible. This includes at least the EntityManagerFactory, transaction handling

  • Profile production: the JNDI DataSource to access the database, a JNDI environment entry for the database dialect, etc.

  • Profile testing: Same as the production profile, but without JNDI. We simply setup the beans in code.

  • Profile mockedServices: contains mocked beans for all services




This setup poses one important restriction: the service beans must all have an interface for their API.
The reason: the service beans are proxied by Spring to provide transaction handling, and the mock objects may not be proxied.
If the mocked beans are proxied, our mocking framework gets a foreign proxy, and not the mock it can manipulate.



Specifically, this means we have this configuration in our normal code:




@Configuration
@ComponentScan(basePackages = "base.package", excludeFilters = )
@EnableTransactionManagement(proxyTargetClass = true)
@PropertySource("classpath:persistence.properties")
@Import(SpringMvcConfiguration.class)
@Profile("default")
public class ApplicationConfiguration {
@Autowired
private DataSource dataSource;
@Autowired
private String hibernateDialect;
@Value("$")
private String hibernateSchemaStrategy;
@Value("$")
private String hibernateLogSql;
@Value("$")
private String hibernateFormatSql;
@Value("$")
private String hibernateUseSqlCommands;


@Bean
public AbstractEntityManagerFactoryBean entityManagerFactory() {
Map<String, Object> jpaProperties = new HashMap<String, Object>();
jpaProperties.put("hibernate.dialect", hibernateDialect);
jpaProperties.put("hibernate.hbm2ddl.auto", hibernateSchemaStrategy);
jpaProperties.put("hibernate.show_sql", hibernateLogSql);
jpaProperties.put("hibernate.format_sql", hibernateFormatSql);
jpaProperties.put("hibernate.use_sql_comments", hibernateUseSqlCommands);

LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
localContainerEntityManagerFactoryBean.setJpaPropertyMap(jpaProperties);
localContainerEntityManagerFactoryBean.setDataSource(dataSource);
return localContainerEntityManagerFactoryBean;
}

@Bean
public SqlDatabaseFeeder importStartingDataIfAvailable() {
SqlDatabaseFeeder feeder = new SqlDatabaseFeeder();
feeder.setFailOnScriptNotFound(false);
feeder.setScriptLocation("classpath:import.sql");
feeder.setDataSource(dataSource);
return feeder;
}

@Bean(name = MESSAGE_SOURCE_BEAN_NAME)
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("messages");
return messageSource;
}

@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory().getObject());
}

@Configuration
@Profile("production")
public static class JndiConnections {

@Resource(mappedName = "jdbc/OurAppDataSource")
private DataSource jndiDataSource;
@Resource(mappedName = "OurAppHibernateDialect")
private String jndiHibernateDialect;

@Bean
public DataSource dataSource() {
return jndiDataSource;
}

@Bean
public String hibernateDialect() {
return jndiHibernateDialect;
}
}
}



@Configuration
@EnableWebMvc
@Profile("default")
public class SpringMvc extends WebMvcConfigurerAdapter {
@Autowired
private ApplicationInfoController applicationInfoController;
private List<HttpMessageConverter<?>> messageConverters;

@Override
public void configureViewControllers(ViewControllerConfigurer configurer) {
configurer.mapViewName("/", "welcome");
}

@Override
public void configureResourceHandling(ResourceConfigurer configurer) {
configurer.addResourceLocation("/images/").addPathMapping("/images/**");
configurer.addResourceLocation("/styles/").addPathMapping("/styles/**");
configurer.addResourceLocation("/scripts/").addPathMapping("/scripts/**");
}

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.addAll(getMessageConverters());
}

private List<HttpMessageConverter<?>> getMessageConverters() {
if (messageConverters == null) {
messageConverters = new ArrayList<HttpMessageConverter<?>>();

MappingJacksonHttpMessageConverter mappingJacksonHttpMessageConverter = new MappingJacksonHttpMessageConverter();
mappingJacksonHttpMessageConverter.setObjectMapper(objectMapper());
messageConverters.add(mappingJacksonHttpMessageConverter);
}
return messageConverters;
}

@Bean(autowire = Autowire.BY_TYPE)
public ObjectMapper objectMapper() {
return new JpaAwareObjectMapper();
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(pathEntityMethodArgumentResolver());
argumentResolvers.add(requestEntityMethodArgumentResolver());
argumentResolvers.add(validatingRequestBodyMethodArgumentResolver());
}

@Bean(autowire = Autowire.BY_TYPE)
public PathEntityMethodArgumentResolver pathEntityMethodArgumentResolver() {
return new PathEntityMethodArgumentResolver();
}

@Bean(autowire = Autowire.BY_TYPE)
public RequestEntityMethodArgumentResolver requestEntityMethodArgumentResolver() {
return new RequestEntityMethodArgumentResolver();
}

@Bean(autowire = Autowire.BY_TYPE)
public ValidatingRequestBodyMethodArgumentResolver validatingRequestBodyMethodArgumentResolver() {
return new ValidatingRequestBodyMethodArgumentResolver(getMessageConverters());
}

@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver internalResourceViewResolver = new InternalResourceViewResolver();
internalResourceViewResolver.setViewClass(JstlView.class);
internalResourceViewResolver.setPrefix("/WEB-INF/views/");
internalResourceViewResolver.setSuffix(".jsp");
internalResourceViewResolver.setExposedContextBeanNames(new String[]);
return internalResourceViewResolver;
}

@Bean
public Map<String, String> versionInfo() {
return applicationInfoController.getVersionInformation();
}
}


Our test configuration is part of our testing code:




@Configuration
@Profile("testing")
public class TestConfiguration {
@Bean
public DataSource dataSource() {
DriverManagerDataSource delegate = new DriverManagerDataSource();
delegate.setDriverClassName("org.hsqldb.jdbcDriver"); // As String, as the dependency is test (not compile).
delegate.setUrl("jdbc:hsqldb:mem:postduif;sql.enforce_strict_size=true");
delegate.setUsername("sa");
delegate.setPassword("");

We use Liquibase; see: http://blog.42.nl/articles/automate-liquibase-migrations
LiquibaseMigrator migrator = new LiquibaseMigrator();
migrator.setChangeLogPath("src/main/db/changelog.groovy");

MigratingDataSource dataSource = new MigratingDataSource();
dataSource.setMigrator(migrator);
dataSource.setDelegate(delegate);
return dataSource;
}


@Bean
public String hibernateDialect() {
// As String, as the dependency is runtime.
return "org.hibernate.dialect.HSQLDialect";
}

/**
* Mock beans for all services. The beans are marked to ensure they take precedence over the implementations for autowiring.
*/
@Configuration
@Profile("mockedServices")
public static class ServiceMocks {
@Bean
@Primary
public InvoiceService mockedInvoiceService() {
return createMock(InvoiceService.class);
}

@Bean
@Primary
public CustomerService mockedCustomerService() {
return createMock(CustomerService.class);
}

@Bean
@Primary
public ReportService mockedReportService() {
return createMock(ReportService.class);
}
}
}


Component testing




As in our previous post, the basic unit test class is simple. Note that two profiles remain unused: production and mockedServices.




@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes = )
@ActiveProfiles()
public abstract class SpringContextTestCase {
// Subclass for unit tests
}



The Spring application context in unit tests uses our in-memory database stub (with test data), which we rollback in all tests that use it using the <code@Transactional annotation.
The listener registered by SpringJUnit4ClassRunner by default does a rollback in unit tests by default. In code:




@Transactional
public class MyServiceTest extends SpringContextTestCase {

@Test
public void testSomething() {
// After the test, the database is rolled back to it's previous state.
}
}



Our controller tests uses mocked services. We do this by activating the mockedServices profile (note that by default the already active profiles are inherited):




@ActiveProfiles("mockedServices")
public class MyControllerTest extends SpringContextTestCase {
@Autowired
private MyService mockedService;
@Autowired
private MyController controllerToTest;

// ...
}


Code coverage & the production profile




During our unit tests, all beans will be created by Spring, so our code coverage for the Spring configuration is always 100% for the used profiles. Obviously we need to verify that our tests use the entire configuration, thus ensuring that all confirugation is tested. This leaves the production profile.




To test the production profile, please look at the code of the nested class JndiConnections. As you can see, it is trivial code, easily tested:




public class ConfigurationTest {
private ApplicationConfiguration.JndiConnections jndiConnections;

@Before
public void setup() {
jndiConnections = new ApplicationConfiguration.JndiConnections();
}

@Test
public void testBeans() {
DataSource dataSource = createMock(DataSource.class);
String dialect = "some string";

ReflectionUtil.setFieldValue(jndiConnections, "jndiDataSource", dataSource);
ReflectionUtil.setFieldValue(jndiConnections, "jndiHibernateDialect", dialect);

assertSame(dataSource, jndiConnections.dataSource());
assertEquals(string1, jndiConnections.hibernateDialect());
}


@Test
public void testJndiNames() {

assertJndiResource("jndiDataSource", "jdbc/OurAppDataSource");
assertJndiResource("jndiHibernateDialect", "OurAppHibernateDialect");
}


private void assertJndiResource(String fieldName, String mappedName) {

Field field = ReflectionUtils.findField(ApplicationConfiguration.JndiConnections.class, fieldName);
assertTrue(field.isAnnotationPresent(Resource.class));
assertEquals(mappedName, field.getAnnotation(Resource.class).mappedName());
}
}


Conclusion




Using a proof by example, this blog post demonstrates how we can use the @Profile annotation to use different (but related) Spring contexts for different tests.
Specifically, the example shown allows to test all data access and services using test data in an in-memory database, while selected components are still unit tested with mocked dependencies.
Noteworthy is that the Spring context provides the mocks, which is tricky when using an XML configuration (you’d need to create a MockFactory).



A next post will demonstrate how the spring-test-mvc project (now on GitHub, maybe part of Spring 3.2) can help us to make integration tests superfluous for REST webservices.

1 comment:

  1. Applicius11/7/13 03:48

    In the same goal of achieving effective persistence unit testing, we've developed the opensource Acolyte framework. Quite interested to have feedback about it: http://applicius-en.tumblr.com/post/55081625831/persistence-unit-test

    ReplyDelete