Подтвердить что ты не робот

Mocking Logger и LoggerFactory с помощью PowerMock и Mockito

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

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

Я хочу, чтобы Mock ANY класс, который используется для 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));

    ...
}

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

  • Могу ли я подделать статический LoggerFactory.getLogger() для работы в любом классе?
  • Кажется, я могу запустить 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 делает, но оба согласны с тем, что проверка не выполняется.

4b9b3361

Ответ 1

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

Обратите внимание на импорт! Обращаем внимание на libs в пути к классам: Mockito, PowerMock, JUnit, logback-core, logback-clasic, slf4j


EDIT2: Как кажется, популярный вопрос, я хотел бы указать, что , если эти сообщения журнала важны и требуют проверки, то есть они являются частью/бизнес-частью система , а затем введя реальную зависимость, которая делает четкие тезисы, что функции будут намного лучше во всем дизайне системы, вместо того, чтобы полагаться на статический код стандартного и технического классов регистратора.

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

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

Ответ 2

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

   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 к моменту настройки ваших mocks.

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

Ответ 3

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

Используя руководство от того, что jheriks amd Joseph Lust положил, я думаю, что знаю, почему - у меня был объект под тестом в качестве поля и он появился в @Before, в отличие от Brice. Тогда фактический логгер был не макетом, а реальным классом init'd, как предложил 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");
        }

    }
}

Test

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 вызова этого. Это довольно брюки, но я не мог найти способ очистить вызовы. Я хотел бы знать, может ли кто-нибудь подумать об этом.

Ответ 4

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

Ответ 5

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

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

Рассмотрим параллельные тесты, о которых я упоминаю, или рассмотрим случай, когда вы хотите перехватить регистрацию компонента A, конструкция которого скрыта за api B. Этот последний случай легко справиться, если вы используете зависимую инъекцию loggerfactory из верхней части, но нет, если вы введете Logger, так как там нет шва в этой сборке в ILoggerFactory.getLogger.

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

Итак...

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