EasyMock et Autowiring

Baptiste Autin, le 27 juillet 2011

Spring Test, JUnit et EasyMock forment un trio de choix pour réaliser des tests unitaires.
Malheureusement, tester des classes qui injectent leurs collaborateurs par autowiring peut poser quelques difficultés lorsque les collaborateurs doivent être mockés (cas des DAO par exemple).

Normalement, injecter des simulacres en environnement Spring passe par l’utilisation de l’attribut factory-method :

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

Supposons maintenant que la classe à tester injecte ainsi son DAO :

@Autowired
protected IClientDAO cDao;

Le démarrage du contexte risque alors d’échouer :

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

En effet, Spring détermine le type d’un bean défini via une factory-method par réflexion. Par conséquent si la méthode en question déclare retourner un type Object ou un type paramétré, comme c’est le cas de la méthode statique EasyMock.createMock(), l’autowiring par type ne fonctionnera pas (même accompagné d’un @Qualifier).

Solutions:

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

    Mais il n’est pas toujours possible/souhaitable d’utiliser cette annotation, ou de faire une référence explicite à un nom de bean.

  • Utiliser une classe adaptatrice, qui indiquera explicitement le bon type de retour (ici, IClientDAO) :
    public class MocksFactory {
    
    	public IClientDAO getClientDAO() {
    		return EasyMock.createMock(IClientDAO.class);
    	}
    }

    Malheureusement, il faudra définir autant de méthodes que d’objets à mocker, ce qui peut s’avérer fastidieux.

    Si aucune des deux précédentes solutions n’est envisageable, il reste une troisième possibilité : définir une version non-mockée du bean (ce qui permettra à l’autowiring de se faire correctement), et recourir ensuite à l’interface BeanFactoryPostProcessor pour remplacer dans le registre Spring ce bean non-mocké par la version mockée.
    Deux classes suffisent à faire cela :
    – une classe MocksFactory pour générer des mocks objects (grâce à l’interface FactoryBean)
    – une classe MocksFactoryPostProcessor à qui on transmet la liste des noms de beans devant être redéfinis

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;
	}
}

Enfin, dans l’applicationContext.xml, on passe la liste des noms de beans à redéfinir via la propriété beanNames, en les séparant par une virgule :

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

Notez que cette dernière solution impose le recours à classextension.EasyMock (qui permet de créer un mock object à partir d’une instance concrète, et non d’une interface).

Remarque : plutôt que de passer une liste exhaustive de beans comme nous le faisons, nous pourrions parcourir tous les beans du registre Spring et redéfinir tous ceux qui sont injectés par @Autowired

Laisser une réponse

«     »