4

In below code, Mockito verify doesn't works as expected on scala methods with default parameter but works fine on methods with no default parameters.

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 )
  }
}

Please suggest, what i am doing wrong in the second test.


Edit 1: @Som Please find below stacktrace for above test class :-

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 ***

Edit 2 : @Mifeet

As suggested, if i pass 0 for default int parameter test passes, but below test case is not passing with the suggested 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 )
  }

Please find below 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)
  ...

Your opinion on other existing mocking libraries like PowerMock, ScalaMock is highly appreciated, if they can provide a neat solution for such cases, as i am open to use any mocking library in my project.

Mifeet
  • 12,949
  • 5
  • 60
  • 108
mogli
  • 1,549
  • 4
  • 29
  • 57

1 Answers1

4

For brevity, I will use withDefaultParam() instead of someMethodWithDefaultParameter().

How default parameters are translated to bytecode: To understand why the test fails, we must first look at how methods with default parameters are translated to Java equivalent/bytecode. Your method withDefaultParam() will be translated to two methods:

  • withDefaultParam - this method accepts both parameters and does contains the actual implementation
  • withDefaultParam$default$2 - returns the default value of the second parameter (i.e. i)

When you call, e.g., withDefaultParam(true), it will be translated to invocation of withDefaultParam$default$2 to get the default parameter value, followed by an invocation of withDefaultParam. You can check out the bytecode below.

What's wrong with your test: What Mockito is complaining about is an extra invocation of withDefaultParam$default$2. This is because the compiler inserts an extra call to this method right before your Mockito.when(...) to fill in the default. Therefore this method is invoked three times and times(2) assertion fails.

How to fix it: Your test will pass if you initialize your mock with:

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

This is strange, you may ask, why should I pass 0 for the default parameter instead of 5? It turns out that Mockito mocks the withDefaultParam$default$2 method too using the default Answers.RETURNS_DEFAULTS setting. Because 0 is the default value for int, all invocations in your code actually pass 0 instead of 5 as the second argument to withDefaultParam().

How to force the correct default value for parameter: If you wanted your test to use 5 as the default value, you could make your test pass with something like this:

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

In my opinion, though, this is exactly where Mockito stops being useful and becomes a burden. What we would do in our team is write a custom test implementation of SUT without Mockito. It doesn't cause any surprising pitfalls like above, you can implement custom assertion logic, and, most importantly, it can be reused among tests.

Update - how I would solve it: I don't think using a mocking library really gives you any advantage in this case. It's less pain to code your own mock. This is how I would go about it:

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}")
  }
}

A test could then look like this:

"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()
  }

This does all you need - provides required return values, blows up on too many or too few invocations. It can be reused. This is a stupid simplified example, but in a practical scenario, you should think about your business logic rather than method invocations. If SUT was a mock for message broker, e.g., you could have a method allMessagesProcessed() instead of assertNoRemainingInvocations(), or even define more complex asserts.


Assume we have a variable val sut:SUT, here is the bytecode of calling 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
Mifeet
  • 12,949
  • 5
  • 60
  • 108