Friday, April 8, 2011

Inyectando un Spring Service con PowerMock + Mockito

Ayer a la tarde estaba intentando hacer un test para nuestra aplicación que usa Spring en el backend. Una de las cosas que me di cuenta que cambiaron desde la ultima vez que use Spring es que todos los beans son inyectados automagicamente con la anotación @Autowire, por lo que no es necesario ni tener un constructor ni un setter para inyectar una dependencia. La pregunta ahora es como hago para testear estos beans? (para inyectar mocks en estos beans). Dps. de investigar un rato encuentro la libreria PowerMock que permite hacer cosas locas, como stubbear un metodo estático u obtener/setear el estado interno de un bean (rompiendo asi la encapsulación). Por ejemplo, la sintaxis para setear una variable privada es la siguiente:
        
UserRepository userRepositoryMock = mock(UserRepository.class); // declaro el mock, usando mockito
when(userRepositoryMock.safeGetLoggedUser()).thenReturn(adminUser); // seteo las expectativas
Whitebox.setInternalState(invoiceRepository, "userRepository", userRepositoryMock); // inyecto el mock, usando Powermock

Cual es el problema con el que me tope? Spring utiliza proxies para manejar las transacciones via AOP y estos proxies implementan la interfase del servicio, que no contiene los setters para sus campos. Osea que si quisieramos hacer lo mismo para un servicio (en vez de un repositorio como en el ejemplo),

Whitebox.setInternalState(scannedFileService, "userRepository", userRepositoryMock); // inyecto el mock, usando Powermock

PowerMock se quejaría (porque estamos queriendo llamar un método en una clase que no tiene ese metodo). La exception que arroja es la siguiente:

Caused by: java.lang.RuntimeException: You want me to set value to this field: 'userRepository' on this class: 'Object' but this field is not declared withing hierarchy of this class!
    at org.mockito.internal.util.reflection.Whitebox.getFieldFromHierarchy(Whitebox.java:40)
    at org.mockito.internal.util.reflection.Whitebox.setInternalState(Whitebox.java:25)
    ... 31 more


Esto se podría arreglar agregando el setter a la interfase del servicio, pero la verdad es q no me parecia limpio tener q modificar la interfase publica solamente a efectos de testear. Por suerte, existe una solución. Investigando un poco mas, encontre este foro que indica como hacer con Spring para obtener el objeto 'aconsejado' (advised) desde el proxy.

Advised advised = (Advised) scannedFileService;
ScannedFileService target = (ScannedFileService)advised.getTargetSource().getTarget();
Whitebox.setInternalState(target, "userRepository", userRepositoryMock);

et voila! Una vez obtenido el objeto aconsejado, powermock se encarga del resto. Espero sus comentarios (les parece un overkill? Agregarian los setters directamente? etc.)

Agrego dependencias de powermock en maven para aquellos que son lentos como yo y les cuesta encontrar las dependencias :-).

       
                org.powermock.modules
                powermock-module-junit4
                1.3.9
                test
       

       
                org.powermock.api
                powermock-api-mockito
                1.3.9
                test