Проверка Mockito не выполняется с помощью TooManyActualInvocations для метода с параметром по умолчанию в Scala

В приведенном ниже коде Mockito verify не работает должным образом для методов scala с параметром по умолчанию, но отлично работает с методами без параметров по умолчанию.

package verifyMethods

import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.Mockito.times
import org.scalatest.FlatSpec
import org.scalatest.Matchers.be
import org.scalatest.Matchers.convertToAnyShouldWrapper
import org.scalatest.junit.JUnitRunner
import org.scalatest.mock.MockitoSugar

trait SUT {

  def someMethod( bool: Boolean ): Int = if ( bool ) 4 else 5

  def someMethodWithDefaultParameter( bool: Boolean, i: Int = 5 ): Int = if ( bool ) 4 else i
}

@RunWith( classOf[JUnitRunner] )
class VerifyMethodWithDefaultParameter extends FlatSpec with MockitoSugar with SUT {

  "mockito verify method" should "pass" in {
    val sutMock = mock[SUT]
    Mockito.when( sutMock.someMethod( true ) ).thenReturn( 4, 6 )

    val result1 = sutMock.someMethod( true )
    result1 should be( 4 )

    val result2 = sutMock.someMethod( true )
    result2 should be( 6 )

    Mockito.verify( sutMock, times( 2 ) ).someMethod( true )
  }
  //this test fails with assertion error 
  "mockito verify method with default parameter" should "pass" in {
    val sutMock = mock[SUT]
    Mockito.when( sutMock.someMethodWithDefaultParameter( true ) ).thenReturn( 4, 6 )

    val result1 = sutMock.someMethodWithDefaultParameter( true )
    result1 should be( 4 )

    val result2 = sutMock.someMethodWithDefaultParameter( true )
    result2 should be( 6 )

    Mockito.verify( sutMock, times( 2 ) ).someMethodWithDefaultParameter( true )
  }
}

Пожалуйста, подскажите, что я делаю не так во втором тесте.


Изменить 1: @Som. Найдите трассировку стека для вышеуказанного тестового класса ниже: -

Run starting. Expected test count is: 2
VerifyMethodWithDefaultParameter:
mockito verify method
- should pass
mockito verify method with default parameter
- should pass *** FAILED ***
  org.mockito.exceptions.verification.TooManyActualInvocations: sUT.someMethodWithDefaultParameter$default$2();
Wanted 2 times:
-> at zeither.VerifyMethodWithDefaultParameter$$anonfun$2.apply$mcV$sp(VerifyMethodWithDefaultParameter.scala:37)
But was 3 times. Undesired invocation:
-> at zeither.VerifyMethodWithDefaultParameter$$anonfun$2.apply$mcV$sp(VerifyMethodWithDefaultParameter.scala:34)
  ...
Run completed in 414 milliseconds.
Total number of tests run: 2
Suites: completed 1, aborted 0
Tests: succeeded 1, failed 1, canceled 0, ignored 0, pending 0
*** 1 TEST FAILED ***

Изменить 2: @Mifeet

Как было предложено, если я передаю 0 для параметра int по умолчанию, тест проходит, но ниже тестовый пример не проходит с предложенным aprroach: -

  "mockito verify method with default parameter" should "pass" in {
    val sutMock = mock[SUT]
    Mockito.when( sutMock.someMethodWithDefaultParameter( true, 0 ) ).thenReturn( 14 )
    Mockito.when( sutMock.someMethodWithDefaultParameter( false, 0 ) ).thenReturn( 16 )
    val result1 = sutMock.someMethodWithDefaultParameter( true )
    result1 should be( 14 )

    val result2 = sutMock.someMethodWithDefaultParameter( false )
    result2 should be( 16 )

    Mockito.verify( sutMock, times( 1 ) ).someMethodWithDefaultParameter( true )
    Mockito.verify( sutMock, times( 1 ) ).someMethodWithDefaultParameter( false )
  }

Пожалуйста, найдите ниже stacktrace: -

mockito verify method with default parameter
- should pass *** FAILED ***
  org.mockito.exceptions.verification.TooManyActualInvocations: sUT.someMethodWithDefaultParameter$default$2();
Wanted 1 time:
-> at zeither.VerifyMethodWithDefaultParameter$$anonfun$2.apply$mcV$sp(VerifyMethodWithDefaultParameter.scala:38)
But was 2 times. Undesired invocation:
-> at zeither.VerifyMethodWithDefaultParameter$$anonfun$2.apply$mcV$sp(VerifyMethodWithDefaultParameter.scala:35)
  ...

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


person mogli    schedule 24.04.2016    source источник
comment
Пожалуйста, четко сформулируйте, чего вы ожидаете и что видите.   -  person Som Bhattacharyya    schedule 24.04.2016
comment
Смотрите мой обновленный ответ. Иногда вам просто нужно запачкать руки, прежде чем утонуть в гибких каркасах для чего-то, для чего они не были предназначены.   -  person Mifeet    schedule 26.04.2016


Ответы (1)


Для краткости я буду использовать withDefaultParam() вместо someMethodWithDefaultParameter().

Как параметры по умолчанию преобразуются в байт-код. Чтобы понять, почему тест не проходит, мы должны сначала посмотреть, как методы с параметрами по умолчанию преобразуются в эквивалент / байт-код Java. Ваш метод withDefaultParam() будет переведен на два метода:

  • withDefaultParam - этот метод принимает оба параметра и содержит фактическую реализацию
  • withDefaultParam$default$2 - возвращает значение второго параметра по умолчанию (т.е. i)

Когда вы вызываете, например, withDefaultParam(true), он будет преобразован в вызов withDefaultParam$default$2 для получения значения параметра по умолчанию с последующим вызовом withDefaultParam. Вы можете проверить байт-код ниже.

Что не так с вашим тестом: Mockito жалуется на дополнительный вызов withDefaultParam$default$2. Это потому, что компилятор вставляет дополнительный вызов этого метода прямо перед вашим Mockito.when(...), чтобы заполнить значение по умолчанию. Следовательно, этот метод вызывается трижды, и утверждение times(2) не выполняется.

Как это исправить. Тест будет успешным, если вы инициализируете макет с помощью:

Mockito.when(sutMock.withDefaultParam(true, 0)).thenReturn(4, 6)

Это странно, спросите вы, почему я должен передавать 0 в качестве параметра по умолчанию вместо 5? Оказывается, Mockito издевается и над withDefaultParam$default$2 методом, используя настройку по умолчанию Answers.RETURNS_DEFAULTS. Поскольку 0 является значением по умолчанию для int, все вызовы в вашем коде фактически передают 0 вместо 5 в качестве второго аргумента withDefaultParam().

Как принудительно установить правильное значение по умолчанию для параметра: если вы хотите, чтобы в вашем тесте использовалось 5 в качестве значения по умолчанию, вы можете пройти тест примерно так:

class SUTImpl extends SUT
val sutMock = mock[SUTImpl](Mockito.CALLS_REAL_METHODS)
Mockito.when(sutMock.withDefaultParam(true, 5)).thenReturn(4, 6)

На мой взгляд, именно здесь Mockito перестает быть полезным и становится обузой. В нашей команде мы бы написали собственную тестовую реализацию SUT без Mockito. Это не вызывает каких-либо удивительных ловушек, подобных вышеизложенному, вы можете реализовать собственную логику утверждения и, что наиболее важно, ее можно повторно использовать в тестах.

Обновление - как я могу это решить: Я не думаю, что использование имитирующей библиотеки действительно дает вам какое-либо преимущество в этом случае. Меньше боли кодировать собственный макет. Вот как я бы это сделал:

class SUTMock(results: Map[Boolean, Seq[Int]]) extends SUT {
  private val remainingResults = results.mapValues(_.iterator).view.force // see http://stackoverflow.com/a/14883167 for why we need .view.force

  override def someMethodWithDefaultParameter(bool: Boolean, i: Int): Int = remainingResults(bool).next()

  def assertNoRemainingInvocations() = remainingResults.foreach {
    case (bool, remaining) => assert(remaining.isEmpty, s"remaining invocations for parameter $bool: ${remaining.toTraversable}")
  }
}

Тогда тест мог бы выглядеть так:

"mockito verify method with default parameter" should "pass" in {
    val sutMock = new SUTMock(Map(true -> Seq(14, 15), false -> Seq(16)))
    sutMock.someMethodWithDefaultParameter(true) should be(14)
    sutMock.someMethodWithDefaultParameter(true) should be(15)

    sutMock.someMethodWithDefaultParameter(false) should be(16)

    sutMock.assertNoRemainingInvocations()
  }

Это делает все, что вам нужно - предоставляет требуемые возвращаемые значения, срывается при слишком большом или слишком малом количестве вызовов. Его можно использовать повторно. Это глупый упрощенный пример, но на практике вы должны думать о своей бизнес-логике, а не о вызовах методов. Если бы SUT был имитацией для брокера сообщений, например, вы могли бы использовать метод allMessagesProcessed() вместо assertNoRemainingInvocations() или даже определять более сложные утверждения.


Предположим, у нас есть переменная val sut:SUT, вот байт-код вызова withDefaultParam(true):

ALOAD 1  # load sut on stack
ICONST_1 # load true on stack
ALOAD 1  # load sut on stack
INVOKEINTERFACE SUT.withDefaultParam$default$2 ()I # call method which returns the value of the default parameter and leave result on stack
INVOKEINTERFACE SUT.withDefaultParam (ZI)I         # call the actual implementation
person Mifeet    schedule 24.04.2016