101 votes

Se moquant de l'énumération Java pour ajouter une valeur pour tester le cas d'échec.

J'ai un enum switch plus ou moins comme ceci:

public static enum MyEnum {A, B}

public int foo(MyEnum value) {
    switch(value) {
        case(A): return calculateSomething();
        case(B): return calculateSomethingElse();
    }
    throw new IllegalArgumentException("Ne sais pas comment gérer " + value);
}

et j'aimerais que toutes les lignes soient couvertes par les tests, mais comme le code est censé traiter toutes les possibilités, je ne peux pas fournir une valeur sans son instruction de cas correspondante dans le switch.

Étendre l'enum pour ajouter une valeur supplémentaire n'est pas possible, et simplement simuler la méthode equals pour renvoyer false ne fonctionnera pas non plus car le bytecode généré utilise une table de saut en coulisses pour accéder au bon cas... J'ai donc pensé que peut-être une sorte de magie noire pourrait être réalisée avec PowerMock ou quelque chose.

Merci!

edit:

Comme je possède l'énumération, j'ai pensé que je pourrais simplement ajouter une méthode aux valeurs et ainsi éviter complètement le problème du switch; mais je laisse la question telle quelle car elle reste intéressante.

71voto

Jonny Heggheim Points 538

Voici un exemple complet.

Le code est presque identique à votre original (juste simplifié avec une meilleure validation des tests) :

public enum MyEnum {A, B}

public class Bar {

    public int foo(MyEnum value) {
        switch (value) {
            case A: return 1;
            case B: return 2;
        }
        throw new IllegalArgumentException("Ne sais pas comment gérer " + value);
    }
}

Et voici le test unitaire avec une couverture de code complète, le test fonctionne avec Powermock (1.4.10), Mockito (1.8.5) et JUnit (4.8.2) :

@RunWith(PowerMockRunner.class)
public class BarTest {

    private Bar bar;

    @Before
    public void createBar() {
        bar = new Bar();
    }

    @Test(expected = IllegalArgumentException.class)
    @PrepareForTest(MyEnum.class)
    public void unknownValueShouldThrowException() throws Exception {
        MyEnum C = mock(MyEnum.class);
        when(C.ordinal()).thenReturn(2);

        PowerMockito.mockStatic(MyEnum.class);
        PowerMockito.when(MyEnum.values()).thenReturn(new MyEnum[]{MyEnum.A, MyEnum.B, C});

        bar.foo(C);
    }

    @Test
    public void AShouldReturn1() {
        assertEquals(1, bar.foo(MyEnum.A));
    }

    @Test
    public void BShouldReturn2() {
        assertEquals(2, bar.foo(MyEnum.B));
    }
}

Résultat :

Tests exécutés : 3, Échecs : 0, Erreurs : 0, Ignorés : 0, Temps écoulé : 0.628 sec

27voto

Frank S. Points 300

Si vous pouvez utiliser Maven comme votre système de construction, vous pouvez adopter une approche beaucoup plus simple. Il vous suffit de définir le même enum avec une constante supplémentaire dans le chemin de classe de votre classe de test.

Disons que vous avez votre enum déclaré sous le répertoire des sources (src/main/java) comme ceci :

package my.package;

public enum MyEnum {
    A,
    B
}

Ensuite, vous déclarez exactement le même enum dans le répertoire des sources de test (src/test/java) comme ceci :

package my.package

public enum MyEnum {
    A,
    B,
    C
}

Les tests voient le chemin de classe de test avec l'enum "surchargé" et vous pouvez tester votre code avec la constante "C" de l'enum. Vous devriez alors voir votre IllegalArgumentException.

Testé sous windows avec maven 3.5.2, AdoptOpenJDK 11.0.3 et IntelliJ IDEA 2019.3.1

8voto

Nantoka Points 358

Voici ma version de la solution de @Jonny Heggheim en utilisant uniquement Mockito. Elle a été testée avec Mockito 3.9.0 et Java 11 :

public class MyTestClass {

  private static MockedStatic myMockedEnum;
  private static MyEnum mockedValue;

  @BeforeClass
  public void setUp() {
    MyEnum[] newEnumValues = addNewEnumValue(MyEnum.class);
    myMockedEnum = mockStatic(MyEnum.class);
    myMockedEnum.when(MyEnum::values).thenReturn(newEnumValues);
    mockedValue = newEnumValues[newEnumValues.length - 1];
  }

  @AfterClass
  public void tearDown(){
    myMockedEnum.close();
  }

  @Test
  public void testCase(){
    // Utilisez mockedValue dans votre cas de test
    ...
  }

  private static > E[] addNewEnumValue(Class enumClazz){
    EnumSet enumSet = EnumSet.allOf(enumClazz);
    E[] newValues = (E[]) Array.newInstance(enumClazz, enumSet.size() + 1);
    int i = 0;
    for (E value : enumSet) {
      newValues[i] = value;
      i++;
    }

    E newEnumValue = mock(enumClazz);
    newValues[newValues.length - 1] = newEnumValue;

    when(newEnumValue.ordinal()).thenReturn(newValues.length - 1);

    return newValues;
  }
}

Quelques mots de prudence lors de l'utilisation de ceci :

  • Il est crucial d'exécuter le code dans la méthode setup() avant que toute classe ne soit chargée par le chargeur de classes JVM contenant une instruction switch pour l'Enum mocké. Je vous recommande de lire l'article cité dans la réponse de @Vampire si vous voulez savoir pourquoi.
  • La manière la plus sûre de réaliser cela est de placer le code dans une méthode statique annotée avec @BeforeClass.
  • Si vous oubliez le code dans la méthode tearDown(), il peut arriver que les tests de la classe de test réussissent mais que les tests dans d'autres classes de test échouent lorsqu'ils sont exécutés ultérieurement dans le même test. Cela est dû au fait que MyEnum reste étendu jusqu'à ce que vous appeliez close() sur MockedStatic.
  • Si dans la même classe de test certains de vos cas de test utilisent l'Enum mocké et d'autres non et que vous devez regrouper le code setUp() et tearDown() en un seul cas de test, je vous recommande vivement d'exécuter les tests avec le runner Robolectric ou tout autre runner de tests, qui garantit que chaque cas de test s'exécute dans une JVM fraîchement démarrée. De cette manière, vous pouvez vous assurer que toutes les classes contenant des instructions switch pour l'Enum sont nouvellement chargées par le chargeur de classes pour chaque cas de test.

2voto

Rogério Points 5460

Au lieu d'utiliser une manipulation radicale des octets pour permettre à un test de toucher la dernière ligne de foo, je la supprimerais et je m'appuierais plutôt sur une analyse statique du code. Par exemple, IntelliJ IDEA a l'inspection de code "Déclaration switch d'énumération manquant de cas", qui produirait un avertissement pour la méthode foo si elle manquait d'un case.

2voto

bert bruynooghe Points 660

Comme vous l'avez indiqué dans votre édition, vous pouvez ajouter la fonctionnalité dans l'enum lui-même. Cependant, ce n'est peut-être pas la meilleure option, car cela peut violer le principe de "Une Responsabilité". Une autre façon d'atteindre cela est de créer une carte statique qui contient les valeurs d'énumération comme clé et la fonctionnalité comme valeur. De cette façon, vous pouvez facilement tester si pour n'importe quelle valeur d'énumération vous avez un comportement valide en bouclant sur toutes les valeurs. Cela peut sembler un peu tiré par les cheveux dans cet exemple, mais c'est une technique que j'utilise souvent pour mapper les ids de ressources aux valeurs d'enum.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X