ScalaMock icon indicating copy to clipboard operation
ScalaMock copied to clipboard

Issues mocking curried method with variable argument parameter

Open morgen-peschke opened this issue 9 years ago • 12 comments

The documentation is unclear about how this interaction should be handled, and I can't seem to find a combination that works.

Given this trait:

trait DataStore {
   def insertAt(timestamp: Long)(elements: String*): Unit
}

And this test suite:

package bugReport.scalamock

import org.scalatest.{ Matchers, WordSpec }
import org.scalamock.scalatest.MockFactory

class CurriedVarArgSpec extends WordSpec with Matchers with MockFactory {

  "Mocking DataStore" should {
    "be able to handled curried varargs" in {
      val dataStore = mock[DataStore]
      val timestamp = 40L

      // Mock call to insertAt here

      dataStore.insertAt(timestamp)("TestString")
    }
  }
}

I expected that mocking insertAt would be fairly straightforward.

(dataStore.insertAt(_: Long)(_:String*)).expects(timestamp, "TestString")

However, this did not compile.

[error] .../bugReport/src/test/scala/scalamock/CurriedVarArgSpec.scala:14: ')' expected but identifier found.
[error]       (dataStore.insertAt(_: Long)(_:String*)).expects(timestamp, "TestString")
[error]                                            ^
[error] .../bugReport/src/test/scala/scalamock/CurriedVarArgSpec.scala:17: ')' expected but '}' found.
[error]     }
[error]     ^
[error] two errors found

Removing the offending * didn't match the signature, but did compile.

(dataStore.insertAt(_: Long)(_:String)).expects(timestamp, "TestString")

Unfortunately, this caused the test to fail.

[info] - should be able to handled curried varargs *** FAILED ***
[info]   Unexpected call: insertAt(40, WrappedArray(TestString))
[info]
[info]   Expected:
[info]   inAnyOrder {
[info]     insertAt(40, TestString) once (never called - UNSATISFIED)
[info]   }
[info]
[info]   Actual:
[info]     insertAt(40, WrappedArray(TestString)) (Option.scala:121)

Attempting to modify the mock to expect a WrappedArray was the next attempt.

import scala.collection.mutable.WrappedArray
(dataStore.insertAt(_: Long)(_: WrappedArray[String])).expects(timestamp, "TestString")

This took me back to a compilation error, which did confirm that String was the appropriate type for that mock parameter.

[error] .../bugReport/src/test/scala/scalamock/CurriedVarArgSpec.scala:38: type mismatch;
[error]  found   : scala.collection.mutable.WrappedArray[String]
[error]  required: String
[error]       (dataStore.insertAt(_: Long)(_: WrappedArray[String])).expects(timestamp, "TestString")
[error]                                     ^
[error] .../bugReport/src/test/scala/scalamock/CurriedVarArgSpec.scala:38: value expects is not a member of (Long, scala.collection.mutable.WrappedArray[String]) => Unit
[error]       (dataStore.insertAt(_: Long)(_: WrappedArray[String])).expects(timestamp, "TestString")
[error]                                                              ^
[error] two errors found

One final stab it the dark was to keep the String in the signature, but expect an Array (WrappedArray cannot be instantiated directly)

(dataStore.insertAt(_: Long)(_: String)).expects(timestamp, Array("TestString"))

This also does not compile.

[error] .../bugReport/src/test/scala/scalamock/CurriedVarArgSpec.scala:49: type mismatch;
[error]  found   : Array[String]
[error]  required: org.scalamock.MockParameter[String]
[error]       (dataStore.insertAt(_: Long)(_: String)).expects(timestamp, Array("TestString"))
[error]                                                                        ^
[error] one error found

So, at this point, I'm stuck. Can this be mocked?

morgen-peschke avatar Jan 30 '16 22:01 morgen-peschke

I'm seeing the same behaviour here. In my case the repeated parameter is in the first parameter list and the second parameter list is an implicit parameter list.

Example:

trait TypeClass[T]

object TypeClass {
  implicit object StringTypeClass extends TypeClass[String]
}

class ClassUnderTest {
  def mydef[T: TypeClass](a: String, b: String*): String = "result"
}

class TestBase extends FlatSpec with Matchers with MockFactory {
  "repeated params" should "work" in {
    val m = mock[ClassUnderTest]

    (m.mydef(_: String, _: String)(_: TypeClass[String])).expects("a", "b", *).returning("mocked result")

    tm.mydef("a", "b")
  }

}

The above compiles but the test fails (correctly) because a Seq("b") is actually passed. Workaround (very ugly) by explicitly defining the parameter and exploiting type erasure:

    val bParam = new MockParameter[Seq[String]](Seq("b"))

    (t.mydef(_: String, _: String)(_: TypeClass[String])).expects("a", bParam.asInstanceOf[MockParameter[String]], *).returning("doof")

carstenlenz avatar Sep 13 '16 16:09 carstenlenz

that is an interesting one. I experimented a bit myself:

  test("varargs") {
    trait Foo {
      def foo(what: String*): String
      def bar(i: Int)(what: String*): String
    }

    val m = mock[Foo]

    m.foo _ expects Seq("a", "b" ) returning "bar" once()
    (m.bar(_: Int)(_: (String*))) expects (42, "1") returning "baz" once()

    m.foo("a", "b") === "bar"
    m.bar(42)("1", "2") === "baz"
  }

but that ends in a compiler error:

Error:(47, 25) no * parameter type allowed here
    (m.bar (_: Int)(_: (String*))) expects (42, "1") returning "baz" once()

barkhorn avatar Nov 30 '16 21:11 barkhorn

We are looking forward for the solution of this bug. Thank you so much.

HDBandit avatar Dec 21 '16 09:12 HDBandit

If def bar(i: Int)(what: Any*): String is mocked with (m.bar(_: Int)(_: Seq[_])) expects (42, Seq("1")) returning "baz" once() everything works fine, but not with String* as a type

barkhorn avatar Mar 05 '17 16:03 barkhorn

I'm personally having trouble mocking a method as such

def someMethod(normalEasyToMockArgument: String, variableArgument: (String, Any)*): Unit

GMadorell avatar Apr 10 '17 14:04 GMadorell

sorry, closed accidentally

barkhorn avatar Dec 30 '17 00:12 barkhorn

Maybe this workaround is helpful in the meantime:

  "Varargs in 2nd param list" should "be mockable" in {
    trait TypeToMock {
      def bar(i: Int)(what: String*): String
    }

    trait Workaround extends TypeToMock {
      def bar(i: Int)(what: Any*): String
    }

    val m = mock[Workaround]
    (m.bar(_: Int)(_: Seq[_])) expects (42, Seq("1", "2")) returning "baz" once()
    m.bar(42)("1", "2") === "baz"
  }

barkhorn avatar Dec 30 '17 01:12 barkhorn

@barkhorn Any idea how to mock or create a workaround for this:

https://github.com/aerospike/aerospike-client-java/blob/master/client/src/com/aerospike/client/IAerospikeClient.java#L401-L453

I just have to mock the first get() method that doesn't have any varargs string parameter, but some overloaded methods of it do have which I am not using but the problem remains.

val aerospikeClient: IAerospikeClient = mock[IAerospikeClient]

(aerospikeClient.get: (Policy, Key) => Record)
      .expects(new Policy(), new Key())
      .returns(record)
      .once() 

With Mockito it works.

?

fpopic avatar Jan 25 '18 12:01 fpopic

Shortened the interface a bit to not depend on any external stuff:

public interface IAerospikeClient {
    public Record get(Policy policy, Key key) throws AerospikeException;
    public Record get(Policy policy, Key key, String... binNames) throws AerospikeException;
}

Testcase:

import VarArgsWorkaround.Overloaded
import iaero.{IAerospikeClient, Key, Policy, Record}
import org.scalamock.function.MockFunction2
import org.scalamock.scalatest.MockFactory
import org.scalatest.{FunSuite, Matchers}

class VarArgsWorkaround extends FunSuite with MockFactory with Matchers {
  test("workaround") {
    val record = mock[Record]
    val policy = mock[Policy]
    val key = mock[Key]
    
    val mockGet = mockFunction[Policy, Key, Record]
    mockGet expects (policy, key) returns record once()
    
    val mockClient = new Overloaded(mockGet)
    mockClient.get(policy, key) should be (record)
  }
}

object VarArgsWorkaround {
  class Overloaded(mf: MockFunction2[Policy, Key, Record]) extends IAerospikeClient {
    override def get(policy: Policy, key: Key): Record = mf(policy, key)
    override def get(policy: Policy, key: Key, binNames: String*): Record = ???
  }
}

probably need to create Record, Key, Policy differently. I stubbed them out to illustrate the pattern.

Note: Any further requests/discussions on workarounds needs to be StackOverflow please.

barkhorn avatar Jan 27 '18 10:01 barkhorn

This thread seemed to go off the rails a bit. I'm a little confused. Is there still no way to use ScalaMock with String* matching?

jcavalieri avatar Feb 19 '24 19:02 jcavalieri

This works fine with 6.0.0-M1 and scala 3 since there is one parameter list and type is inferred.

        trait DataStore {
          def insertAt(timestamp: Long, elements: String*): Unit
        }

        val dataStore = mock[DataStore]
        val timestamp = 40L


        (dataStore.insertAt _).expects(*, Seq("TestString", "TestString2")).returns(())

        dataStore.insertAt(timestamp, "TestString", "TestString2")
      }

goshacodes avatar Feb 25 '24 11:02 goshacodes

I'll search for solution with scala 3. At first glance it can be done. But not really simple

goshacodes avatar Feb 25 '24 12:02 goshacodes