¿Es este un uso apropiado del método de reinicio de Mockito?

68

Tengo un método privado en mi clase de prueba que construye un Barobjeto de uso común . El Barconstructor llama al someMethod()método en mi objeto burlado:

private @Mock Foo mockedObject; // My mocked object
...

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
}

En algunos de mis métodos de prueba que quiero verificar someMethodtambién fue invocado por esa prueba en particular. Algo como lo siguiente:

@Test
public void someTest() {
  Bar bar = getBar();

  // do some things

  verify(mockedObject).someMethod(); // <--- will fail
}

Esto falla porque el objeto burlado se había someMethodinvocado dos veces. No quiero que mis métodos de prueba se preocupen por los efectos secundarios de mi getBar()método, ¿sería razonable restablecer mi objeto simulado al final de getBar()?

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
  reset(mockedObject); // <-- is this OK?
}

Pregunto, porque la documentación sugiere que restablecer objetos simulados es generalmente indicativo de malas pruebas. Sin embargo, esto me parece bien.

Alternativa

La opción alternativa parece estar llamando:

verify(mockedObject, times(2)).someMethod();

lo que en mi opinión obliga a cada prueba a conocer las expectativas de getBar(), sin ningún beneficio.

Duncan Jones
fuente

Respuestas:

60

Creo que este es uno de los casos en los que usar reset()está bien. La prueba que está escribiendo es la prueba de que "algunas cosas" activan una sola llamada someMethod(). Escribir la verify()declaración con cualquier número diferente de invocaciones puede generar confusión.

  • atLeastOnce() permite falsos positivos, lo cual es algo malo, ya que desea que sus pruebas siempre sean correctas.
  • times(2)evita el falso positivo, pero hace que parezca que esperas dos invocaciones en lugar de decir "sé que el constructor agrega una". Además, si algo cambia en el constructor para agregar una llamada adicional, la prueba ahora tiene la posibilidad de un falso positivo. Y al eliminar la llamada, la prueba fallará porque la prueba ahora es incorrecta en lugar de lo que se está probando.

Al utilizar reset()el método auxiliar, evita ambos problemas. Sin embargo, debe tener cuidado de que también restablecerá cualquier apéndice que haya realizado, así que tenga cuidado. La razón principal que reset()se desaconseja es evitar

bar = mock(Bar.class);
//do stuff
verify(bar).someMethod();
reset(bar);
//do other stuff
verify(bar).someMethod2();

Esto no es lo que el OP está tratando de hacer. El OP, supongo, tiene una prueba que verifica la invocación en el constructor. Para esta prueba, el reinicio permite aislar esta acción individual y su efecto. Este es uno de los pocos casos con los que reset()puede ser útil. Las otras opciones que no lo usan tienen desventajas. El hecho de que el OP hizo esta publicación muestra que está pensando en la situación y no solo utilizando ciegamente el método de reinicio.

muestreador
fuente
17
Deseo que Mockito proporcione la llamada resetInteractions () para olvidar las interacciones pasadas con el fin de verificar (..., veces (...)) y mantener el apéndice. Esto haría que las situaciones de prueba de {setup; Actuar; verificar;} mucho más fácil de tratar. Sería {setup; resetInteractions; Actuar; verificar}
Arkadiy
2
En realidad, desde Mockito 2.1, proporciona una forma de borrar invocaciones sin restablecer los trozos:Mockito.clearInvocations(T... mocks)
Colin D Bennett
6

Los usuarios de Smart Mockito apenas usan la función de reinicio porque saben que podría ser un signo de pruebas deficientes. Normalmente, no necesita restablecer sus simulacros, solo cree simulacros nuevos para cada método de prueba.

En lugar de reset()considerar la posibilidad de escribir métodos de prueba simples, pequeños y enfocados en pruebas largas y demasiado especificadas. El primer olor potencial del código está reset()en el medio del método de prueba.

Extraído de los documentos mockito .

Mi consejo es que intentes evitar usarlo reset(). En mi opinión, si llama dos veces a someMethod, eso debería probarse (tal vez sea un acceso a la base de datos u otro proceso largo del que desee ocuparse).

Si realmente no te importa eso, puedes usar:

verify(mockedObject, atLeastOnce()).someMethod();

Tenga en cuenta que esto último podría causar un resultado falso, si llama a someMethod desde getBar, y no después (este es un comportamiento incorrecto, pero la prueba no fallará).

greuze
fuente
2
Sí, he visto esa cita exacta (la he vinculado desde mi pregunta). Actualmente todavía no veo un argumento decente sobre por qué mi ejemplo anterior es "malo". ¿Puedes suministrar uno?
Duncan Jones
Si necesita restablecer sus objetos simulados, parece que está intentando probar demasiadas cosas en su prueba. Puedes dividirlo en dos pruebas, probando cosas más pequeñas. En cualquier caso, no sé por qué está verificando dentro del método getBar, es difícil rastrear lo que está probando. Le recomiendo que diseñe su prueba pensando en lo que debe hacer su clase (si debe llamar a someMethod exactamente dos veces, al menos una vez, solo una vez, nunca, etc.), y haga todas las verificaciones en el mismo lugar.
greuze
He editado mi pregunta para resaltar que el problema persiste incluso si no llamo verifya mi método privado (que estoy de acuerdo, probablemente no pertenezca allí). Agradezco sus comentarios sobre si su respuesta cambiaría.
Duncan Jones
Hay muchas buenas razones para usar reset, no le prestaría demasiada atención en este caso a esa cita simulada. Puede ejecutar el JUnit Class Runner de Spring cuando ejecute un conjunto de pruebas que provoque interacciones no deseadas, especialmente si realiza pruebas que implican llamadas de base de datos simuladas o llamadas que involucran métodos privados sobre los que no le importa usar la reflexión.
Sandy Simonton
Por lo general, encuentro esto difícil cuando quiero probar varias cosas, pero JUnit simplemente no ofrece ninguna forma agradable (!) De parametrizar las pruebas. A diferencia de NUnit, por ejemplo, con anotaciones.
Stefan Hendriks
3

Absolutamente no. Como suele ser el caso, la dificultad que tiene para escribir una prueba limpia es una gran señal de alerta sobre el diseño de su código de producción. En este caso, la mejor solución es refactorizar su código para que el constructor de Bar no llame a ningún método.

Los constructores deben construir, no ejecutar la lógica. Tome el valor de retorno del método y páselo como un parámetro constructor.

new Bar(mockedObject);

se convierte en:

new Bar(mockedObject.someMethod());

Si esto resultara en la duplicación de esta lógica en muchos lugares, considere crear un método de fábrica que pueda probarse independientemente de su objeto Bar:

public Bar createBar(MockedObject mockedObject) {
    Object dependency = mockedObject.someMethod();
    // ...more logic that used to be in Bar constructor
    return new Bar(dependency);
}

Si esta refactorización es demasiado difícil, entonces usar reset () es una buena solución. Pero seamos claros: indica que su código está mal diseñado.

tonelada
fuente