Mockito 验证失败,"TooManyActualInvocations" for 在 Scala 中使用默认参数的方法



在下面的代码中,Mockito 验证在具有默认参数的 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

按照建议,

如果我为默认 int 参数通过 0 测试通过,但下面的测试用例没有通过建议的 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 )
  }

请在下面找到堆栈跟踪:-

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)的意见表示高度赞赏,如果它们可以为这种情况提供简洁的解决方案,因为我愿意在我的项目中使用任何模拟库。

为简洁起见,我将使用 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也使用默认的Answers.RETURNS_DEFAULTS设置来模拟withDefaultParam$default$2方法。因为 0int 的默认值,所以代码中的所有调用实际上都传递 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不再有用并成为负担的地方。我们在团队中要做的是编写一个没有 Mockito 的SUT的自定义测试实现。它不会像上面那样造成任何令人惊讶的陷阱,您可以实现自定义断言逻辑,最重要的是,它可以在测试中重用。

更新 - 我将如何解决它:我认为在这种情况下使用模拟库不会真正给您带来任何优势。编写自己的模拟代码可以减少痛苦。这就是我会怎么做:

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

相关内容

  • 没有找到相关文章