Les tests unitaires
Egalement appelés « TU », ils sont destinés à tester une unité du logiciel. Afin d’être vraiment unitaires, ils doivent être en totale isolation pour ne tester qu’une classe et qu’une méthode à la fois.
Tester le bon comportement de son application c’est bien, détecter les éventuelles régressions c’est encore mieux ! Et c’est là que réside tout l’intérêt d’écrire des TU.
De la théorie… à la bonne pratique
Un test unitaire doit être véritablement unitaire. Pour cela, les appels aux services, aux bases de données et fichiers doivent être évités. Le TU doit s’exécuter le plus rapidement possible afin d’avoir un retour quasi immédiat.
Un TU faisant partie intégrante du code applicatif, les pratiques suivantes sont recommandées :
- il doit respecter les conventions de code
- il doit être simple et lisible,
- il ne doit tester qu’un seul comportement à la fois
- il doit faire le moins d’appel possible aux dépendances
- l’utilisation de mocks est recommandée pour fiabiliser les TU.
Un mock est un objet qui permet de simuler un objet réel tel que la base de données, un web service…
L’approche TDD (Test Driven Development) reste la meilleure solution pour éviter que les tests soient écrits à la fin du développement de l’application.
Frameworks de mock
L’écriture d’objets de type mock peut s’avérer longue et fastidieuse, les objets ainsi codés peuvent contenir des bugs comme n’importe quelle portion du code. Des frameworks ont donc été conçus pour rendre la création de ces objets fiable et rapide.
La plupart des frameworks de mock permettent de spécifier le comportement que doit avoir l’objet mocké avec :
- les méthodes invoquées : paramètres d’appel et valeurs de retour,
- l’ordre d’invocation de ces méthodes,
- le nombre d’invocations de ces méthodes.
Les frameworks de mock permettent de créer dynamiquement des objets généralement à partir d’interfaces ou de classes. Ils proposent fréquemment des fonctionnalités très utiles au-delà de la simple simulation d’une valeur de retour comme :
- la simulation de cas d’erreurs en levant des exceptions,
- la validation des appels de méthodes,
- la validation de l’ordre de ces appels,
- la validation des appels avec un timeout.
Plusieurs frameworks de mock existent en Java, notamment :
- EasyMock,
- JMockIt,
- Mockito,
- JMock,
- MockRunner.
Dans ce qui suit, nous allons détailler le framework Mockito.
Mockito
C’est un framework Java très connu permettant de générer automatiquement des objets ‘mockés‘. Couplé avec JUnit, il permet de tester le comportement des objets réels associés à un ou des objets ‘mockés’ facilitant ainsi l’écriture des tests unitaires.
Configuration du projet
Pour intégrer Mockito à son projet, il suffit simplement de rajouter la dépendance Maven :
<dependency>
<groupid>org.mockito</groupid>
<artifactid>mockito-all</artifactid>
<version>1.9.5</version>
</dependency>
Deux manières sont possibles pour intégrer Mockito dans les tests Junit :
1- Ajouter l’annotation @RunWith (MockitoJunitRunner.class) à la classe de test :
@RunWith(MockitoJunitRunner.class)
public class MyTestClass {
}
2- Faire appel à la méthode initMocks dans la méthode de SetUp :
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
Création d’objets mockés avec @Mock
La création d’objets mockés se fait soit en appelant la méthode mock(), soit en rajoutant l’annotation @Mock pour les instances de classes.
User user = Mockito.mock(User.class);
ou
@Mock
User user;
Mockito encapsule et contrôle tous les appels effectués sur l’objet User. Ainsi user.getLogin() retournera tout le temps null si on ne « stubb » pas la méthode getLogin().
Définition du comportement des objets mockés ou « Stubbing »
Le stubbing permet de définir le comportement des objets mockés face aux appels de méthodes sur ces objets. Plusieurs méthodes de stubbing sont possibles :
- Retour d’une valeur unique
Mockito.when(user.getLogin()).thenReturn(‘user1’); //la chaine de caractères user1 sera renvoyée quand la méthode getLogin() sera appelée.
- Faire appel à la méthode d’origine
Mockito.when(user.getLogin()).thenCallRealMethod();
- Levée d’exceptions
Mockito.when(user.getLogin()).thenThrow(new RuntimeException());
Il faut noter que la méthode retournera toujours la valeur stubbée, peu importe combien de fois elle est appelée . Si on stubb la même méthode ayant la même signature plusieurs fois, le dernier stubbing sera pris en compte.
Mockito.when(user.getLogin()).ThenThrow(new RuntimeException()).ThenReturn(« foo »);
Ici le premier appel va lever une exception, tous les appels qui suivront retourneront « foo ».
- Retours de valeurs consécutives
Mockito.when(user.getLogin()).thenReturn(‘user1’,’user2’,’user3’);
Le premier appel retourne user1, le deuxième retournera user2 le troisième user3. Tous les appels qui suivent retourneront la dernière valeur c’est à dire user3.
- Ne rien retourner
Mockito.doNothing().when(user.getLogin());
Espionner un objet avec @Spy
Au lieu d’utiliser l’annotation @Mock, nous pouvons utiliser l’annotation @Spy. La différence entre les deux réside dans le fait que la deuxième permet d’instancier l’objet mocké, ce qui peut être très utile quand nous souhaitons mocker une classe et non pas une interface.
Une autre différence est à signaler est le stubbing. Si nous ne redéfinissons pas le comportement des méthodes de l’objet espionné, les méthodes réelles seront appelées, alors qu’avec @Mock, nous sommes obligés de spécifier leurs comportements, sinon la valeur nulle est retournée par défaut. Dans l’exemple qui suit la méthode getLogin() sera appelée.
@Spy
User user = new User(‘user1’);
user.getLogin() // retourne user1
Vérification d’interactions @Verify
@Verify permet de vérifier qu’une méthode a été bien appelée et que que les interactions avec le mock sont celles attendues.
Nous pouvons également vérifier le nombre total de fois ou la méthode a été appelée (atMost(3),atLeastOnce(),never(),times(5)) , l’ordre des invocations des objets mockés en utilisant inOrder() et aussi vérifier si une méthode a été invoquée avant la fin d’un timeout. Nous pouvons également ignorer les appels stubbés en utilisant ignoreStubs() et aussi vérifier qu’il n y’a pas eu d’interaction en utilisant verifyNoMoreInvocations().
verify(user).getLogin();
//le test passes si getLogin() est appelée avant la fin du timeout
verify(mock, timeout(100)).getLogin();
Injection
Mockito permet également d’injecter des resources (classes nécessaires au fonctionnement de l’objet mocké), en utilisant l’annotation @InjectMock. L’injection est faite soit en faisant appel au constructeur, soit en faisant appel au ‘setter’ ou bien en utilisant l’injection de propriétés.
public Class DocumentManagerBeanTest{
@Mock EntityManager em;
@Mock UserManager userManager;
@Spy RoleProvider role = new RoleProvider();
@InjectMocks DocumentManagerBean docBean;
@Before public void initMocks() {
MockitoAnnotations.initMocks(this);
}
@Test
public void uploadDocument(){
docBean.uploadDoc(file);
}
}
public Class DocumentManagerBean {
private EntityManager em;
UserManager user;
RoleProvider role
public String uploadDoc(String file){
if (user.hasAcess()){
em.checkFileExists(file);
….
}
}
}
Conclusion
Mockito est un framework de mock qui, associé à Junit, permet :
- une écriture rapide de tests unitaires,
- de se focaliser sur le comportement de la méthode à tester en mockant les connexions aux bases de données, les appels aux web services …
Cependant il a certaines limitations, en effet , il ne permet pas de mocker :
- les classes finales,
- les enums,
- less méthodes final,
- les méthodes static,
- les méthodes privées,
- les méthodes hashCode() et equals().
Aussi, les objets ‘mockés’ doivent être maintenus au fur et à mesure des évolutions du code à tester.