Scala, Specs2, Mockito и нулевые возвращаемые значения

Я пытаюсь протестировать код Scala, используя Specs2 и Mockito. Я относительно новичок во всех трех, и у меня возникают трудности с издевательством над методами, возвращающими нуль.

Далее (переписано с некоторыми изменениями имени)

  "My Component's process(File)" should  {

    "pass file to Parser" in new modules {
      val file = mock[File]
      myComponent.process(file)

      there was one(mockParser).parse(file)
    }

    "pass parse result to Translator" in new modules {
      val file = mock[File]
      val myType1 = mock[MyType1]

      mockParser.parse(file) returns (Some(myType1))
      myComponent.process(file)

      there was one(mockTranslator).translate(myType1)
    }

  }

«Передать файл парсеру» работает до тех пор, пока я не добавлю вызов транслятора в SUT, а затем умирает, потому что метод mockParser.parse вернул нуль, который код транслятора не может принять.

Точно так же «передать результат синтаксического анализа переводчику» проходит до тех пор, пока я не попытаюсь использовать результат перевода в SUT.

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

Я, конечно, могу обойти это, поместив проверки нулей в SUT, но я бы предпочел этого не делать, так как я никогда не возвращаю нули и вместо этого использую Option, None и Some.

Указатели на хороший учебник Scala/Specs2/Mockito были бы замечательными, как и простой пример того, как изменить строку, например

there was one(mockParser).parse(file)

чтобы заставить его возвращать что-то, что позволяет продолжить выполнение в SUT, когда он не имеет дело с нулями.

Пытаясь понять это, я попытался изменить эту строку на

there was one(mockParser).parse(file) returns myResult

со значением для myResult того типа, который я хочу вернуть. Это дало мне ошибку компиляции, поскольку она ожидает найти там MatchResult, а не мой возвращаемый тип.

Если это имеет значение, я использую Scala 2.9.0.


person Don Roby    schedule 02.09.2011    source источник


Ответы (3)


Если вы его не видели, вы можете посмотреть страница фиктивных ожиданий документации spec2.

В вашем коде заглушка должна быть mockParser.parse(file) returns myResult

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

Произошло недоразумение. То, как вы делаете это во втором примере, является хорошим, и вы должны сделать то же самое в первом тесте:

val file = mock[File]
val myType1 = mock[MyType1]

mockParser.parse(file) returns (Some(myType1))
myComponent.process(file)
there was one(mockParser).parse(file)

Идея юнит-тестирования с моками всегда одна и та же: объясните, как работают ваши моки (заглушки), выполните, проверьте.

Это должно ответить на вопрос, теперь личный совет:

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

В вашем примере метод process должен «переводить вещи», поэтому ваши модульные тесты должны быть сосредоточены на нем: издеваться над вашими парсерами и трансляторами, заглушать их и проверять только результат всего процесса. Это менее мелкое зерно, но цель модульного теста не в том, чтобы проверять каждый шаг метода. Если вы хотите изменить реализацию, вам не нужно изменять кучу модульных тестов, проверяющих каждую строку метода.

person Nicolas    schedule 02.09.2011
comment
Я использую это в заглушке для второго теста, но проблема в ожидании, а не в заглушках. Я отредактирую, чтобы уточнить. - person Don Roby; 03.09.2011
comment
Я сделал это редактирование сейчас, и я также посмотрю на страницу, на которую вы ссылаетесь. Я думаю, что видел это раньше, но, возможно, пропустил важную подсказку в предыдущем чтении. - person Don Roby; 03.09.2011
comment
Ага! Никогда не думал о том, чтобы просто заглушить и ожидать одного и того же метода. Но это работает, по крайней мере, с Mockito. Мне кажется, что это не так с некоторыми фреймворками... - person Don Roby; 04.09.2011

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

Что мне нужно было сделать, так это предоставить разумное возвращаемое значение по умолчанию для макета в форме org.mockito.stubbing.Answer<T> с типом возвращаемого значения T.

Я смог сделать это со следующей макетной настройкой:

val defaultParseResult = new Answer[Option[MyType1]] {
  def answer(p1: InvocationOnMock): Option[MyType1] = None
}
val mockParser = org.mockito.Mockito.mock(implicitly[ClassManifest[Parser]].erasure,
                         defaultParseResult).asInstanceOf[Parser]

после небольшого просмотра источника для признака org.specs2.mock.Mockito и вещей, которые он вызывает.

И теперь, вместо того, чтобы возвращать null, синтаксический анализ возвращает None, когда он не заглушен (в том числе когда это ожидается, как в первом тесте), что позволяет пройти тест с этим значением, используемым в тестируемом коде.

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

Я не смог найти поддержку более короткого способа сделать это в org.specs2.mock.Mockito, но, возможно, это вдохновит Эрика на его добавление. Приятно, что автор в разговоре...

Изменить

При дальнейшем изучении источника мне пришло в голову, что я должен просто вызвать метод

def mock[T, A](implicit m: ClassManifest[T], a: org.mockito.stubbing.Answer[A]): T = org.mockito.Mockito.mock(implicitly[ClassManifest[T]].erasure, a).asInstanceOf[T]

определено в org.specs2.mock.MockitoMocker, что на самом деле послужило источником вдохновения для моего решения выше. Но я не могу понять звонок. mock довольно перегружен, и все мои попытки, кажется, заканчиваются вызовом другой версии и не нравятся мои параметры.

Итак, похоже, что Эрик включил поддержку для этого, но я не понимаю, как это сделать.

Обновить

Я определил черту, содержащую следующее:

  def mock[T, A](implicit m: ClassManifest[T], default: A): T = {
    org.mockito.Mockito.mock(
      implicitly[ClassManifest[T]].erasure,
      new Answer[A] {
      def answer(p1: InvocationOnMock): A = default
    }).asInstanceOf[T]
  }

и теперь, используя эту черту, я могу настроить свой макет как

implicit val defaultParseResult = None
val mockParser = mock[Parser,Option[MyType1]]

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

Мне все еще было бы интересно, как справиться с этой проблемой, не добавляя эту черту.

person Don Roby    schedule 03.09.2011
comment
В последней версии 1.7-SNAPSHOT я добавил возможность передавать любые настройки Mockito, включая ответы по умолчанию. В этой версии вы можете написать: val mockParser = mock[Parser].defaultReturn(None) Если у вас есть более подробные ответы, вы можете написать: val mockParser = mock[Parser].defaultAnswer((i: InvocationOnMock) => whatever(i)). Документация доступна здесь - person Eric; 05.09.2011
comment
Спасибо. Я посмотрю на это. - person Don Roby; 05.09.2011

Без полного сложно сказать, но не могли бы вы проверить, что метод, который вы пытаетесь имитировать, не является окончательным методом? Потому что в этом случае Mockito не сможет издеваться над ним и вернет null.

Еще один совет, когда что-то не работает, — переписать код с помощью Mockito в стандартном тесте JUnit. Затем, если это не удастся, на ваш вопрос может лучше всего ответить кто-то из списка рассылки Mockito.

person Eric    schedule 03.09.2011
comment
Это не окончательно. Макеты и заглушки работают, и тесты проходят с добавленными нулевыми проверками в тестируемом коде. - person Don Roby; 03.09.2011