mock example dzone java junit mockito slf4j powermock

java - example - mockito verify spy



Mocking Logger y LoggerFactory con PowerMock y Mockito (5)

Tengo el siguiente Logger que quiero simular, pero para validar entradas de registro se están llamando, no para el contenido.

private static Logger logger = LoggerFactory.getLogger(GoodbyeController.class);

Quiero simular CUALQUIER clase que se use para LoggerFactory.getLogger () pero no pude averiguar cómo hacerlo. Esto es con lo que terminé hasta ahora:

@Before public void performBeforeEachTest() { PowerMockito.mockStatic(LoggerFactory.class); when(LoggerFactory.getLogger(GoodbyeController.class)). thenReturn(loggerMock); when(loggerMock.isDebugEnabled()).thenReturn(true); doNothing().when(loggerMock).error(any(String.class)); ... }

Me gustaría saber:

  1. ¿Puedo LoggerFactory.getLogger() estático funciona para cualquier clase?
  2. Solo puedo ejecutar when(loggerMock.isDebugEnabled()).thenReturn(true); en @Before y por lo tanto no puedo cambiar las características por método. ¿Hay alguna forma de evitar esto?

Editar hallazgos:

Pensé que había intentado esto y no funcionó:

when(LoggerFactory.getLogger(any(Class.class))).thenReturn(loggerMock);

Pero gracias, ya que funcionó.

Sin embargo, he intentado innumerables variaciones para:

when(loggerMock.isDebugEnabled()).thenReturn(true);

No puedo hacer que loggerMock cambie su comportamiento fuera de @Before pero esto solo ocurre con Coburtura. Con Clover, la cobertura muestra el 100% pero todavía hay un problema en ambos sentidos.

Tengo esta clase simple:

public ExampleService{ private static final Logger logger = LoggerFactory.getLogger(ExampleService.class); public String getMessage() { if(logger.isDebugEnabled()){ logger.debug("isDebugEnabled"); logger.debug("isDebugEnabled"); } return "Hello world!"; } ... }

Entonces tengo esta prueba:

@RunWith(PowerMockRunner.class) @PrepareForTest({LoggerFactory.class}) public class ExampleServiceTests { @Mock private Logger loggerMock; private ExampleServiceservice = new ExampleService(); @Before public void performBeforeEachTest() { PowerMockito.mockStatic(LoggerFactory.class); when(LoggerFactory.getLogger(any(Class.class))). thenReturn(loggerMock); //PowerMockito.verifyStatic(); // fails } @Test public void testIsDebugEnabled_True() throws Exception { when(loggerMock.isDebugEnabled()).thenReturn(true); doNothing().when(loggerMock).debug(any(String.class)); assertThat(service.getMessage(), is("Hello null: 0")); //verify(loggerMock, atLeast(1)).isDebugEnabled(); // fails } @Test public void testIsDebugEnabled_False() throws Exception { when(loggerMock.isDebugEnabled()).thenReturn(false); doNothing().when(loggerMock).debug(any(String.class)); assertThat(service.getMessage(), is("Hello null: 0")); //verify(loggerMock, atLeast(1)).isDebugEnabled(); // fails } }

En trébol, muestro 100% de cobertura del bloque if(logger.isDebugEnabled()){ . Pero si intento verificar el loggerMock :

verify(loggerMock, atLeast(1)).isDebugEnabled();

Obtengo cero interacciones. También probé PowerMockito.verifyStatic() ; en @Before pero eso también tiene cero interacciones.

Esto parece extraño que Cobertura muestre if(logger.isDebugEnabled()){ no está completo al 100%, y Clover sí, pero ambos aceptan que la verificación falla.


En respuesta a su primera pregunta, debería ser tan simple como reemplazar:

when(LoggerFactory.getLogger(GoodbyeController.class)).thenReturn(loggerMock);

con

when(LoggerFactory.getLogger(any(Class.class))).thenReturn(loggerMock);

En cuanto a su segunda pregunta (y posiblemente la desconcertante conducta con la primera), creo que el problema es que el registrador es estático. Asi que,

private static Logger logger = LoggerFactory.getLogger(GoodbyeController.class);

se ejecuta cuando la clase se inicializa, no cuando se crea una instancia del objeto . A veces, esto puede ser aproximadamente al mismo tiempo, por lo que estará bien, pero es difícil garantizar eso. Así que configura LoggerFactory.getLogger para devolver su simulación, pero la variable del registrador ya puede haberse configurado con un objeto Logger real en el momento en que se configuran los simuladores.

Puede configurar el registrador explícitamente usando algo como ReflectionTestUtils (no sé si eso funciona con campos estáticos) o cambiarlo de un campo estático a un campo de instancia. De cualquier manera, no es necesario que se burle de LoggerFactory.getLogger porque estará inyectando directamente la instancia falsa de Logger.


@Mick, intente preparar también al propietario del campo estático, por ejemplo:

@PrepareForTest({GoodbyeController.class, LoggerFactory.class})

EDIT1: Acabo de crear un pequeño ejemplo. Primero el controlador:

import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Controller { Logger logger = LoggerFactory.getLogger(Controller.class); public void log() { logger.warn("yup"); } }

Entonces la prueba:

import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.verify; import static org.powermock.api.mockito.PowerMockito.mock; import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.when; @RunWith(PowerMockRunner.class) @PrepareForTest({Controller.class, LoggerFactory.class}) public class ControllerTest { @Test public void name() throws Exception { mockStatic(LoggerFactory.class); Logger logger = mock(Logger.class); when(LoggerFactory.getLogger(any(Class.class))).thenReturn(logger); new Controller().log(); verify(logger).warn(anyString()); } }

Tenga en cuenta las importaciones! Libs notables en el classpath: Mockito, PowerMock, JUnit, logback-core, logback-clasic, slf4j

EDIT2: Como parece ser una pregunta popular, me gustaría señalar que si estos mensajes de registro son tan importantes y requieren ser probados, es decir, son parte funcional / comercial del sistema y luego presentan una dependencia real que deja en claro los registros de tesis son características que serían mucho mejores en todo el diseño del sistema , en lugar de depender del código estático de un estándar y las clases técnicas de un registrador.

Para este caso, recomendaría crear algo así como = una clase Reporter con métodos como reportIncorrectUseOfYAndZForActionX o reportIncorrectUseOfYAndZForActionX . Esto tendría el beneficio de hacer que la característica sea visible para cualquiera que lea el código. Pero también ayudará a lograr pruebas, cambiar los detalles de las implementaciones de esta característica en particular.

Por lo tanto, no necesitaría herramientas de burla estáticas como PowerMock. En mi opinión, el código estático puede estar bien, pero tan pronto como la prueba exige verificar o simular el comportamiento estático, es necesario refactorizar e introducir dependencias claras.


Algo tarde para la fiesta: estaba haciendo algo similar y necesitaba algunos consejos y terminé aquí. Sin tomar crédito, tomé todo el código de Brice pero recibí las "cero interacciones" que recibió Cengiz.

Utilizando la guía de lo que Jheriks y Joseph Lust pusieron, creo que sé por qué. Tuve mi objeto bajo prueba como campo y lo reinicié en un @Before a diferencia de Brice. Entonces el registrador real no era el simulacro, sino una verdadera clase iniciada como jhriks sugirió ...

Normalmente haría esto para mi objeto bajo prueba a fin de obtener un objeto nuevo para cada prueba. Cuando moví el campo a un local y lo reinicié en la prueba, se ejecutó correctamente. Sin embargo, si probé una segunda prueba, no fue el simulacro en mi prueba, sino el simulacro de la primera prueba y volví a tener las interacciones cero.

Cuando pongo la creación del simulacro en @BeforeClass, el registrador en el objeto bajo prueba es siempre el simulacro pero vea la nota a continuación para los problemas con esto ...

Clase bajo prueba

import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyClassWithSomeLogging { private static final Logger LOG = LoggerFactory.getLogger(MyClassWithSomeLogging.class); public void doStuff(boolean b) { if(b) { LOG.info("true"); } else { LOG.info("false"); } } }

Prueba

import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.mockito.Mockito.*; import static org.powermock.api.mockito.PowerMockito.mock; import static org.powermock.api.mockito.PowerMockito.*; import static org.powermock.api.mockito.PowerMockito.when; @RunWith(PowerMockRunner.class) @PrepareForTest({LoggerFactory.class}) public class MyClassWithSomeLoggingTest { private static Logger mockLOG; @BeforeClass public static void setup() { mockStatic(LoggerFactory.class); mockLOG = mock(Logger.class); when(LoggerFactory.getLogger(any(Class.class))).thenReturn(mockLOG); } @Test public void testIt() { MyClassWithSomeLogging myClassWithSomeLogging = new MyClassWithSomeLogging(); myClassWithSomeLogging.doStuff(true); verify(mockLOG, times(1)).info("true"); } @Test public void testIt2() { MyClassWithSomeLogging myClassWithSomeLogging = new MyClassWithSomeLogging(); myClassWithSomeLogging.doStuff(false); verify(mockLOG, times(1)).info("false"); } @AfterClass public static void verifyStatic() { verify(mockLOG, times(1)).info("true"); verify(mockLOG, times(1)).info("false"); verify(mockLOG, times(2)).info(anyString()); } }

Nota

Si tiene dos pruebas con la misma expectativa, tuve que hacer la verificación en @AfterClass, ya que las invocaciones sobre la estática se acumulan: verify(mockLOG, times(2)).info("true"); - en lugar de veces (1) en cada prueba ya que la segunda prueba fallaría diciendo que hay una invocación de esto. Esto es bastante pantalones, pero no pude encontrar una manera de borrar las invocaciones. Me gustaría saber si alguien puede pensar en una forma de evitar esto ...


Creo que puede restablecer las invocaciones utilizando Mockito.reset (mockLog). Deberías llamar a esto antes de cada prueba, por lo que dentro de @Before sería un buen lugar.


Use inyección explícita. Ningún otro enfoque le permitirá, por ejemplo, ejecutar pruebas en paralelo en la misma JVM.

Los patrones que usan cualquier cargador de clase como el archivador de anotaciones estáticas o los problemas con el medio ambiente como logback.XML son irrelevantes en lo que respecta a las pruebas.

Considere las pruebas paralelizadas que menciono, o considere el caso en el que desea interceptar el registro del componente A cuya construcción está oculta detrás de la api B. Este último caso es fácil de tratar si está utilizando un loggerfactory inyectado de dependencia desde la parte superior, pero no si inyectas Logger ya que no hay costura en este ensamblaje en ILoggerFactory.getLogger.

Y no todo se trata de pruebas unitarias tampoco. A veces queremos pruebas de integración para emitir el registro. A veces no lo hacemos. Alguien quiere que parte del registro de prueba de integración sea suprimido selectivamente, por ejemplo, para errores esperados que de otra manera saturarían la consola de CI y confundirían. Todo fácil si se inyecta ILoggerFactory desde la parte superior de su línea principal (o cualquier di framework que pueda usar)

Asi que...

O bien, inyecte un reportero según lo sugerido o adopte un patrón de inyección de ILoggerFactory. Mediante la inyección explícita de ILoggerFactory en lugar de Logger puede admitir muchos patrones de acceso / intercepción y paralelización.