Registrador Burlón y LoggerFactory con PowerMock y Mockito


Tengo el siguiente Logger que quiero burlarme, pero para validar las 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 lo que terminé con 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 burlarme de la estática LoggerFactory.getLogger() para trabajar para cualquier clase?
  2. Solo puedo parecer que corro when(loggerMock.isDebugEnabled()).thenReturn(true); en el @Before y por lo tanto no puedo parecer cambiar el características por método. ¿Hay alguna manera de evitar esto?

Editar hallazgos:

Pensé que ya había probado esto y no funcionó:{[17]]}

 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 conseguir que el loggerMock cambie su comportamiento fuera de @Before pero esto solo sucede con Coburtura. Con Clover, la cobertura muestra 100%, pero todavía hay un problema manera.

Tengo esta clase simple:{[17]]}

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 clover muestro una cobertura del 100% del bloque if(logger.isDebugEnabled()){. Pero si trato de verificar el loggerMock:

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

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

Esto parece extraño que Cobertura muestre que el if(logger.isDebugEnabled()){ no está 100% completo, y Clover lo hace, pero ambos están de acuerdo en que la verificación falla.

Author: Adrian Jałoszewski, 2012-01-21

5 answers

@ Mick, intenta preparar al dueño del campo estático también, 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"); }
}

Luego 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 ! Las bibliotecas más destacadas en el classpath son: 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, que son característica / parte de negocio del sistema entonces introducir una dependencia real que deje claro que estos registros son características sería mucho mejor en todo el diseño del sistema, en lugar de confiar en el código estático de un estándar y las clases técnicas de un registrador.

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

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

 53
Author: Brice,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-07-12 13:26:08

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

Usando la orientación de lo que jheriks amd Joseph Lust había puesto creo que sé por qué - Tenía mi objeto bajo prueba como un campo y newed en un @Antes a diferencia de Brice. Entonces el logger real no era el simulacro sino una clase real iniciada como jhriks sugirió...

Normalmente lo haría haga esto para mi objeto bajo prueba para obtener un objeto nuevo para cada prueba. Cuando moví el campo a un local y lo newed en la prueba funcionó bien. 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 cero interacciones.

Cuando pongo la creación del mock en la @BeforeClass el logger en el objeto bajo prueba es siempre el mock 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 tienes dos pruebas con la misma expectativa tuve que hacer la verificación en la @AfterClass ya que las invocaciones en la estática se apilan - verify(mockLOG, times(2)).info("true"); - en lugar de veces(1) en cada prueba como la segunda prueba fallaría diciendo allí donde 2 invocación de esto. Esto es pantalones bonitos, pero no pude encontrar una manera de despejar las invocaciones. Me gustaría saber si alguien puede pensar de todo esto....

 10
Author: user1276925,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-11 20:57:37

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);

Con respecto a su segunda pregunta (y posiblemente el comportamiento desconcertante con la primera), creo que el problema es que logger es estático. Así,

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

Se ejecuta cuando se inicializa la clase , no cuando se instanciael objeto . A veces esto puede ser casi al mismo tiempo, por lo que estarás bien, pero es difícil garantizarlo. Para configurar LoggerFactory.getLogger para devolver tu mock, pero la variable logger ya puede haber sido configurada con un objeto Logger real en el momento en que tus mock sean configurados.

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

 5
Author: jhericks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-01-24 17:20:43

Creo que puedes restablecer las invocaciones usando Mockito.reset(mockLog). Deberías llamar a esto antes de cada prueba, así que inside @ Before sería un buen lugar.

 2
Author: Markus Wendl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-11-05 12:06:50

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

Los patrones que utilizan cualquier cosa classloader ancho como carpeta de registro estático o jugando con el medio ambiente piensa como logback.XML son un fracaso cuando se trata de 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 una dependencia inyectada loggerfactory desde la parte superior, pero no si inyecta Logger ya que no hay costura en este ensamblaje en ILoggerFactory.getLogger.

Y tampoco se trata de pruebas unitarias. A veces queremos que las pruebas de integración emitan registros. Queremos que algunos de los registros de pruebas de integración se supriman selectivamente, por ejemplo, para los errores esperados que de otro modo desordenarían la consola de CI y confundirían. Todo es fácil si inyecta ILoggerFactory desde el parte superior de su línea principal (o cualquier marco di que pueda usar)

So...

O bien inyectar un reportero como se sugiere o adoptar un patrón de inyección de la ILoggerFactory. Mediante la inyección explícita de ILoggerFactory en lugar de Logger, puede admitir muchos patrones de acceso / interceptación y paralelización.

 0
Author: johnlon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-30 19:08:48