web-dev-qa-db-fra.com

Comment écrire des tests junit pour les interfaces?

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

par exemple. Vous avez cette interface et implémentant des classes:

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 écririez-vous un test sur l'interface pour 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:

  • Besoin d'une seule méthode à mettre en œuvre

Con:

  • Les dépendances et les objets fantaisie de la classe testée 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:

  • granualtion fine pour chaque test, y compris les dépendances et les objets fictifs

Con:

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

Quelle possibilité préféreriez-vous ou quelle autre méthode utilisez-vous?

68
Xeno Lupus

Contrairement à la réponse très apprécié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'elle s'exprime via son interface, est la chose la plus importante à tester. Cela étant dit, je n'utiliserais ni l'une ni l'autre des approches que vous avez mentionnées, mais plutôt un test paramétré , 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()}
        );
    }
}
72
Ryan Stewart

Je suis fortement en désaccord 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émentez un cas de test abstrait avec divers tests qui utilisent l'interface. Déclarez une méthode protégée abstraite qui renvoie une instance concrète. Héritez maintenant de cette classe abstraite autant de fois que nécessaire pour chaque implémentation de votre interface et implémentez la méthode d'usine mentionnée en conséquence. Vous pouvez également ajouter des tests plus spécifiques ici.

  2. Utilisez suites de tests .

18
AlexR

Je suis également en désaccord avec dlev, il n'y a rien de mal à écrire vos tests sur des interfaces au lieu d'implémentations concrètes.

Vous voudrez probablement utiliser des tests paramétrés. Voici à quoi cela ressemblerait avec TestNG , c'est un peu plus artificiel avec JUnit (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
}
14
Cedric Beust

Ajout tardif au sujet, partage de nouvelles idées de solution

Je recherche également un moyen approprié et efficace de tester (basé sur JUnit) l'exactitude de plusieurs implémentations de certaines interfaces et classes abstraites. Malheureusement, ni les tests @Parameterized De JUnit ni le concept équivalent de TestNG ne répondent correctement à mes exigences, car je ne connais pas a priori la liste des implémentations de ces classes d'interface/abstraites qui pourraient exister . Autrement dit, de nouvelles implémentations peuvent être développées et les testeurs peuvent ne pas avoir accès à toutes les implémentations existantes; il n'est donc pas efficace que les classes de test spécifient la liste des classes d'implémentation.

À ce stade, j'ai trouvé le projet suivant qui semble offrir une solution complète et efficace pour simplifier ce type de tests: https://github.com/Claudenw/junit-contracts . Il permet essentiellement la définition de "Contrats de Test", grâce à l'annotation @Contract(InterfaceClass.class) sur les classes de test de contrat. Ensuite, un implémenteur créerait une classe de test spécifique à l'implémentation, avec les annotations @RunWith(ContractSuite.class) et @ContractImpl(value = ImplementationClass.class); le moteur doit appliquer automatiquement tout test de contrat qui s'applique à ImplementationClass, en recherchant tous les tests de contrat définis pour toute interface ou classe abstraite dont dérive ImplementationClass. Je n'ai pas encore testé cette solution, mais cela semble prometteur.

J'ai également trouvé la bibliothèque suivante: http://www.jqno.nl/equalsverifier/ . Celui-ci répond à un besoin similaire mais beaucoup plus spécifique, qui consiste à affirmer une conformité de classe spécifiquement aux contrats Object.equals et Object.hashcode.

De même, https://bitbucket.org/chas678/testhelpers/src montre une stratégie pour valider certains Java contrats fondamentaux, y compris Object.equals, Object.hashcode, Comparable.compare, Serializable. Ce projet utilise des structures de test simples, qui, je crois, peuvent être facilement reproduites pour répondre à tous les besoins spécifiques.

Eh bien, c'est tout pour l'instant; Je garderai ce post mis à jour avec d'autres informations utiles que je pourrais trouver.

10
jwatkins

J'éviterais généralement d'écrire des tests unitaires par rapport à une interface, pour la simple raison qu'une interface, quelle que soit votre préférence, ne définit pas la fonctionnalité. Il encombre ses implémenteurs avec des 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 étant dit, il existe des situations où ce type de test pourrait avoir un 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é, alors je préférerais votre première option. Il est plus facile pour les sous-classes d'implémentation de s'injecter dans le processus de test. De plus, je ne pense pas que votre "con" soit vraiment vrai. Il n'y a aucune raison pour que les classes actuellement testées ne fournissent pas leurs propres simulations (bien que je pense que si vous avez vraiment besoin de simulations différentes, cela suggère que vos tests d'interface ne sont pas uniformes de toute façon.)

5
dlev

avec Java 8 je fais cela

public interface MyInterfaceTest {
   public MyInterface createInstance();

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

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

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

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

   @Disabled
   @Override
   @Test
   public void testMyMethod_True() {
       MyInterfaceTest.super.testMyMethod_True();
   };
}
1
Xavier Gouraud