Test d’une classe abstraite avec JUnit

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.

Donclorsque 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.