CDI et l’injection de mock pour les tests unitaires
L’injection de dépendances avec CDI me semble vraiment très intéressante en terme d’usabilité et de lisibilité du code.
La seule chose qui me dérange un peu est le manque de facilité à tester un objet utilisant l’injection de dépendances. Ou, plutôt, comment utiliser un framework de mock et injecter un mock sur lequel j’ai la main en test.
Je préviens d’avance. J’ai trouvé quelques d’articles qui gravitent autour de ce sujet et j’en ai fait un mix pour arriver à des solutions. Mais, à ce jour, je n’ai rien trouvé qui me semble simple et élégant à utiliser… (du moins, c’est perfectible)
Voyons voir sur un exemple simple comment procéder pour arriver à utiliser un framework tel que Mockito.
Tout d’abord, voilà mes classes et l’interface côté implémentation.
L’interface représentant le service que je vais vouloir mocker par la suite.
public interface MyService {
String foo();
}
Son implémentation par défaut :
public class DefaultMyService implements MyService {
public String foo() {
return "DefaultMyService.foo()";
}
}
La classe dans laquelle le service est injecté par constructeur :
public class MyApplication {
private MyService myService;
@Inject public MyApplication(MyService myService) {
this.myService = myService;
}
public void run() {
System.out.println("myService.foo() : " + myService.foo());
}
}
Voilà le test le plus basic que vous puissiez faire :
public class MyApplicationTest {
@Test public void shouldAppWorkWithMocks() {
WeldContainer weld = new Weld().initialize();
MyApplication app = weld.instance().select(MyApplication.class).get();
app.run();
}
}
Il n’y a pas à dire, les 2 premières lignes du test ne sont pas très élégantes… Imaginez que je les répète… (copier/coller : booouuuhh, c’est mal ! il y en a qui ont perdu des doigts comme ça ! ) Non, je déconne, pas de copier/coller, c’est le mal !
De plus, si j’avais dupliqué ce test, vous auriez eu une initialisation du container à chaque test. Il doit y avoir moyen de faire mieux, non ?
En cherchant un peu à résoudre ces soucis, je suis tombé sur ce très bon article du blog d’Alexis Hassler. Il utilise un Runner JUnit qui lui permet d’initialiser le container avec la classe à tester (il a aussi développé une Rule JUnit si vous êtes contraint d’utiliser un Runner particulier). Voilà à quoi ressemble son Runner :
public class WeldRunner extends BlockJUnit4ClassRunner {
private Weld weld;
private WeldContainer container;
public WeldRunner(Class klass) throws InitializationError {
super(klass);
}
@Override public void run(RunNotifier notifier) {
initializeWeld();
super.run(notifier);
shutdownWeld();
}
@Override protected Object createTest() throws Exception {
return container.instance().select(getTestClass().getJavaClass()).get();
}
private void initializeWeld() {
weld = new Weld();
container = weld.initialize();
}
private void shutdownWeld() {
weld.shutdown();
}
}
Y’a pas à dire, c’est simple, c’est beau !
Utilisons donc sont Runner. La classe de test devient alors :
@RunWith(WeldRunner.class)
public class MyApplicationRunnerTest {
@Inject private MyApplication app;
@Test public void shouldAppWorkWithMocks() {
app.run();
}
}
C’est déjà beaucoup plus lisible et améliore clairement l’usabilité.
Seule chose gênante, je n’ai pas la main sur l’instance de MyService injectée sur MyApplication. Donc, si je veux isoler MyApplication pour faire un test unitaire, je dois bouchonner l’instance injectée. Alors comment faire ?
1ère solution : La solution la plus simple serait d’avoir un setter pour injecter l’instance de MyService. Or, ça ne me plaît pas de retomber dans les travers (de porc) d’autres framework d’injection ou vous êtes forcé d’ouvrir votre code pour qu’il fonctionne. CDI permet de faire de l’injection sans cette “contrainte” au niveau du design du code donc pourquoi créer un setter. J’avoue que cette solution ne me plaît pas trop…
2ème solution : utiliser une alternative à l’implémentation par défaut de MyService en créant manuellement un mock qui sera alors injecté pour chacun de vos cas de test. Cela peut être intéressant mais c’est assez contraignant tout de même car vous allez sans doute devoir tester différents comportements de votre mock. Voyons voir comment cela se fait.
Tout d’abord, je vais créer une annotation pour annoter mes bouchons manuels que je souhaite injecter dans le container.
@Alternative
@Stereotype
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface MockAlternative {
}
Dans le scope de test, voilà le contenu du fichier META-INF/beans.xml (ou WEB-INF/beans.xml) pour que CDI prenne en compte cette Alternative :
<beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
<alternatives>
<stereotype>fr.stateofmind.cdi.test.annotation.MockAlternative</stereotype>
</alternatives>
</beans>
Définissez alors votre mock décoré de l’Alternative :
@MockAlternative
public class MyServiceMock implements MyService {
public String foo() {
return "MyServiceMock.foo()";
}
}
Et voilà, si vous rejouez le test, c’est bien le mock qui a été injecté.
3ème solution : c’est la solution qui me semble la plus pratique au jour le jour si on ne souhaite pas introduire de setter. Néanmoins, je ne la trouve pas super jolie, un peu trop verbeuse. Cette solution est en fait un complément de la solution précédente qui consiste à aller plus loin en injectant une instance mockée dans le container et de garder la main sur cette instance mockée. Avec cette solution, vous pourrez alors utiliser votre framework de mock favori. Ici, je compte utiliser Mockito.
@RunWith(WeldRunner.class)
public class MyApplicationRunnerTest {
@Inject private MyApplication app;
@Produces @MockAlternative private MyService myMockService = mock(MyService.class);
@Before public void setUp() throws Exception {
when(myMockService.foo()).thenReturn("My Mock !!!!");
}
@Test public void shouldAppWorkWithMocks() {
app.run();
}
}
Voilà ! Maintenant, vous avez la main sur le mock et vous pouvez vraiment faire ce que vous voulez dans votre test en gérant cas nominaux et cas limites.
J’espère que ce petit article vous aura éclairé sur l’injection avec CDI dans le contexte des tests unitaires.
Je ne sais pas vous mais j’aime vraiment bien la dernière solution. Mais j’aurais préféré une écriture plus compacte en utilisant une simple annotation magique sur l’attribut myMockService sans faire appel manuellement à Mockito. Par contre, je n’ai pas trouvé comment faire. *Si quelqu’un sait comment faire, je suis preneur !!!*

