Tester 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 que le test des classes abstraites devrait presque toujours passer par l'API publique des implémentations concrètes , donc n'appliquez pas les techniques ci-dessous à moins que vous ne soyez sûr de ce que vous faites.

2. Dépendances de 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 sur Maven 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 non abstraite indépendante

Considérons 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éthode defaultImpl () , et nous avons deux solutions possibles - en utilisant une classe concrète ou en utilisant Mockito.

3.1. Utiliser une classe de béton

Créez une classe concrète qui étend la classe AbstractIndependent 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 utiliser Mockito 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 la préparation de la simulation pour utiliser le code réel lorsqu'une méthode est appelée à l' aide de Mockito.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 selon le 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'avant - 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, abstractFunc () 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 abstraite defaultImpl () , 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 obstructive 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éthode defaultImpl () appelle la méthode privée getCurrentDateTime () . 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 se moquer du comportement standard de cette méthode privée, nous ne pouvons même pas utiliser Mockito car il ne peut pas contrôler les méthodes privées.

Nous avons plutôt besoin d'utiliser PowerMock ( n ote que cet exemple ne fonctionne qu'avec JUnit 4 parce que le soutien de cette dépendance ne sont pas disponibles pour 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 le coureur du test
  • @PrepareForTest (classe) indique à PowerMock de préparer la classe pour un traitement ultérieur

Fait intéressant, nous demandons à PowerMock de stuber la méthode privée getCurrentDateTime (). PowerMock utilisera la réflexion pour le trouver car il n'est pas accessible de l'extérieur.

Ainsi , lorsque nous appelons defaultImpl () , 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 testée.

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 utiliser PowerMockito :

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

Here, the testFunc() method is using instance-level fields count and active before it returns.

When testing testFunc(), we can change the value of the count field by accessing instance created using Mockito.

On the other hand, to test the behavior with the private active field, we'll again have to use PowerMockito, and its Whitebox class:

@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()); }

We're creating a stub class using PowerMockito.mock(), and we're using Whitebox class to control object's internal state.

The value of the active field is changed to true.

7. Conclusion

Dans ce didacticiel, nous avons vu plusieurs exemples qui couvrent de nombreux cas d'utilisation. Nous pouvons utiliser des classes abstraites dans de nombreux autres scénarios en fonction de la conception suivie.

En outre, l'écriture de tests unitaires pour les méthodes de classe abstraites est aussi importante que pour les classes et méthodes normales. Nous pouvons tester chacun d'eux en utilisant différentes techniques ou différentes bibliothèques de support de test disponibles.

Le code source complet est disponible à l'adresse over sur GitHub.