Test d'une classe abstraite avec JUnit
1. Vue d'ensemble
Dans ce didacticiel, nous analyserons divers cas d'utilisation et des solutions alternatives possibles aux tests unitaires de classes abstraites avec des méthodes non abstraites.
Notez quetesting abstract classes should almost always go through the public API of the concrete implementations, n'appliquez donc pas les techniques ci-dessous à moins d'être sûr de ce que vous faites.
2. Dépendances Maven
Commençons par les dépendances Maven:
org.junit.jupiter
junit-jupiter-engine
5.1.0
test
org.mockito
mockito-core
2.8.9
test
org.powermock
powermock-module-junit4
1.7.4
test
junit
junit
org.powermock
powermock-api-mockito2
1.7.4
test
Vous pouvez trouver les dernières versions de ces bibliothèques surMaven Central.
Powermock n'est pas entièrement pris en charge pour Junit5. De plus,powermock-module-junit4 n'est utilisé que pour un exemple présenté dans la section 5.
3. Méthode indépendante non abstraite
Prenons un cas où nous avons une classe abstraite avec une méthode publique non abstraite:
public abstract class AbstractIndependent {
public abstract int abstractFunc();
public String defaultImpl() {
return "DEFAULT-1";
}
}
Nous voulons tester la méthodedefaultImpl(), et nous avons deux solutions possibles - en utilisant une classe concrète, ou en utilisant Mockito.
3.1. Utiliser une classe concrète
Créez une classe concrète qui étendAbstractIndependent class et utilisez-la pour tester la méthode:
public class ConcreteImpl extends AbstractIndependent {
@Override
public int abstractFunc() {
return 4;
}
}
@Test
public void givenNonAbstractMethod_whenConcreteImpl_testCorrectBehaviour() {
ConcreteImpl conClass = new ConcreteImpl();
String actual = conClass.defaultImpl();
assertEquals("DEFAULT-1", actual);
}
L'inconvénient de cette solution est la nécessité de créer la classe concrète avec des implémentations factices de toutes les méthodes abstraites.
3.2. Utiliser Mockito
Alternativement, nous pouvons utiliserMockito pour créer une maquette:
@Test
public void givenNonAbstractMethod_whenMockitoMock_testCorrectBehaviour() {
AbstractIndependent absCls = Mockito.mock(
AbstractIndependent.class,
Mockito.CALLS_REAL_METHODS);
assertEquals("DEFAULT-1", absCls.defaultImpl());
}
La partie la plus importante ici est lepreparation of the mock to use the real code when a method is invoked utilisantMockito.CALLS_REAL_METHODS.
4. Méthode abstraite appelée à partir d'une méthode non abstraite
Dans ce cas, la méthode non abstraite définit le flux d'exécution global, tandis que la méthode abstraite peut être écrite de différentes manières en fonction du cas d'utilisation:
public abstract class AbstractMethodCalling {
public abstract String abstractFunc();
public String defaultImpl() {
String res = abstractFunc();
return (res == null) ? "Default" : (res + " Default");
}
}
Pour tester ce code, nous pouvons utiliser les deux mêmes approches qu'auparavant: créer une classe concrète ou utiliser Mockito pour créer une maquette:
@Test
public void givenDefaultImpl_whenMockAbstractFunc_thenExpectedBehaviour() {
AbstractMethodCalling cls = Mockito.mock(AbstractMethodCalling.class);
Mockito.when(cls.abstractFunc())
.thenReturn("Abstract");
Mockito.doCallRealMethod()
.when(cls)
.defaultImpl();
assertEquals("Abstract Default", cls.defaultImpl());
}
Ici, leabstractFunc() est stubbed avec la valeur de retour que nous préférons pour le test. Cela signifie que lorsque nous appelons la méthode non abstraitedefaultImpl(), elle utilisera ce stub.
5. Méthode non abstraite avec obstruction au test
Dans certains scénarios, la méthode que nous voulons tester appelle une méthode privée qui contient une obstruction de test.
Nous devons contourner la méthode de test obstruant avant de tester la méthode cible:
public abstract class AbstractPrivateMethods {
public abstract int abstractFunc();
public String defaultImpl() {
return getCurrentDateTime() + "DEFAULT-1";
}
private String getCurrentDateTime() {
return LocalDateTime.now().toString();
}
}
Dans cet exemple, la méthodedefaultImpl() appelle la méthode privéegetCurrentDateTime(). Cette méthode privée obtient l'heure actuelle au moment de l'exécution, ce qui devrait être évité dans nos tests unitaires.
Maintenant, pour nous moquer du comportement standard de cette méthode privée, nous ne pouvons même pas utiliserMockito car il ne peut pas contrôler les méthodes privées.
À la place, nous devons utiliserPowerMock (note that this example works only with JUnit 4 because support for this dependency isn’t available for JUnit 5):
@RunWith(PowerMockRunner.class)
@PrepareForTest(AbstractPrivateMethods.class)
public class AbstractPrivateMethodsUnitTest {
@Test
public void whenMockPrivateMethod_thenVerifyBehaviour() {
AbstractPrivateMethods mockClass = PowerMockito.mock(AbstractPrivateMethods.class);
PowerMockito.doCallRealMethod()
.when(mockClass)
.defaultImpl();
String dateTime = LocalDateTime.now().toString();
PowerMockito.doReturn(dateTime).when(mockClass, "getCurrentDateTime");
String actual = mockClass.defaultImpl();
assertEquals(dateTime + "DEFAULT-1", actual);
}
}
Bits importants dans cet exemple:
-
@RunWith définit PowerMock comme runner pour le test
-
@PrepareForTest(class) demande à PowerMock de préparer la classe pour un traitement ultérieur
Fait intéressant, nous demandons àPowerMock de stuber la méthode privéegetCurrentDateTime(). PowerMock utilisera la réflexion pour la trouver car elle n'est pas accessible de l'extérieur.
Donc, lorsque nous appelonsdefaultImpl(), le stub créé pour une méthode privée sera appelé à la place de la méthode réelle.
6. Méthode non abstraite qui accède aux champs d'instance
Les classes abstraites peuvent avoir un état interne implémenté avec des champs de classe. La valeur des champs pourrait avoir un effet significatif sur la méthode à tester.
Si un champ est public ou protégé, nous pouvons facilement y accéder à partir de la méthode de test.
Mais si c'est privé, nous devons utiliserPowerMockito:
public abstract class AbstractInstanceFields {
protected int count;
private boolean active = false;
public abstract int abstractFunc();
public String testFunc() {
if (count > 5) {
return "Overflow";
}
return active ? "Added" : "Blocked";
}
}
Ici, la méthodetestFunc() utilise les champs au niveau de l'instancecount etactive avant son retour.
Lors du test detestFunc(), nous pouvons changer la valeur du champcount en accédant à l'instance créée à l'aide deMockito.
Par contre, pour tester le comportement avec le champ privéactive, il faudra à nouveau utiliserPowerMockito, et sa classeWhitebox:
@Test
public void whenPowerMockitoAndActiveFieldTrue_thenCorrectBehaviour() {
AbstractInstanceFields instClass = PowerMockito.mock(AbstractInstanceFields.class);
PowerMockito.doCallRealMethod()
.when(instClass)
.testFunc();
Whitebox.setInternalState(instClass, "active", true);
assertEquals("Added", instClass.testFunc());
}
Nous créons une classe stub à l'aide dePowerMockito.mock() et nous utilisons la classeWhitebox pour contrôler l'état interne de l'objet.
La valeur du champactive est remplacée partrue.
7. Conclusion
Dans ce didacticiel, nous avons vu plusieurs exemples qui couvrent de nombreux cas d'utilisation. Nous pouvons utiliser des classes abstraites dans beaucoup plus de scénarios en fonction de la conception suivie.
L'écriture de tests unitaires pour les méthodes de classes abstraites est aussi importante que pour les classes et les méthodes normales. Nous pouvons tester chacune d’elles en utilisant différentes techniques ou différentes bibliothèques de support de test disponibles.
Le code source complet est disponibleover on GitHub.