Имитация Logger и LoggerFactory с помощью PowerMock и Mockito

У меня есть следующий Logger, который я хочу издеваться, но для проверки записей журнала вызываются, а не для содержимого.

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

Я хочу издеваться над ЛЮБЫМ классом, который используется для LoggerFactory.getLogger(), но я не мог понять, как это сделать. Вот что у меня получилось до сих пор:

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

    ...
}

Я бы хотел знать:

  1. Могу ли я издеваться над статическим LoggerFactory.getLogger() для работы с любым классом?
  2. Кажется, я могу запустить только when(loggerMock.isDebugEnabled()).thenReturn(true); в @Before, и поэтому я не могу изменить характеристики для каждого метода. Есть ли способ обойти это?

Изменить результаты:

Я думал, что уже пробовал это, и это не сработало:

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

Но спасибо, как то заработало.

Однако я пробовал бесчисленное количество вариантов:

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

Я не могу заставить loggerMock изменить свое поведение за пределами @Before, но это происходит только с Coburtura. С Clover покрытие показывает 100%, но в любом случае есть проблема.

У меня есть этот простой класс:

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!";
    }
    ...
}

Тогда у меня есть этот тест:

@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
    }
}

В клевере показываю 100% покрытие блока if(logger.isDebugEnabled()){. Но если я попытаюсь проверить loggerMock:

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

Я получаю ноль взаимодействий. Я также пробовал PowerMockito.verifyStatic(); в @Before, но это также не имеет взаимодействий.

Просто кажется странным, что Cobertura показывает if(logger.isDebugEnabled()){ как не завершенный на 100%, а Clover это делает, но оба соглашаются, что проверка не удалась.


person Mick Knutson    schedule 20.01.2012    source источник
comment
Вы пробовали @MockPolicy? Примеры здесь относятся к макетам в стиле EasyMock, но их можно адаптировать для Mockito.   -  person Matt Lachman    schedule 27.06.2013


Ответы (7)


РЕДАКТИРОВАТЬ 2020-09-21: Начиная с версии 3.4.0, Mockito поддерживает имитацию статических методов, API все еще находится в стадии инкубации и, вероятно, изменится, в частности, в отношении заглушки и проверки. Требуется артефакт mockito-inline. И вам не нужно готовить тест или использовать какой-то конкретный бегун. Все, что вам нужно сделать, это:

@Test
public void name() {
    try (MockedStatic<LoggerFactory> integerMock = mockStatic(LoggerFactory.class)) {
        final Logger logger = mock(Logger.class);
        integerMock.when(() -> LoggerFactory.getLogger(any(Class.class))).thenReturn(logger);
        new Controller().log();
        verify(logger).warn(any());
    }
}

Два важных аспекта в этом коде заключаются в том, что вам нужно определить область действия, когда применяется статический макет, то есть в этом блоке try. А API заглушки и проверки нужно вызывать из объекта MockedStatic.


@Mick, попробуй также подготовить владельца статического поля, например:

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

EDIT1: я только что создал небольшой пример. Сначала контроллер:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Controller {
    Logger logger = LoggerFactory.getLogger(Controller.class);

    public void log() { logger.warn("yup"); }
}

Затем тест:

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

Обратите внимание на импорт! Примечательные библиотеки в пути к классам: Mockito, PowerMock, JUnit, logback-core, logback-clasic, slf4j.


РЕДАКТИРОВАТЬ2: Поскольку это кажется популярным вопросом, я хотел бы указать, что если эти сообщения журнала настолько важны и требуют тестирования, то есть они являются функциональной/бизнес-частью системы < strong>затем введение реальной зависимости, которая ясно показывает, что эти журналы являются функциями, было бы намного лучше в дизайне всей системы вместо того, чтобы полагаться на статический код стандартного и технического классов регистратора.

В этом случае я бы рекомендовал создать что-то вроде класса Reporter с такими методами, как reportIncorrectUseOfYAndZForActionX или reportProgressStartedForActionX. Это позволит сделать функцию видимой для всех, кто читает код. Но это также поможет добиться тестов, изменить детали реализации этой конкретной функции.

Следовательно, вам не понадобятся статические инструменты для имитации, такие как PowerMock. На мой взгляд, статический код может подойти, но как только тест требует проверки или имитации статического поведения, необходимо провести рефакторинг и ввести четкие зависимости.

person Brice    schedule 29.01.2012
comment
ПРИМЕЧАНИЕ. Если вы попытаетесь выполнить второй тест @ Test, у вас возникнут проблемы. verify() не будет работать при повторном вызове в дополнительном тесте. Нет, если вы используете @ Before или новые имена переменных. ClassLoader создаст только один из них, поэтому у вас не может быть двух разных статических классов. Разбейте тесты на отдельные классы. - person Joseph Lust; 23.02.2013
comment
К сожалению, я не могу заставить его работать... Мокито говорит... На самом деле, с этим макетом не было никаких взаимодействий. - person Cengiz; 18.03.2013
comment
У меня работает нормально (включая проверку). Убедитесь, что вы используете @PrepareForTest, а также убедитесь, что вы имитировали правильный метод поиска в журнале фабрики журналов. - person Lo-Tan; 22.05.2013
comment
+1 за упоминание о введении реальной зависимости, которая ясно показывает, что эти журналы - это функции, которые были бы намного лучше во всей конструкции системы. - person Vlad Dinulescu; 10.10.2016
comment
@Brice Я знаю, что это довольно старый вопрос, но я хотел бы спросить вас кое-что о вашем совете по введению реальной зависимости и избеганию необходимости в инструментах статического моделирования. Я полностью согласен с вашей точкой зрения о том, чтобы сделать эту функцию видимой для всех, однако я думаю, что если вам нужно также провести модульное тестирование этого класса отчетов, вам все равно нужно будет использовать эти статические инструменты имитации. Не так ли? - person beni0888; 22.05.2018
comment
@ beni0888 beni0888 Действительно, рекомендуется провести модульное тестирование этого класса, но конкретную зависимость можно спроектировать с помощью поля Logger, установленного в конструкторе (-ах), и поэтому можно было бы передать чистый mockito Logger mock в каком-то конструкторе, который виден только для целей тестирования. - person Brice; 23.05.2018

Несколько опоздал на вечеринку - я делал что-то подобное, мне нужны были подсказки, и я оказался здесь. Без кредита — я взял весь код у Брайса, но получил «нулевое взаимодействие», чем Ченгиз.

Используя руководство из того, что написали jheriks и Joseph Lust, я думаю, что знаю, почему - я тестировал свой объект как поле и обновил его в @Before, в отличие от Брайса. Тогда настоящий регистратор был не макетом, а настоящим классом, как предложил jhriks...

Обычно я делаю это для своего тестируемого объекта, чтобы получать новый объект для каждого теста. Когда я переместил поле на локальное и обновил его в тесте, все прошло нормально. Однако, если я попробовал второй тест, это был не макет в моем тесте, а макет из первого теста, и я снова получил нулевое взаимодействие.

Когда я помещаю создание макета в @BeforeClass, регистратор в тестируемом объекте всегда является макетом, но см. примечание ниже для проблем с этим...

Тестируемый класс

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

    }
}

Тест

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

Примечание

Если у вас есть два теста с одним и тем же ожиданием, мне пришлось выполнить проверку в @AfterClass, поскольку вызовы статики складываются — verify(mockLOG, times(2)).info("true"); — а не раз (1) в каждом тесте, поскольку второй тест не сработает, говоря там, где 2 вызов этого. Это красивые штаны, но я не смог найти способ очистить вызовы. Я хотел бы знать, может ли кто-нибудь придумать способ обойти это....

person user1276925    schedule 11.06.2014
comment
Предложение Маркуса Вендла использовать сброс сработало для меня. - person Ryan Pfister; 16.11.2015
comment
+500. @BeforeClass изменил меня. - person Jameson; 09.06.2018

В ответ на ваш первый вопрос это должно быть так же просто, как заменить:

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

с участием

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

Что касается вашего второго вопроса (и, возможно, загадочного поведения с первым), я думаю, проблема в том, что регистратор статичен. Так,

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

выполняется при инициализации класса, а не при создании экземпляра объекта. Иногда это может быть примерно в одно и то же время, так что все будет в порядке, но это трудно гарантировать. Таким образом, вы настроили LoggerFactory.getLogger для возврата вашего макета, но переменная logger, возможно, уже была установлена ​​​​с реальным объектом Logger к тому времени, когда ваши макеты настроены.

Вы можете установить регистратор явно, используя что-то вроде ReflectionTestUtils (я не знаю, работает ли это со статическими полями) или изменить его из статического поля в поле экземпляра. В любом случае вам не нужно имитировать LoggerFactory.getLogger, потому что вы будете напрямую внедрять фиктивный экземпляр Logger.

person jhericks    schedule 21.01.2012

Я думаю, вы можете сбросить вызовы, используя Mockito.reset(mockLog). Вы должны вызывать это перед каждым тестом, так что внутри @Before будет хорошим местом.

person Markus Wendl    schedule 05.11.2014

Используйте явную инъекцию. Никакой другой подход не позволит вам, например, запускать тесты параллельно в одной и той же JVM.

Шаблоны, использующие загрузчик классов, такие как статическое связывание журналов, или возня с внешними мыслями, такие как logback.XML, терпят неудачу, когда дело доходит до тестирования.

Рассмотрим параллелизованные тесты, о которых я упоминаю, или рассмотрим случай, когда вы хотите перехватить логирование компонента А, конструкция которого скрыта за API Б. С этим последним случаем легко справиться, если вы используете внедренный сверху loggerfactory зависимостей, а не если вы вводите Logger, так как в этой сборке нет шва в ILoggerFactory.getLogger.

И дело не только в модульном тестировании. Иногда мы хотим, чтобы интеграционные тесты выводили журнал. Иногда нет. Кто-то хочет, чтобы некоторые журналы интеграционного тестирования выборочно подавлялись, например, для ожидаемых ошибок, которые в противном случае загромождали бы консоль CI и приводили к путанице. Все легко, если вы вводите ILoggerFactory из верхней части вашей основной ветки (или любой другой фреймворк, который вы можете использовать)

So...

Либо внедрите генератор отчетов, как было предложено, либо примените шаблон внедрения ILoggerFactory. Путем явного внедрения ILoggerFactory, а не Logger, вы можете поддерживать множество шаблонов доступа/перехвата и распараллеливания.

person johnlon    schedule 30.11.2015

Ниже приведен тестовый класс, который имитирует частный статический окончательный регистратор с именем log в классе LogUtil.

В дополнение к издевательству над вызовом фабрики getLogger необходимо явно установить поле с помощью отражения, в @BeforeClass

public class LogUtilTest {

    private static Logger logger;

    private static MockedStatic<LoggerFactory> loggerFactoryMockedStatic;

    /**
     * Since {@link LogUtil#log} being a static final variable it is only initialized once at the class load time
     * So assertions are also performed against the same mock {@link LogUtilTest#logger}
     */
    @BeforeClass
    public static void beforeClass() {
        logger = mock(Logger.class);
        loggerFactoryMockedStatic = mockStatic(LoggerFactory.class);
        loggerFactoryMockedStatic.when(() -> LoggerFactory.getLogger(anyString())).thenReturn(logger);
        Whitebox.setInternalState(LogUtil.class, "log", logger);
    }

    @AfterClass
    public static void after() {
        loggerFactoryMockedStatic.close();
    }
} 
person Gayan Weerakutti    schedule 21.01.2021

class SomeService{
  private static final Logger logger = LogManager.getLogger(SomeService.class);

  public void callSomeMethod(){
   ...
   ...
   if (logger.isDebugEnabled()) {
        logger.debug(String.format("some message ....."));
    }
  }

}

Тестовые покрытия JUNIT –

import java.io.IOException;
 
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
 
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
 

@RunWith(MockitoJUnitRunner.class)
public class SomeServiceTest {
  @Mock
  private Appender mockAppender;
 
  private Logger logger;
 
  @Before
  public void setup() {
    when(mockAppender.getName()).thenReturn("MockAppender");
    when(mockAppender.isStarted()).thenReturn(true);
    when(mockAppender.isStopped()).thenReturn(false);
 
    logger = (Logger)LogManager.getLogger(SomeService.class);
    logger.addAppender(mockAppender);
    logger.setLevel(Level.DEBUG);
  }
 
  
 
  @Test
  public void callSomeMethodTest() {
    //write test cases, logger.isDebugEnabled() will call.
  }
 
}
person Ajay Kumar    schedule 10.06.2021