2016-04-24 140 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