Usando Mockito para probar clases abstractas

Me gustaría probar una clase abstracta. Claro, puedo escribir manualmente un simulacro que hereda de la clase.

¿Puedo hacer esto usando un marco de burla (estoy usando Mockito) en lugar de crear a mano mi simulacro? ¿Cómo?

La siguiente sugerencia permite probar clases abstractas sin crear una subclase “real”: el simulacro es la subclase.

usa Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS) y luego Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS) métodos abstractos invocados.

Ejemplo:

 public abstract class My { public Result methodUnderTest() { ... } protected abstract void methodIDontCareAbout(); } public class MyTest { @Test public void shouldFailOnNullIdentifiers() { My my = Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS); Assert.assertSomething(my.methodUnderTest()); } } 

Nota: La belleza de esta solución es que no tiene que implementar los métodos abstractos, siempre que nunca se invoquen.

En mi opinión honesta, esto es más limpio que usar un espía, ya que un espía requiere una instancia, lo que significa que tiene que crear una subclase instanciable de su clase abstracta.

Si solo necesita probar algunos de los métodos concretos sin tocar ninguno de los resúmenes, puede usar CALLS_REAL_METHODS (ver la respuesta de Morten ), pero si el método concreto bajo prueba llama a algunos de los resúmenes, o métodos de interfaz no implementados, esto no trabajo – Mockito se quejará “No se puede llamar al método real en la interfaz de Java”.

(Sí, es un diseño pésimo, pero algunos marcos, por ejemplo, Tapestry 4, te fuerzan a ti).

La solución consiste en invertir este enfoque: use el comportamiento simulado ordinario (es decir, todo se burla / repite) y use doCallRealMethod() para doCallRealMethod() explícitamente el método concreto bajo prueba. P.ej

 public abstract class MyClass { @SomeDependencyInjectionOrSomething public abstract MyDependency getDependency(); public void myMethod() { MyDependency dep = getDependency(); dep.doSomething(); } } public class MyClassTest { @Test public void myMethodDoesSomethingWithDependency() { MyDependency theDependency = mock(MyDependency.class); MyClass myInstance = mock(MyClass.class); // can't do this with CALLS_REAL_METHODS when(myInstance.getDependency()).thenReturn(theDependency); doCallRealMethod().when(myInstance).myMethod(); myInstance.myMethod(); verify(theDependency, times(1)).doSomething(); } } 

Actualizado para agregar:

Para los métodos no vacíos, deberá usar thenCallRealMethod() lugar, por ejemplo:

 when(myInstance.myNonVoidMethod(someArgument)).thenCallRealMethod(); 

De lo contrario, Mockito se quejará de que se ha detectado un “tropezón inacabado”.

Puede lograr esto utilizando un espía (sin embargo, use la última versión de Mockito 1.8+).

 public abstract class MyAbstract { public String concrete() { return abstractMethod(); } public abstract String abstractMethod(); } public class MyAbstractImpl extends MyAbstract { public String abstractMethod() { return null; } } // your test code below MyAbstractImpl abstractImpl = spy(new MyAbstractImpl()); doReturn("Blah").when(abstractImpl).abstractMethod(); assertTrue("Blah".equals(abstractImpl.concrete())); 

Los frameworks de burla están diseñados para facilitar el cálculo de las dependencias de la clase que está probando. Cuando utiliza un marco de burla para simular una clase, la mayoría de los marcos crean dinámicamente una subclase y reemplazan la implementación del método con código para detectar cuándo se llama un método y devolver un valor falso.

Al probar una clase abstracta, desea ejecutar los métodos no abstractos del sujeto sometido a prueba (SUT), por lo que un marco de burla no es lo que desea.

Parte de la confusión es que la respuesta a la pregunta con la que te vinculaste decía que se hacía una muestra a mano que se extiende desde tu clase abstracta. No llamaría a esa clase una burla. Un simulacro es una clase que se usa como reemplazo de una dependencia, se progtwig con expectativas y se puede consultar para ver si se cumplen esas expectativas.

En cambio, sugiero que defina una subclase no abstracta de su clase abstracta en su prueba. Si eso resulta en demasiados códigos, entonces eso puede ser una señal de que su clase es difícil de extender.

Una solución alternativa sería hacer que su caso de prueba sea abstracto, con un método abstracto para crear el SUT (en otras palabras, el caso de prueba usaría el patrón de diseño de Método de plantilla ).

Intenta usar una respuesta personalizada.

Por ejemplo:

 import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; public class CustomAnswer implements Answer { public Object answer(InvocationOnMock invocation) throws Throwable { Answer answer = null; if (isAbstract(invocation.getMethod().getModifiers())) { answer = Mockito.RETURNS_DEFAULTS; } else { answer = Mockito.CALLS_REAL_METHODS; } return answer.answer(invocation); } } 

Devolverá el simulacro de métodos abstractos y llamará al método real para métodos concretos.

Lo que realmente me hace sentir mal por burlarse de las clases abstractas es el hecho de que ni el constructor predeterminado YourAbstractClass () se llama (missing super () en mock) ni parece haber ninguna forma en Mockito de inicializar por defecto las propiedades simuladas (p. Ej. con ArrayList o LinkedList vacías).

Mi clase abstracta (básicamente, el código fuente de la clase se genera) NO proporciona una inyección de setter de dependencia para los elementos de la lista, ni un constructor donde inicializa los elementos de la lista (que traté de agregar manualmente).

Solo los atributos de clase usan la inicialización predeterminada: private List dep1 = new ArrayList; private List dep2 = new ArrayList

Por lo tanto, NO hay forma de burlarse de una clase abstracta sin utilizar una implementación de objeto real (por ejemplo, definición de clase interna en clase de prueba de unidad, métodos abstractos generales) y espiar el objeto real (que realiza una inicialización de campo adecuada).

Lástima que solo PowerMock ayude más allá.

Suponiendo que sus clases de prueba están en el mismo paquete (bajo una raíz de origen diferente) que las clases bajo prueba, simplemente puede crear el simulacro:

 YourClass yourObject = mock(YourClass.class); 

y llame a los métodos que desea probar tal como lo haría con cualquier otro método.

Debe proporcionar expectativas para cada método que se llame con la expectativa de cualquier método concreto que invoque el súper método; no estoy seguro de cómo lo haría con Mockito, pero creo que es posible con EasyMock.

Todo lo que está haciendo es crear una instancia concreta de YouClass y ahorrarle el esfuerzo de proporcionar implementaciones vacías de cada método abstracto.

Como comentario adicional, a menudo me resulta útil implementar la clase abstracta en mi prueba, donde sirve como una implementación de ejemplo que pruebo a través de su interfaz pública, aunque esto depende de la funcionalidad proporcionada por la clase abstracta.

Puede extender la clase abstracta con una clase anónima en su prueba. Por ejemplo (usando Junit 4):

 private AbstractClassName classToTest; @Before public void preTestSetup() { classToTest = new AbstractClassName() { }; } // Test the AbstractClassName methods. 

Puede instanciar una clase anónima, inyectar sus simulaciones y luego probar esa clase.

 @RunWith(MockitoJUnitRunner.class) public class ClassUnderTest_Test { private ClassUnderTest classUnderTest; @Mock MyDependencyService myDependencyService; @Before public void setUp() throws Exception { this.classUnderTest = getInstance(); } private ClassUnderTest getInstance() { return new ClassUnderTest() { private ClassUnderTest init( MyDependencyService myDependencyService ) { this.myDependencyService = myDependencyService; return this; } @Override protected void myMethodToTest() { return super.myMethodToTest(); } }.init(myDependencyService); } } 

Tenga en cuenta que la visibilidad debe estar protected para la propiedad myDependencyService de la clase abstracta ClassUnderTest .

Whitebox.invokeMethod (..) puede ser útil en este caso.

Mockito permite burlarse de las clases abstractas mediante la anotación @Mock :

 public abstract class My { public abstract boolean myAbstractMethod(); public void myNonAbstractMethod() { // ... } } @RunWith(MockitoJUnitRunner.class) public class MyTest { @Mock(answer = Answers.CALLS_REAL_METHODS) private My my; @Test private void shouldPass() { BDDMockito.given(my.myAbstractMethod()).willReturn(true); my.myNonAbstractMethod(); // ... } } 

La desventaja es que no se puede usar si necesita parámetros de constructor.