2016-04-24 138 views
1

在下面的代码中,Mockito验证不按预期方式在默认参数的scala方法上正常工作,但可以在没有默认参数的方法上正常工作。Mockito验证失败,并在Scala中使用默认参数“TooManyActualInvocations”

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,但在测试用例下面没有传递给建议的aprr oach: -

"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的高度赞赏,如果能提供这样的情况下很好地解决,如我愿意在我的项目中使用任何模拟库。

+0

请一定要清楚你“期望”和你看到什么。 –

+0

查看我的更新回答。有时候你只需要在弯曲的框架中淹没他们没有设计的东西之前弄脏自己的手。 – Mifeet

回答

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设置。由于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