Usando Mockito para probar clases abstractas

213

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

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

ripper234
fuente
2
A partir de Mockito 1.10.12 , Mockito admite clases abstractas de espionaje / burla directamente:SomeAbstract spy = spy(SomeAbstract.class);
pesche
66
A partir de Mockito 2.7.14, también puede burlarse de clases abstractas que requieren argumentos de constructor a través demock(MyAbstractClass.class, withSettings().useConstructor(arg1, arg2).defaultAnswer(CALLS_REAL_METHODS))
Gediminas Rimsa

Respuestas:

315

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

use Mockito.mock(My.class, Mockito.CALLS_REAL_METHODS), luego simule cualquier método abstracto que se invoque.

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 y cuando nunca se invoquen.

En mi sincera opinión, esto es más ordenado que usar un espía, ya que un espía requiere una instancia, lo que significa que debe crear una subclase de su clase abstracta con instancia.

Morten Lauritsen Khodabocus
fuente
14
Como se indica a continuación, esto no funciona cuando la clase abstracta llama a métodos abstractos para ser probados, lo que a menudo es el caso.
Richard Nichols
11
Esto realmente funciona cuando la clase abstracta llama a métodos abstractos. Solo use la sintaxis doReturn o doNothing en lugar de Mockito. Cuando realice un trozo de los métodos abstractos, y si calla cualquier llamada concreta, asegúrese de que primero se tapen las llamadas abstractas.
Gonen I
2
¿Cómo puedo inyectar dependencias en este tipo de objeto (clase abstracta simulada que llama a métodos reales)?
Samuel
2
Esto se comporta de manera inesperada si la clase en cuestión tiene inicializadores de instancia. Mockito omite los inicializadores para simulacros, lo que significa que las variables de instancia que se inicializan en línea serán inesperadamente nulas, lo que puede causar NPE.
baño digital
1
¿Qué pasa si el constructor de la clase abstracta toma uno o más parámetros?
SD
68

Si solo necesita probar algunos de los métodos concretos sin tocar ninguno de los resúmenes, puede usar CALLS_REAL_METHODS(vea 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 funcionará - 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, lo fuerzan).

La solución alternativa es revertir este enfoque: use el comportamiento simulado ordinario (es decir, todo se burla / tropeza) y úselo doCallRealMethod()para llamar 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 nulos, deberá usar thenCallRealMethod()en su lugar, por ejemplo:

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

De lo contrario, Mockito se quejará "Se detectó un trozo inacabado".

David Moles
fuente
99
Esto funcionará en algunos casos, sin embargo, Mockito no llama al constructor de la clase abstracta subyacente con este método. Esto puede hacer que el "método real" falle debido a la creación de un escenario inesperado. Por lo tanto, este método tampoco funcionará en todos los casos.
Richard Nichols
3
Sí, no puede contar con el estado del objeto en absoluto, solo el código en el método que se llama.
David Moles
Ah, entonces los métodos de objeto se separan del estado, genial.
Haelix
17

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()));
Richard Nichols
fuente
14

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

Al probar una clase abstracta, desea ejecutar los métodos no abstractos del Sujeto bajo prueba (SUT), por lo que un marco de imitación no es lo que desea.

Parte de la confusión es que la respuesta a la pregunta que vinculaste dijo para crear una simulación que se extiende desde tu clase abstracta. No llamaría a una clase como una burla. Un simulacro es una clase que se usa como reemplazo de una dependencia, se programa con expectativas y se puede consultar para ver si se cumplen esas expectativas.

En cambio, sugiero definir una subclase no abstracta de su clase abstracta en su prueba. Si eso resulta en demasiado código, 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 del Método de plantilla ).

NamshubWriter
fuente
8

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<Object> {

    public Object answer(InvocationOnMock invocation) throws Throwable {

        Answer<Object> answer = null;

        if (isAbstract(invocation.getMethod().getModifiers())) {

            answer = Mockito.RETURNS_DEFAULTS;

        } else {

            answer = Mockito.CALLS_REAL_METHODS;
        }

        return answer.answer(invocation);
    }
}

Devolverá la simulación de métodos abstractos y llamará al método real para métodos concretos.


fuente
5

Lo que realmente me hace sentir mal por burlarme de las clases abstractas es el hecho de que ni el constructor predeterminado YourAbstractClass () se llama (falta super () en simulacro) ni parece que haya alguna forma en Mockito para inicializar las propiedades simuladas por defecto (por ejemplo, Propiedades de lista con ArrayList o LinkedList vacíos).

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

Solo los atributos de clase utilizan la inicialización predeterminada: lista privada dep1 = nueva lista de matriz; Lista privada dep2 = nueva ArrayList

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

Lástima que solo PowerMock ayudaría más aquí.

Thomas Heiss
fuente
2

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

YourClass yourObject = mock(YourClass.class);

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

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

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

Por otro lado, 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.

Nick Holt
fuente
3
Pero usar el simulacro no probará los métodos concretos de YourClass, ¿o me equivoco? Esto no es lo que busco.
ripper234
1
Eso es correcto, lo anterior no funcionará si desea invocar los métodos concretos en la clase abstracta.
Richard Nichols
Disculpas, editaré un poco sobre las expectativas, que son necesarias para cada método que llames, no solo los abstractos.
Nick Holt
pero aún estás probando tu simulacro, no los métodos concretos.
Jonatan Cloutier
2

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.
DwB
fuente
2

Mockito permite burlarse de clases abstractas mediante la @Mockanotación:

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.

Jorge Pastor
fuente
0

Puede crear una instancia de una clase anónima, inyectar sus simulacros 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 ser protectedpara la propiedad myDependencyServicede la clase abstracta ClassUnderTest.

Samuel
fuente
0

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

Codificador inteligente
fuente