EasyMock et Autowiring

27 July 2011, by Baptiste Autin

Spring Test, JUnit and EasyMock are a good team for unit testing.
Unfortunately, classes that inject their properties by autowiring can be difficult to test when these properties have to be mocked (DAO for example).

Normally, with Spring, you inject your mocks thanks to the attribute factory-method :

<bean class="org.easymock.EasyMock" factory-method="createMock" id="ClientDAO">
	<constructor-arg value="com.example.dao.IClientDAO" />
</bean>

Now, let’s say that the class to be tested injects its DAO like this:

@Autowired
protected IClientDAO cDao;

The context startup is then likely to fail:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [com.example.dao.IClientDAO] is defined: Unsatisfied dependency of type [interface com.example.dao.IClientDAO]: expected at least 1 matching bean

Why? Because Spring determines the type of a factory-method-defined bean by reflection. So if the method returns an Object or a parameterized type, just like EasyMock.createMock() does, autowiring by type will not work (even if you specify the bean’s name with @Qualifier).

Solutions :

  • Utiliser l’annotation @Resource
    @Resource(name="ClientDAO")
    protected IClientDAO cDao;

    But you may not want to use that annotation, or make an explicit reference to the name of a bean.

  • Use an adapter class, that will explicitely specify the good return type (here, IClientDAO) :
    public class MocksFactory {
    
    	public IClientDAO getClientDAO() {
    		return EasyMock.createMock(IClientDAO.class);
    	}
    }

    Unfortunately, you will have to write as many methods as there are objects to mock, which might be tedious.

    If you are not satisfied with any of these two solutions, there is a last possibilty: defining an unmocked version of your class (so that autowiring can work), and then relying on the interface BeanFactoryPostProcessor to replace this unmocked bean, in Spring’s registry, by a mocked version.
    Just two classes are required for that:
    – a class MocksFactory to generate the mocked object (thanks to FactoryBean)
    – a class MocksFactoryPostProcessor which will receive a list of beans names to redefine.

import org.easymock.classextension.EasyMock;
import org.springframework.beans.factory.FactoryBean;

public class MocksFactory implements FactoryBean {

	private Class type;

	public void setType(final Class type) {
		this.type = type;
	}

	@Override
	public Object getObject() throws Exception {
		return EasyMock.createMock(type);
	}

	@Override
	public Class getObjectType() {
		return type;
	}

	@Override
	public boolean isSingleton() {
		return true;
	}
}
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;

public class MocksFactoryPostProcessor implements BeanFactoryPostProcessor {

	private static final Class factoryClass = MocksFactory.class;
	
	private String[] beanNames;

	@Override
	public void postProcessBeanFactory(final ConfigurableListableBeanFactory context) throws BeansException {
		
		BeanDefinitionRegistry registry = (BeanDefinitionRegistry) context;
		
		for (String beanName : beanNames) {
			
			BeanDefinition bd = registry.getBeanDefinition(beanName);
											
			MutablePropertyValues values = new MutablePropertyValues();
			values.addPropertyValue(new PropertyValue("type", bd.getBeanClassName()));
			
			RootBeanDefinition definition = new RootBeanDefinition(factoryClass, values);
			registry.registerBeanDefinition(beanName, definition);
		}
	}
	
	public void setBeanNames(String[] beans) {
		this.beanNames = beans;
	}
}

Finally, in the applicationContext.xml, the comma-separated list of beans names is set through the property beanNames :

<bean id="ClientDAO" class="com.example.dao.ClientDAO"/>
<bean id="mocksFactoryPostProcessor" class="com.example.MocksFactoryPostProcessor">
	<property name="beanNames" value="ClientDAO,ProductDAO,ContractDAO"/>
</bean>

Note that this solution requires that you make use of classextension.EasyMock (to mock by concrete class, and not by interface).

Remarque : instead of defining all the bean names, like we are doing, we could browse Spring’s registry and redefine all those that are injected with @Autowired

Laisser une réponse

«     »