Sharing Our Passion for Technology
& Continuous Learning
Testing Spring Wiring
Overview
Spring is an essential part of my technology stack. I can't imagine providing quality software that doesn't leverage an IoC container. However, decoupling components requires some amount of configuration. Whether this is accomplished through annotations or XML, it's fairly easy to mess up. Fixing these missing or incorrect configurations doesn't take very long. The real question is how quickly can you identify these errors? This question of how long, is a feedback loop question. Unfortunately many teams wait until they fire up the application server to see if their Spring context is wired correctly. This is too late.One of our partners suffered from this very issue. Due to environmental constraints they could not run automated, in-container tests that would have identified misconfigured beans. After repeatedly committing stupid configuration mistakes, I decided that I would write a Spring wiring test. As I began to write this I encountered five problems.
Problem 1: Scope
The first problem that I encountered was that not all of my beans were being created when I loaded my context.@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class SpringWiringTest{
@Test
public void testSomething(){
...
I quickly remembered that only beans with the default 'singleton' scope will be loaded immediately. The rest of the bean scopes ('prototype', 'request', and 'session') will not be created until they are requested. In order to solve this I looped through all of the bean names and retrieved each bean from the context.
String[] names = context.getBeanDefinitionNames();
for(String name : names){
context.getBean(name);
}
However, when I ran this I got the following error
java.lang.IllegalStateException: No Scope registered for scope 'request'</pre>
In order to correct this issue I added the following setup method.
<pre lang="java">@Before
public void testCaseSetUp(){
context.getBeanFactory().registerScope("session", new SessionScope());
context.getBeanFactory().registerScope("request", new RequestScope());
MockHttpServletRequest request = new MockHttpServletRequest();
ServletRequestAttributes attributes =
new ServletRequestAttributes(request);
RequestContextHolder.setRequestAttributes(attributes);
}
I threw in the session scope too just to be safe.
Problem 2: Abstract Bean Definitions
The second problem that I had to solve was requesting abstract beans.org.springframework.beans.factory.BeanIsAbstractException: Error creating bean
with name 'testBean': Bean definition is abstract</pre>
In order to solve this issue I added a check to verify if a bean definition was abstract.
<pre lang="java">String[] names = context.getBeanDefinitionNames();
for(String name : names){
BeanDefinition beanDefinition = context.getBeanDefinition(name);
if(!beanDefinition.isAbstract()){
context.getBean(name);
}
}
Problem 3: Mock Beans
The third issue I found was environment specific beans. JNDI references are one type of environment specific bean.<jee:jndi-lookup id="testBean" jndi-name="jdbc/test"/>
In order to solve this issue, I decided to create a test bootstrap context that imported all of the production contexts. This provided a place for me to override beans that I wanted to create for the test. I used Mockito to create mock beans in most situations.
<bean id="testBean" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="com.something.Foo" />
</bean>
While this worked for most beans, I received errors when I wired up mock DataSources. Instead I used embedded databases to replace real DataSources.
<bean id="testDataSource"
class="org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactoryBean"
p:databaseType="H2"
abstract="true"/>
Problem 4: Components
This fourth issue was harder to identify but equally problematic. I frequently wired up beans with the @Component annotation and failed to configure an 'component-scan' for those packages.<context:component-scan base-package="com.sourceallies"/>
In other words I needed to reconcile the classes marked with the @Component annotation with the beans that were available in the Spring context.
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(true);
scanner.addIncludeFilter(new AnnotationTypeFilter(Component.class));
for (BeanDefinition bean : scanner.findCandidateComponents("com.something")){
components.add(bean.getBeanClassName());
}
This will create a list of components found on the classpath under the specified base package. Reconciling this list with the beans loaded in the Spring context will identify any components that are not configured in Spring. This list came in handy for two reasons. First it identified any components that I needed to configure in Spring. Second it listed components that were provided in dependent jars. This list helped us become proactive instead of reactive.
Problem 5: Duplicate Bean Definitions
The fifth issue is duplicate bean definitions. This can occur when more than one bean has the same bean definition name. This can also occur when the same context is loaded multiple times. By default Spring allows the new bean definition to overwrite the previous one. This sounds annoying but harmless. On the contrary, Spring does not guarantee the loading order. Furthermore, each deployment environment's classloader can load context files in a different order. The bottom line is that duplicate bean definitions can result in subtle and painful bugs.While this issue was clear to me the solution was a bit more illusive. At first I tried to set the allowBeanDefinitionOverriding flag on org.springframework.beans.factory.support.DefaultListableBeanFactory. This throws an exception if the same bean definition is loaded multiple times. Case closed,right?
Unfortunately the solution was not this simple. Problem 3, Mock Beans, requires the creation of a test bootstrap context that overrides bean definitions. So the allowBeanDefinitionOverriding flag is out. After crying a little, I started exploring Spring. Long story short, I overrode the 'protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)' method on org.springframework.context.support.ClassPathXmlApplicationContext and injected a delegating proxy.
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)
throws BeansException, IOException {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(DefaultListableBeanFactory.class);
BeanohBeanFactoryMethodInterceptor callback = new BeanohBeanFactoryMethodInterceptor(
beanFactory);
callbacks.add(callback);
enhancer.setCallback(callback);
DefaultListableBeanFactory proxy = (DefaultListableBeanFactory) enhancer
.create();
super.loadBeanDefinitions(proxy);
}
I used 'net.sf.cglib.proxy.Enhancer' and 'net.sf.cglib.proxy.MethodInterceptor' to create a proxy. The trick was to delegate to the real 'org.springframework.beans.factory.support.DefaultListableBeanFactory' instance.
@Override
public Object intercept(Object object, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
Method delegateMethod = delegateClass.getMethod(method.getName(),
method.getParameterTypes());
if ("registerBeanDefinition".equals(method.getName())) {
if (beanDefinitionMap.containsKey(args[0])) {
List definitions = beanDefinitionMap
.get(args[0]);
definitions.add((BeanDefinition) args[1]);
} else {
List beanDefinitions = new ArrayList();
beanDefinitions.add((BeanDefinition) args[1]);
beanDefinitionMap.put((String) args[0], beanDefinitions);
}
}
return delegateMethod.invoke(delegate, args);
}
This passively collects bean definitions that are registered with the registerBeanDefinition method. This method takes two arguments: a String bean name (arg[0]) and a 'org.springframework.beans.factory.config.BeanDefinition' (arg[1]). This map of bean definitions can be inspected later to determine if beans were loaded multiple times.
Conclusions
This experience has lead our team to develop a complete, open source solution called Beanoh (pronounced \'beanˌō\). Simply download the jar or build the project with maven and start using it.public class SomeTest extends BeanohTestCase {
@Test
public void testSomething() {
assertContextLoading();
}
}
For more information visit http://beanoh.org/overview.html.
In just a few short weeks this testing approach prevented our team from committing broken Spring configurations. Furthermore, we took time to evaluate the inherited components that we received through our project dependencies. This provided valuable insights into our dependency structure and our packaging strategy.
These wiring tests even gave us feedback as we refactored our spring contexts. We were able to simplify our context files without worrying that we would break something. We also identified unused and duplicate bean definitions. Without the quick feedback of this wiring test we would have committed broken changes that effected the whole team.
Automated tests help you manage code behavior rather than allowing code to drive your behavior. Download Beanoh today and take control of your Spring configuration.
For more information checkout our YouTube video tutorials.