2013-11-25 2 views
4

У меня есть метод, который содержит 2 условия. В каждом условии вызывается метод Logger.error. Первый тест, который проверяет вызов этого метода, преуспевает, но и любой другой тест не пройден сMockito не удалось проверить несколько вызовов методов из org.slf4j.Logger

Wanted, но не применяло ... На самом деле, были нулевые взаимодействий с это макет.

Кто-нибудь знает, почему это происходит?

Ниже я представил пример класса и модульный тест, который будет генерировать вопрос:

package packageName; 

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

public class X { 

    private static final Logger LOGGER = LoggerFactory.getLogger(X.class); 

    public void execute(boolean handle1stCase) { 
     if (handle1stCase) { 
      LOGGER.error("rumpampam"); 
     } else { 
      LOGGER.error("latida"); 
     } 
    } 
} 

Тест:

package packageName; 

import org.apache.commons.logging.LogFactory; 
import org.junit.Before; 
import org.junit.Test; 
import org.junit.runner.RunWith; 
import org.mockito.Mock; 
import org.mockito.MockitoAnnotations; 
import org.powermock.api.mockito.PowerMockito; 
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.Mockito.*; 
import static org.powermock.api.mockito.PowerMockito.mockStatic; 

@RunWith(PowerMockRunner.class) 
@PrepareForTest({LoggerFactory.class}) 
public class XTest { 

    @Mock 
    private Logger loggerMock; 

    private X x; 

    @Before 
    public void construct() { 
     MockitoAnnotations.initMocks(this); 

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

     x = new X(); 
    } 

    @Test 
    public void whenFirstCaseErrorLogged() throws Exception { 
     x.execute(true); 
     verify(loggerMock, times(1)).error("rumpampam"); 
    } 

    @Test 
    public void whenSecondCaseErrorLogged() throws Exception { 
     x.execute(false); 
     verify(loggerMock, times(1)).error("latida"); 
    } 
} 

Результат:

Требуются, но не вызывается: loggerMock.error ("latida"); -> в пакетеName.XTest.whenSecondCaseErrorLogged (XTest.java:51)
На самом деле было нулевое взаимодействие с этим макетом.

EDIT:
Я дал краткий ответ, почему каждый тест, за исключением 1-го терпел неудачу в comment of this answer.

МОЕ РЕШЕНИЕ ПРОБЛЕМЫ:
В тесте обеспечивают:

public static Logger loggerMockStatic; 

чем создать только один экземпляр для всех тестов и обеспечить его в статической переменной, и использовать статический loggerMockStatic из чем на. Таким образом, у вас было бы:

... 
    MockitoAnnotations.initMocks(this); 

    if (loggerMockStatic == null) { 
     loggerMockStatic = loggerMock; 
    } 

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

и использовать loggerMockStatic в методах проверки вместо loggerMock.

НЕКОТОРЫЕ МЫСЛИ ОТНОСИТЕЛЬНО ПОДХОД:
Для меня это хорошо, потому что
1. это не нарушает дизайн (если считать, что необходимая переменная должна быть постоянной, чем он будет оставаться таким образом) ,
2. В тест добавлены только 4 строки, которые позволят вам протестировать поведение константы (в данном случае регистратора). Не так много загрязняющих и тестовых случаев по-прежнему ясно.

Метод «удалить окончательный и обеспечить сеттер», как я объяснил в this answer, открывает систему уязвимостям. Нет необходимости кому-то устанавливать регистратор в класс, и мне всегда хотелось бы, чтобы система была открыта по мере необходимости. Не требуется установка сеттера только для необходимости теста. Тест должен работать для реализации, а не наоборот.

В частности, при проверке ведения журнала я не считаю, что регистрация должна быть проверена в целом (в большинстве случаев). Регистрация должна быть аспектом приложения.И когда у вас есть другие выходы для проверки определенного пути, чем те, которые должны быть протестированы. Но в этом случае (и, возможно, другие), где нет другого выхода для определенного пути, такого как ведение журнала и возврат в определенное состояние, требуется проверка журнала (по мне). Я хочу всегда знать, что сообщение журнала останется в журнале, даже если кто-то изменит это условие. Если не было журнала, и если кто-то изменит условие, то не будет никакого способа узнать, что ошибка находится в этом фрагменте кода (за исключением, может быть, отладки).

Я обсуждал с некоторыми коллегами, что наличие отдельного класса для ведения журнала приведет к трюку. Таким образом, константа изолирована в другом классе, и вы сможете проверять поведение только с помощью Mockito. Они сделали еще одно замечание, что таким образом, если вы захотите отправить журнал по электронной почте, было бы легче изменить его.
Прежде всего, я считаю это преждевременной модуляцией, если вы не собираетесь в ближайшем будущем переключаться между способами ведения журнала.
Во-вторых, используя только Mockito +, имеющий еще один класс и + 3 строки кода VS, моя одна строка кода (logger.error (...)) +, используя PowerMockito, я бы снова воспользовался позже. Добавление дополнительных зависимостей во время тестирования не сделает ваш производственный код более медленным и громоздким. Возможно, при рассмотрении вопроса о продолжении интеграции и о том, что тестирование так же важно, как и другие этапы, вы можете сказать, что это сделает процесс медленнее и громоздким при тестировании, но я бы пожертвовал этим - кажется, это не слишком важно для меня.

ответ

1

Вот почему это не работает:

поле в классе X является статичным и окончательным, который позволяет установить это только первый класс время загрузки. Это опасно из-за того, что я написал в своем первом ответе. В вашем случае вам повезет, и это не происходит, но ...

Junit выполняет ваш тест в следующем порядке: конструкция() whenFirstCaseErrorLogged() конструкт() whenSecondCaseErrorLogged()

Теперь давайте скажем, что после первого вызова метода construct() поле XlogestLoggerMock указывает на объект, который находится по адресу 0001. Этот объект затем используется LoggerFactory для инициализации поля LOGGER объекта x. Затем x.error вызывается из ifFirstCaseErrorLogged(), и это работает finde, потому что оба loggerMock и X :: Logger указывают на один и тот же объект.

Теперь мы переходим ко второй конструкции(). Ваш loggerMock повторно инициализируется, и теперь он указывает на другой объект, пусть предполагается, что он хранится в памяти по адресу 0002. Это новый объект, отличный от ранее созданного. Теперь, поскольку ваш X :: LOGGER является статическим окончательным, он не будет повторно инициализирован, поэтому он все еще указывает на объект, хранящийся по адресу 0001. Когда вы попытаетесь проверить методы, вызванные в loggerMock, вы получите ошибку, потому что на этом объекте ничего не было выполнено вместо этого вызывается метод ошибки вашего предыдущего объекта.

И вот некоторые мысли от меня. Может быть, они окажутся полезными. Я думаю, что в будущем вы должны пересмотреть использование статического дважды. Почему вы хотите сделать что-то постоянным, когда оно не является постоянным? Будет ли ваша ссылочная переменная иметь такое же значение после того, как вы будете работать во второй раз?Конечно, это может произойти, но это маловероятно. Может ли статическое окончание мешать вам изменить состояние объекта? Разумеется, они не будут препятствовать переназначению LOGGER в другой экземпляр. Вы упомянули в своем предыдущем комментарии, что вы не хотите, чтобы пользователь вашего кода предоставлял нулевую ссылку для вашего логина. Это нормально, но вы можете предотвратить это, выбросив исключение, если оно предоставлено, или используя другой механизм обработки null.

Много говорилось об использовании статического ключевого слова. Некоторые считают это чистым злом, а некоторые не любят, а некоторые все еще любят синглтоны :)

Независимо от того, что вы думаете, вам нужно знать, что статичность не подходит для тестирования и нарезания резьбы. Я использую статический финал, когда что-то статично, как PI, или число эйлеров, но я не использую статический final для объектов, которые имеют изменяемое состояние. Я использую статические методы для классов утилит, которые не хранят состояние, но просто выполняют некоторую обработку (разбор, подсчет и т. Д.) И возвращают результат imediatelly. Хорошим примером является математическая функция, такая как власть.

Я думаю, что это будет полезно;)

+0

«В вашем случае вам повезло, и это не происходит, но ... «Что касается удачи, это неправда. Я точно объяснил, почему в комментарии к вашему другому ответу: http://stackoverflow.com/a/20196591/521754 Я бы предложил в будущем не писать несколько ответов, а отредактировать свои старые с комментарием, который был отредактирован. – despot

+0

«Почему вы хотите сделать что-то постоянное, когда оно не постоянное?» Логгер не постоянный ?! Возможно, я ошибаюсь, но он не должен меняться во время выполнения. Разве это не определение константы в java ?! :) «Может ли статический финал помешать вам изменить состояние объекта?» какое состояние вы измените в регистраторе, мне просто любопытно :) (проверьте org.slf4j.Logger и посмотреть, какие методы у него есть;)) – despot

+0

Не нужно было вовлекать адреса памяти в ваш ответ :) этого было бы достаточно: «потому что ваш X :: LOGGER статичен, он не будет повторно инициализирован»;) Никогда тем меньше, я собираюсь принять ваш ответ, потому что 2 абзаца, которые начинаются с «Now let say ...», являются тем же самым комментарием комментария, который я дал вам в вашем предыдущем ответе http://stackoverflow.com/a/20196591/521754 (и я вижу, что вы заслуживаете повышения :)). Спасибо за ваши усилия Люк! Я предоставлю конкретный способ того, как я справлялся с ситуацией для всех, кого это интересует. – despot

4

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

+0

В этом случае первый тест должен иногда терпеть неудачу. Но это не так! – despot

+0

Потому что вам повезло, ваш первый тест всегда выполняется первым :) – MariuszS

+0

Я надеюсь, что такое счастье следует за мной :), но 1-й случай неудачи никогда не происходил. И будьте моим гостем :) попробуйте 1000 раз, и вы не получите его к неудаче. Вот почему (не успел ответить). В первом тесте powermockito инструктирует статический метод getLogger возвращать loggerMock ', и 1-й тест всегда будет успешным. Мое предположение заключалось в том, что классы перезагружаются, так что, когда во втором тесте powermockito инструктировал метод getLogger вернуть loggerMock '', второй тест будет успешным. Но второй тест работает с loggerMock '. Вот почему каждый другой тест терпит неудачу. Мое решение в редакторе – despot

0

Добавьте способ к классу X, чтобы разрешить установку регистратора, и удалите из него final. Затем сделайте что-то подобное в тесте.

@Mock private Logger mockLogger; 
private X toTest = new X(); 

... 
@Before 
public void setUp() throws Exception { 
    toTest.setLogger(mockLogger); 
} 

@Test 
public void logsRumpampamForFirstCall() throws Exception { 
    toTest.execute(true); 
    verify(mockLogger).error("rumpampam"); 
} 

@Test 
public void logsLatidaForOtherCalls() throws Exception { 
    toTest.execute(false); 
    verify(mockLogger).error("latida"); 
} 
+1

Это заставит тесты работать, вероятно. К сожалению, я бы никогда не попытался изменить дизайн системы, чтобы удовлетворить тест. Переменная LOGGER намеренно является константой (так что другие не могут ее изменить во всем приложении). – despot

+1

Это кажется странным выбором для меня. Это изменение сделает код более проверенным. Код, который более подвержен тестированию, менее подвержен ошибкам. В самом деле, простота тестирования - большая часть ремонтопригодности кода. –

+0

@ Давид Уоллес прав, но в этой ситуации мы можем тестировать без изменения системы :) – MariuszS

Смежные вопросы