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:
Con:
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:
Con:
Quelle possibilité préféreriez-vous ou quelle autre méthode utilisez-vous?
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()}
);
}
}
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.
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.
Utilisez suites de tests .
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
}
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.
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.)
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();
};
}