Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issues mocking curried method with variable argument parameter #131

Open
morgen-peschke opened this issue Jan 30, 2016 · 12 comments
Open

Issues mocking curried method with variable argument parameter #131

morgen-peschke opened this issue Jan 30, 2016 · 12 comments

Comments

@morgen-peschke
Copy link

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?

@carstenlenz
Copy link

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

@barkhorn
Copy link
Collaborator

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

@HDBandit
Copy link

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

@barkhorn barkhorn added this to the v3.6.0 milestone Jan 22, 2017
@barkhorn barkhorn removed the triage label Jan 22, 2017
@barkhorn
Copy link
Collaborator

barkhorn commented Mar 5, 2017

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

@GMadorell
Copy link

I'm personally having trouble mocking a method as such

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

@barkhorn barkhorn modified the milestones: v3.7.0, v3.6.0 May 6, 2017
@barkhorn barkhorn added the ready label Aug 13, 2017
@barkhorn barkhorn modified the milestones: v4.0.0, v4.1.0 Oct 29, 2017
barkhorn added a commit to barkhorn/ScalaMock that referenced this issue Dec 30, 2017
@barkhorn barkhorn mentioned this issue Dec 30, 2017
3 tasks
@barkhorn barkhorn modified the milestones: v4.1.0, v4.0.0 Dec 30, 2017
@barkhorn
Copy link
Collaborator

sorry, closed accidentally

@barkhorn barkhorn reopened this Dec 30, 2017
@barkhorn barkhorn modified the milestones: v4.0.0, v4.1.0 Dec 30, 2017
@barkhorn
Copy link
Collaborator

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

@fpopic
Copy link

fpopic commented Jan 25, 2018

@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.

?

@barkhorn
Copy link
Collaborator

barkhorn commented Jan 27, 2018

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 barkhorn removed this from the v4.1.0 milestone Feb 17, 2018
@barkhorn barkhorn added this to the v4.2.0 milestone Feb 17, 2018
@barkhorn barkhorn modified the milestones: v4.2.0, v4.3.0 Apr 21, 2019
@barkhorn barkhorn removed the ready label Apr 21, 2019
@barkhorn barkhorn removed this from the v4.3.0 milestone Jun 23, 2019
@jcavalieri
Copy link

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?

@goshacodes
Copy link
Contributor

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
Copy link
Contributor

goshacodes commented Feb 25, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants