style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">

Probar una cadena de llamadas con Mockito

Como @neodevelop explica en esta entrada: http://www.javamexico.org/blogs/neodevelop/pruebas_de_unidad_con_mockito... se puede probar con Mockito si un metodo fue llamado o no utilizando verify(objeto).nombreDelMetodo();

Utilice varias veces esa comprobación hasta que me fallo recientemente en un escenario como el siguiente: quería ver si mi nuevo método PhoneManager.getPhone(id) estaba invocando el método Profile.getPhone()

// FAIL
// prueba que el getter getPhone fue invocado:
class PhoneManagerTest {
    @InjectMocks PhoneManager phoneManager;
    @Mock Profile profile;
    ...
    @Test
    public void testGetPhone() {
        Assert.assertEquals("(55) 5658-1111", phoneManager.getContactPhone(1));
        Mockito.verify(profile).getPhone();
    }

El problema es que profile no es un atributo de PhoneManager sino que es obtenido con una cadena de llamadas como esta:

Profile profile = phoneManager.getAccountAdapter().getProfileService().getProfileById(id);

Agregarlo como atributo o pasarlo como parámetro arruinaba mi clase y en general creo que es mala idea cambiar el diseño para hacer feliz a las pruebas (por muy benéficas que sean)

Afortunadamente mockito permite anotar el mock para que simule las llamadas en cadena con la anotacion:

@Mock(answer = Answers.RETURNS_DEEP_STUBS)

Esto regresa otro stub en vez de nulo cada vez que se invoca un método.

Luego solo tuve que decir, cuando ejecutes esta cadena regresame este mock y mi prueba funciono:

// WIN
// prueba que el getter getPhone fue invocado:
class PhoneManagerTest {
    @InjectMocks
    PhoneManager phoneManager;
    @Mock
    Profile profile;
    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
    AccountAdapter accountAdapter;
    @Before
    void setUp() {
         Mockito.when(accountAdapter.getProfileService().getProfileById(1)).thenReturn(profile);
         Mockito.when(profile.getPhone()).thenReturn("(55) 5658-1111");
    }
 
    ...
    // Da daaaaaa
    @Test
    public void testGetPhone() {
        Assert.assertEquals("(55) 5658-1111", phoneManager.getContactPhone(1));
        Mockito.verify(profile).getPhone();
    }

Eso y con la ayuda de Mockito.when( cadena().de().llamadas() ) .thenReturn(elValorDePrueba) me permitieron que mi nuevo método funciona como espero.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de WinDoctor

interesante

Bastante interesante, muchas gracias por compartirlo...

También funcionara con PowerMockito para métodos estáticos?

Ejemplo:
ServiceBeanFactory.getServiceSecurity().obtenerParametroDeSistema(ServiceSecurity.PS_MODE_SAP);

Imagen de ElderMael

Ley de Demeter

Creo que seria bueno puntualizar que si haces tests donde ocupes Answers.RETURNS_DEEP_STUBS probablemente estes rompiendo el principio del menor conocimiento (Ley de Demeter).

Incluso en la documentacion de Mockito advierte sobre esto:

WARNING: This feature should rarely be required for regular clean code! Leave it for legacy code. Mocking a mock to return a mock, to return a mock, (...), to return something meaningful hints at violation of Law of Demeter or mocking a value object (a well known anti-pattern).

Hay programadores que bromean diciendo “Every time a mock returns a mock, a fairy dies (cada vez que un mock devuelve un mock, una hada muere)".

Aunque en tu codigo de ejemplo usas clases de modelo... no se si esto tambien deberia aplicar en este caso.

Si, lo vi y lo consideré y

Si, lo vi y lo consideré y aunque para ser sincero no lo pensé mucho y no quería dejar este caso sin cubrir.

Lo mencioné así:


El problema es que profile no es un atributo de PhoneManager sino que es obtenido con una cadena de llamadas como esta:
Profile profile = phoneManager.getAccountAdapter().getProfileService().getProfileById(id);

El account adapter está una biblioteca externa ( de un proyecto interno si, pero que usan otras aplicaciones ) y como tal no podía cambiar su firma sin romper los demás clientes.

Las opciones que pensé eran:

- Agregar al profile como atributo de instancia e inyectarlo lo cual me pareció muy mala opción pues un "administrador de telefonos" no necesita saber nada de una cuenta en especifico
- Pasarlo como parámetro lo cual llevaba a que tenía que exponer fuera del phone manager al account adapter para que de ahí obtuviera el profile y pasárselo como parámetro, lo cual es peor aún porque expone cosas que van dentro del objeto ( como sacarle el corazón para mostrarselo antes de mor... no bueno creo que es mal ejemplo )

Y todo para poder pasarle un profile ficticio .... mmhhhh suena a que estaba cambiando mi diseño para hacer feliz un concepto. Y si, en esas estaba pensando y pensando y después de una hora perdida ( o quizá más, ya sabes, empiezas buscando una cosa en Google y terminas leyendo artículos de no se que cosa no relacionada o peor aún viendo los videos más chistosos del 2013 y así ) Así que por mi bien opté por esa opción.

Aún me queda un poquito la espinita de si podría haber otra opción, pero pensé que un par de hadas muertas no estarían mal ( malo que fueran gatitos, ahí si ni hablar )

Super el comentario, no sabía que así se llamaba el principio ese ( si vi la doc y es lo que me hizo sentirme mal cuando lo hice, pero me aguanté)

Imagen de ElderMael

No queda de otra

Hay veces en que no queda de otra pero... yo creo que también hay muchos gatos ya!

En mi caso muy particular estaba haciendo test contra el API de Spring Batch (en la cual y opte por mejor hacer una clase de utileria en lugar de hacer tantos "method chaining" pero en el test case no hubo de otra que usar deep stubs; entonces al principio tenia algo asi:

        JobExecution jobExecution = stepExecution.getJobExecution();
        ExecutionContext jobContext = jobExecution.getExecutionContext();
        String param = jobContext.get("someKey");

y luego lo cambie por algo asi:

        String param = TaskUtils.getRequiredParameter(jobExecution, "someKey");

Como ves, igual que en tu caso, es una librería externa.

style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">