java - unitaria - ¿Cómo escribir pruebas junit para interfaces?
pruebas unitarias de software ejemplo (6)
Contrariamente a la respuesta muy votada que @dlev dio, a veces puede ser muy útil / necesario escribir una prueba como la que está sugiriendo. La API pública de una clase, tal como se expresa a través de su interfaz, es lo más importante para probar. Dicho esto, no usaría ninguno de los enfoques que mencionó, sino una prueba Parameterized lugar, donde los parámetros son las implementaciones que se probarán:
@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()}
);
}
}
¿Cuál es la mejor manera de escribir pruebas junit para interfaces para que puedan ser utilizadas para las clases de implementación concretas?
Por ejemplo, tiene esta interfaz e implementa clases:
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;
}
}
¿Cómo escribirías una prueba en la interfaz para que puedas usarla para la clase?
Posibilidad 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:
- Solo se necesita implementar un método
Estafa:
- Las dependencias y los objetos simulados de clase bajo prueba tienen que ser iguales para todas las pruebas
Posibilidad 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:
- granualción fina para cada prueba que incluye dependencias y objetos falsos
Estafa:
- Cada clase de prueba de implementación requiere escribir métodos de prueba adicionales
¿Qué posibilidad preferirías o de qué otra manera usas?
En general, evitaría escribir pruebas unitarias en una interfaz, por la sencilla razón de que una interfaz, por mucho que le guste, no define la funcionalidad . Grava a sus implementadores con requisitos sintácticos, pero eso es todo.
Las pruebas unitarias, a la inversa, están destinadas a garantizar que la funcionalidad que espera esté presente en una ruta de código determinada.
Dicho esto, hay situaciones en las que este tipo de prueba podría tener sentido. Suponiendo que desea estas pruebas para asegurarse de que las clases que escribió (que comparten una interfaz determinada), de hecho, comparten la misma funcionalidad, entonces preferiría su primera opción. Hace que sea más fácil en las subclases de implementación inyectarse en el proceso de prueba. Además, no creo que tu "estafa" sea realmente cierta. No hay ninguna razón por la que no pueda hacer que las clases en cuestión proporcionen sus propios simulacros (aunque creo que si realmente necesita simulacros diferentes, eso sugiere que las pruebas de interfaz no son uniformes de todos modos).
Estoy totalmente en desacuerdo con @dlev. Muy a menudo es una muy buena práctica escribir pruebas que usan interfaces. La interfaz define el contrato entre el cliente y la implementación. Muy a menudo todas sus implementaciones deben pasar exactamente las mismas pruebas. Obviamente, cada implementación puede tener sus propias pruebas.
Entonces, sé 2 soluciones.
Implementar el caso de prueba abstracta con varias pruebas que usan interfaz. Declara un método protegido abstracto que devuelve una instancia concreta. Ahora herede esta clase abstracta tantas veces como necesite para cada implementación de su interfaz e implemente el método de fábrica mencionado en consecuencia. Puede agregar pruebas más específicas aquí también.
Usa suites de prueba
También estoy en desacuerdo con dlev, no hay nada de malo en escribir tus pruebas contra interfaces en lugar de implementaciones concretas.
Probablemente quiera usar pruebas parametrizadas. Esto es lo que se vería con TestNG , es un poco más artificial con JUnit (ya que no puede pasar los parámetros directamente a las funciones de prueba):
@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
}
con Java 8 hago esto
public abstract interface MyInterfaceTest {
public abstract 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();
};
}
Última incorporación al tema, compartiendo nuevas ideas de la solución
También estoy buscando una manera adecuada y eficiente de probar (en base a JUnit) la corrección de múltiples implementaciones de algunas interfaces y clases abstractas. Desafortunadamente, ni las pruebas @Parameterized
de JUnit ni el concepto equivalente de TestNG se ajustan correctamente a mis requisitos, ya que no conozco a priori la lista de implementaciones de estas clases de interfaces / abstractas que pueda existir. Es decir, se podrían desarrollar nuevas implementaciones y los probadores podrían no tener acceso a todas las implementaciones existentes; por lo tanto, no es eficiente tener clases de prueba que especifiquen la lista de clases de implementación.
En este punto, he encontrado el siguiente proyecto que parece ofrecer una solución completa y eficiente para simplificar este tipo de pruebas: https://github.com/Claudenw/junit-contracts . Básicamente, permite la definición de "Pruebas de contrato", a través de la anotación @Contract(InterfaceClass.class)
en las clases de prueba de contrato. Entonces, un implementador crearía una clase de prueba específica de implementación, con las anotaciones @RunWith(ContractSuite.class)
y @ContractImpl(value = ImplementationClass.class)
; el motor aplicará automáticamente cualquier prueba de contrato que aplique a ImplementationClass, buscando toda la Prueba de Contrato definida para cualquier interfaz o clase abstracta de la cual se deriva ImplementationClass. Todavía no he probado esta solución, pero esto parece prometedor.
También encontré la siguiente biblioteca: http://www.jqno.nl/equalsverifier/ . Éste satisface una necesidad similar aunque mucho más específica, que es afirmar una conformidad de clase específicamente para los contratos Object.equals y Object.hashcode.
Del mismo modo, https://bitbucket.org/chas678/testhelpers/src muestra una estrategia para validar algunos contratos fundamentales de Java, incluidos Object.equals, Object.hashcode, Comparable.compare, Serializable. Este proyecto utiliza estructuras de prueba simples, que, creo, se pueden reproducir fácilmente para adaptarse a cualquier necesidad específica.
Bueno eso es todo por ahora; Mantendré esta publicación actualizada con otras informaciones útiles que pueda encontrar.