在下面的代码中,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 请在下面的测试类中找到以下stacktrace: -
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 )
}
请在下面找到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)的看法非常受欢迎,如果他们可以为这种情况提供一个简洁的解决方案,因为我可以在我的项目中使用任何模拟库。
答案 0 :(得分: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
作为{{}的第二个参数传递1}}。
如何强制参数的正确默认值:如果您希望测试使用withDefaultParam()
作为默认值,您可以使用以下内容进行测试:
5
在我看来,这正是Mockito停止使用并成为负担的地方。我们在团队中要做的是在没有Mockito的情况下编写class SUTImpl extends SUT
val sutMock = mock[SUTImpl](Mockito.CALLS_REAL_METHODS)
Mockito.when(sutMock.withDefaultParam(true, 5)).thenReturn(4, 6)
的自定义测试实现。它不会导致任何令人惊讶的陷阱,如上所述,您可以实现自定义断言逻辑,最重要的是,它可以在测试中重用。
更新 - 我将如何解决它:我不认为使用模拟库在这种情况下确实给你带来任何好处。对自己的模拟进行编码不那么痛苦。我就是这样做的:
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}")
}
}
这就是你所需要的 - 提供所需的返回值,在太多或太少的调用中爆炸。它可以重复使用。这是一个愚蠢的简化示例,但在实际场景中,您应该考虑业务逻辑而不是方法调用。如果SUT是消息代理的模拟,例如,您可以使用方法"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()
}
而不是allMessagesProcessed()
,甚至可以定义更复杂的断言。
假设我们有一个变量assertNoRemainingInvocations()
,这里是调用val sut:SUT
的字节码:
withDefaultParam(true)