89 votes

Comment écrire des tests junit pour les interfaces ?

Quelle est la meilleure façon d'écrire des tests junit pour les interfaces afin qu'ils puissent être utilisés pour les classes d'implémentation concrètes ?

Par exemple, vous avez cette interface et des classes d'implémentation :

public interface MyInterface {
    /** Return the given value. */
    public boolean myMethod(boolean retVal);
}

public class MyClass1 implements MyInterface {
    public boolean myMethod(boolean retVal) {
        return retVal;
    }
}

public class MyClass2 implements MyInterface {
    public boolean myMethod(boolean retVal) {
        return retVal;
    }
}

Comment écrire un test contre l'interface afin de pouvoir l'utiliser pour la classe ?

Possibilité 1 :

public abstract class MyInterfaceTest {
    public abstract MyInterface createInstance();

    @Test
    public final void testMyMethod_True() {
        MyInterface instance = createInstance();
        assertTrue(instance.myMethod(true));
    }

    @Test
    public final void testMyMethod_False() {
        MyInterface instance = createInstance();
        assertFalse(instance.myMethod(false));
    }
}

public class MyClass1Test extends MyInterfaceTest {
    public MyInterface createInstance() {
        return new MyClass1();
    }
}

public class MyClass2Test extends MyInterfaceTest {
    public MyInterface createInstance() {
        return new MyClass2();
    }
}

Pro :

  • Une seule méthode doit être mise en œuvre

Con :

  • Les dépendances et les objets fantaisie de la classe à tester doivent être les mêmes pour tous les tests.

Possibilité 2 :

public abstract class MyInterfaceTest
    public void testMyMethod_True(MyInterface instance) {
        assertTrue(instance.myMethod(true));
    }

    public void testMyMethod_False(MyInterface instance) {
        assertFalse(instance.myMethod(false));
    }
}

public class MyClass1Test extends MyInterfaceTest {
    @Test
    public void testMyMethod_True() {
        MyClass1 instance = new MyClass1();
        super.testMyMethod_True(instance);
    }

    @Test
    public void testMyMethod_False() {
        MyClass1 instance = new MyClass1();
        super.testMyMethod_False(instance);
    }
}

public class MyClass2Test extends MyInterfaceTest {
    @Test
    public void testMyMethod_True() {
        MyClass1 instance = new MyClass2();
        super.testMyMethod_True(instance);
    }

    @Test
    public void testMyMethod_False() {
        MyClass1 instance = new MyClass2();
        super.testMyMethod_False(instance);
    }
}

Pro :

  • granularité fine pour chaque test, y compris les dépendances et les objets fantaisie.

Con :

  • Chaque classe de test implémentée nécessite d'écrire des méthodes de test supplémentaires.

Quelle possibilité préférez-vous ou quel autre moyen utilisez-vous ?

0 votes

La possibilité 1 n'est pas suffisante lorsque la classe concrète se trouve dans un paquet, un composant ou une équipe de développement différents.

1 votes

@AndyThomas : Pourquoi dites-vous cela ? J'utilise la possibilité 1 avec des classes concrètes (à la fois pour les implémentations et les tests) dans différents paquets et projets Maven.

1 votes

@TrevorRobinson - En repensant à ce commentaire vieux de trois ans, tout ce qui me vient à l'esprit pour le moment est que les classes hors de votre contrôle peuvent avoir de multiples constructeurs, mais la possibilité 1 exécute chaque test sur un objet créé uniquement avec l'un d'entre eux.

89voto

Ryan Stewart Points 46960

Contrairement à la réponse très votée de @dlev, il peut parfois être très utile/nécessaire d'écrire un test comme vous le suggérez. L'API publique d'une classe, telle qu'exprimée par son interface, est la chose la plus importante à tester. Ceci étant dit, je n'utiliserais aucune des approches que vous avez mentionnées, mais une Paramétré test à la place, où les paramètres sont les implémentations à tester :

@RunWith(Parameterized.class)
public class InterfaceTesting {
    public MyInterface myInterface;

    public InterfaceTesting(MyInterface myInterface) {
        this.myInterface = myInterface;
    }

    @Test
    public final void testMyMethod_True() {
        assertTrue(myInterface.myMethod(true));
    }

    @Test
    public final void testMyMethod_False() {
        assertFalse(myInterface.myMethod(false));
    }

    @Parameterized.Parameters
    public static Collection<Object[]> instancesToTest() {
        return Arrays.asList(
                    new Object[]{new MyClass1()},
                    new Object[]{new MyClass2()}
        );
    }
}

4 votes

Il semble y avoir un problème avec cette approche. La même instance de MyClass1 et MyClass2 est utilisée pour exécuter toutes les méthodes de test. Idéalement, chaque méthode de test devrait être exécutée avec une nouvelle instance de MyClass1/MyClass2, cet inconvénient rend cette approche inutilisable.

14 votes

Si vous avez besoin d'une nouvelle instance de dispositif de fixation pour chaque méthode de test, faites en sorte que la méthode des paramètres renvoie une fabrique que chaque test invoque pour obtenir son dispositif de fixation. Cela n'affecte pas la viabilité de cette approche.

1 votes

Que diriez-vous de mettre la référence de la classe dans les paramètres, et dans la méthode "InterfaceTesting" instancier avec la réflexion ?

22voto

AlexR Points 60796

Je ne suis pas du tout d'accord avec @dlev. Très souvent, c'est une très bonne pratique d'écrire des tests qui utilisent des interfaces. L'interface définit le contrat entre le client et l'implémentation. Très souvent, toutes vos implémentations doivent passer exactement les mêmes tests. Évidemment, chaque implémentation peut avoir ses propres tests.

Donc, je connais 2 solutions.

  1. Implémenter un scénario de test abstrait avec divers tests qui utilisent l'interface. Déclarer une méthode abstraite protégée qui retourne une instance concrète. Maintenant, héritez de cette classe abstraite autant de fois que nécessaire pour chaque implémentation de votre interface et implémentez la méthode de fabrique mentionnée en conséquence. Vous pouvez également ajouter des tests plus spécifiques ici.

  2. Utilice suites de tests .

16voto

Cedric Beust Points 7209

Je ne suis pas d'accord avec dlev non plus, il n'y a rien de mal à écrire vos tests sur des interfaces plutôt que sur des implémentations concrètes.

Vous souhaitez probablement utiliser des tests paramétrés. Voici à quoi cela ressemblerait avec TestNG Avec JUnit, c'est un peu plus compliqué (puisque vous ne pouvez pas passer de paramètres directement aux fonctions de test) :

@DataProvider
public Object[][] dp() {
  return new Object[][] {
    new Object[] { new MyImpl1() },
    new Object[] { new MyImpl2() },
  }
}

@Test(dataProvider = "dp")
public void f(MyInterface itf) {
  // will be called, with a different implementation each time
}

0 votes

Bonne réponse. Il semble que le testing soit un bon mécanisme pour cela.

5voto

dlev Points 28160

En général, j'évite d'écrire des tests unitaires pour une interface, pour la simple raison qu'une interface, même si vous le souhaitez, ne peut pas être testée, ne définit pas la fonctionnalité . Il encombre ses implémenteurs d'exigences syntaxiques, mais c'est tout.

À l'inverse, les tests unitaires visent à garantir que la fonctionnalité que vous attendez est présente dans un chemin de code donné.

Cela dit, il y a des situations où ce type de test peut avoir du sens. En supposant que vous vouliez que ces tests garantissent que les classes que vous avez écrites (qui partagent une interface donnée) partagent en fait la même fonctionnalité, je préférerais votre première option. Elle permet aux sous-classes d'implémentation de s'injecter plus facilement dans le processus de test. De plus, je ne pense pas que votre "contre" soit vraiment vrai. Il n'y a aucune raison pour que les classes réellement testées ne fournissent pas leurs propres mocks (bien que je pense que si vous avez vraiment besoin de mocks différents, alors cela suggère que vos tests d'interface ne sont pas uniformes de toute façon).

30 votes

Une interface ne définit pas la fonctionnalité, mais elle définit l'API à laquelle ses implémentations doivent se conformer. C'est ce sur quoi un test devrait se concentrer, alors pourquoi ne pas écrire un test pour exprimer cela ? En particulier lorsque vous faites bon usage du polymorphisme avec plusieurs implémentations d'une interface, ce type de test est extrêmement précieux.

2 votes

Je suis d'accord avec dlev - je ne vois pas l'intérêt de tester une interface. Le compilateur vous dira si votre implémentation concrète n'implémente pas l'interface. Je n'y vois aucune valeur. Les tests unitaires sont pour les classes concrètes.

0 votes

@Ryan (et tous les autres qui ne sont pas d'accord avec moi (certains fortement)) : Je comprends que c'est une convention qu'une interface définisse un contrat/API/etc. Et en fait, comme la dernière partie de ma réponse l'indique, sous certaines conditions, je pense que c'est une bonne idée (mais je vais mettre à jour pour que ce soit plus clair.) Cependant, ce n'est toujours qu'une convention. Si j'implémente une interface, je ne suis pas obligé de suivre l'API. En C#, je suis libre de lancer des NotImplementedExceptions pour certains membres si je le souhaite.

0voto

cWarren Points 36

Le problème avec les solutions ci-dessus est que les interfaces peuvent être mélangées dans l'implémentation de la classe mais pas les abstraits. Ainsi, pour construire un test unitaire complet de toutes les interfaces, vous devez construire des tests d'interface séparés pour chaque implémentation. Ce que je veux voir, c'est un mécanisme (piloté par annotation ?) qui prend des classes de test abstraites et fusionne toutes les méthodes annotées @Test en un seul test. Cela permettrait de tester tous les contrats d'interface dans toutes les implémentations. Une telle bête n'existe pas.

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