Skip to content

Improve varargs support #70

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

Merged
merged 3 commits into from
Jan 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,19 @@ aMock.method(n < 5.1) was called
aMock.method(n <= 5) was called
```

## Varargs

Most matchers that makes sense to, work with varargs out of the box, the only thing to notice is that if you are passing more than one value
and want to use `eqTo` then you should pass all of them to the same instance of `eqTo` e.g.

```scala
trait FooWithVarArgAndSecondParameterList {
def bar(bells: String*)(cheese: String): String
}

foo.bar(eqTo("cow", "blue"))(*) was called //RIGHT
foo.bar(eqTo("cow"), eqTo("blue"))(*) was called //WRONG - it will complain it was expecting 2 matchers but got 3
```

## Scalactic integration

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
package org.mockito.matchers

import org.mockito.{ArgumentMatcher, ArgumentMatchers => JavaMatchers}

private[mockito] trait FunctionMatchers {

def function0[T](value: T): () => T =
JavaMatchers.argThat(new ArgumentMatcher[() => T] {

override def matches(argument: () => T): Boolean = argument() == value

override def toString: String = s"() => $value"
})
def function0[T](value: T): () => T = ThatMatchers.argThat((f: () => T) => f() == value, s"() => $value")

}
68 changes: 32 additions & 36 deletions common/src/main/scala/org/mockito/matchers/NumericMatchers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,54 +18,50 @@ class N {
import ThatMatchers.argThat

/**
* Creates a matcher that works only if there is a Numeric[T] associated with the type, this allows you to write stuff like
*
* aMock.pepe(4.1)
* aMock.pepe(n > 4) was called
*
*/
* Creates a matcher that works only if there is a Numeric[T] associated with the type, this allows you to write stuff like
*
* aMock.pepe(4.1)
* aMock.pepe(n > 4) was called
*
*/
def >[N: Numeric](n: N): N = argThat[N](new NumericMatcher(n, ">", _ > _))

/**
* Creates a matcher that works only if there is a Numeric[T] associated with the type, this allows you to write stuff like
*
* aMock.pepe(4)
* aMock.pepe(n >= 4) was called
*
*/
* Creates a matcher that works only if there is a Numeric[T] associated with the type, this allows you to write stuff like
*
* aMock.pepe(4)
* aMock.pepe(n >= 4) was called
*
*/
def >=[N: Numeric](n: N): N = argThat[N](new NumericMatcher(n, ">=", _ >= _))

/**
* Creates a matcher that works only if there is a Numeric[T] associated with the type, this allows you to write stuff like
*
* aMock.pepe(3.1)
* aMock.pepe(n < 4) was called
*
*/
* Creates a matcher that works only if there is a Numeric[T] associated with the type, this allows you to write stuff like
*
* aMock.pepe(3.1)
* aMock.pepe(n < 4) was called
*
*/
def <[N: Numeric](n: N): N = argThat[N](new NumericMatcher(n, "<", _ < _))

/**
* Creates a matcher that works only if there is a Numeric[T] associated with the type, this allows you to write stuff like
*
* aMock.pepe(4)
* aMock.pepe(n <= 4) was called
*
*/
* Creates a matcher that works only if there is a Numeric[T] associated with the type, this allows you to write stuff like
*
* aMock.pepe(4)
* aMock.pepe(n <= 4) was called
*
*/
def <=[N: Numeric](n: N): N = argThat[N](new NumericMatcher(n, "<=", _ <= _))

/**
* Creates a matcher that delegates on {{org.scalactic.TripleEqualsSupport.Spread}} so you can get around the lack of
* precision on floating points, e.g.
*
* aMock.barDouble(4.999)
* verify(aMock).barDouble(=~(5.0 +- 0.001))
*
*/
def =~[T](spread: Spread[T]): T =
ThatMatchers.argThat(new ArgumentMatcher[T] {
override def matches(v: T): Boolean = spread.isWithin(v)
override def toString: String = s"=~($spread)"
})
* Creates a matcher that delegates on {{org.scalactic.TripleEqualsSupport.Spread}} so you can get around the lack of
* precision on floating points, e.g.
*
* aMock.barDouble(4.999)
* verify(aMock).barDouble(=~(5.0 +- 0.001))
*
*/
def =~[T](spread: Spread[T]): T = ThatMatchers.argThat[T](spread.isWithin _, s"=~($spread)")
}

private[mockito] trait NumericMatchers extends Tolerance {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
package org.mockito.matchers

import org.mockito.{ArgumentMatchers => JavaMatchers}

private[mockito] trait StringThatMatchers {

import ThatMatchers.argThat

/**
* Delegates to <code>ArgumentMatchers.matches()</code>, it's only here so we expose all the `ArgumentMatchers`
* on a single place
*
*/
def matches(regex: String): String = JavaMatchers.matches(regex)
def matches(regex: String): String = argThat((s: String) => s.matches(regex), s"matches($regex)")

/**
* Delegates to <code>ArgumentMatchers.startsWith()</code>, it's only here so we expose all the `ArgumentMatchers`
* on a single place
*
*/
def startsWith(prefix: String): String = JavaMatchers.startsWith(prefix)
def startsWith(prefix: String): String = argThat((s: String) => s.startsWith(prefix), s"startsWith($prefix)")

/**
* Delegates to <code>ArgumentMatchers.contains()</code>, it's only here so we expose all the `ArgumentMatchers`
* on a single place
*
*/
def contains(substring: String): String = JavaMatchers.contains(substring)
def contains(substring: String): String = argThat((s: String) => s.contains(substring), s"contains($substring)")

/**
* Delegates to <code>ArgumentMatchers.endsWith()</code>, it's only here so we expose all the `ArgumentMatchers`
* on a single place
*
*/
def endsWith(suffix: String): String = JavaMatchers.endsWith(suffix)
def endsWith(suffix: String): String = argThat((s: String) => s.endsWith(suffix), s"endsWith($suffix)")
}
25 changes: 19 additions & 6 deletions common/src/main/scala/org/mockito/matchers/ThatMatchers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,17 @@ private[mockito] trait ThatMatchers {
* on a single place
*
*/
def argThat[T](matcher: ArgumentMatcher[T]): T = JavaMatchers.argThat(matcher)
def argThat[T](matcher: ArgumentMatcher[T]): T = argThat(matcher.matches, matcher.toString)

/*
* Overloaded version to avoid having to instantiate a matcher without using SAM to keep it compatible with 2.11,
* It also adds support for varargs out of the box
*/
def argThat[T](f: T => Boolean, desc: => String = "argThat(<condition>)"): T =
JavaMatchers.argThat(new VarargAwareArgumentMatcher[T] {
override def doesMatch(argument: T): Boolean = f(argument)
override def toString: String = desc
})

/**
* Delegates the call to <code>argThat</code> but using the Scala "primitives", this
Expand Down Expand Up @@ -75,11 +85,14 @@ private[mockito] trait ThatMatchers {
*/
def longThat(matcher: ArgumentMatcher[Long]): Long = argThat(matcher)

def argMatching[T](pf: PartialFunction[Any, Unit]) =
argThat[T](new ArgumentMatcher[T] {
override def matches(argument: T): Boolean = pf.isDefinedAt(argument)
override def toString: String = "argMatching(...)"
})
/**
* Creates a matcher that delegates on a partial function to enable syntax like
*
* foo.bar(argMatching({ case Baz(n, _) if n > 90 => })) shouldReturn "mocked!"
* foo.bar(argMatching({ case Baz(_, "pepe") => })) was called
*
*/
def argMatching[T](pf: PartialFunction[Any, Unit]) = argThat[T](pf.isDefinedAt(_), "argMatching(...)")
}

private[mockito] object ThatMatchers extends ThatMatchers
13 changes: 13 additions & 0 deletions common/src/main/scala/org/mockito/matchers/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.mockito
import scala.collection.mutable

package object matchers {
trait VarargAwareArgumentMatcher[T] extends ArgumentMatcher[T] {
override def matches(argument: T): Boolean = argument match {
case a: mutable.WrappedArray[_] if a.length == 1 => doesMatch(a.head.asInstanceOf[T])
case other => doesMatch(other)
}

def doesMatch(argument: T): Boolean
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ trait EqMatchers_211 {
* Creates a matcher that delegates on {{org.scalactic.Equality}} so you can always customise how the values are compared
* Also works with value classes
*/
def eqTo[T](value: T)(implicit eq: Equality[T]): T = macro MacroMatchers_211.eqToMatcher[T]
def eqTo[T](value: T, others: T*)(implicit eq: Equality[T]): T = macro MacroMatchers_211.eqToMatcher[T]

/**
* It was intended to be used instead of eqTo when the argument is a value class,
Expand Down
33 changes: 21 additions & 12 deletions core/src/main/scala-2.12/org/mockito/matchers/EqMatchers_212.scala
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
package org.mockito.matchers

import org.mockito.ArgumentMatcher
import org.mockito.internal.ValueClassExtractor
import org.mockito.{ArgumentMatcher, ArgumentMatchers => JavaMatchers}
import org.scalactic.Equality

import scala.collection.mutable

trait EqMatchers_212 {

/**
* Creates a matcher that delegates on {{org.scalactic.Equality}} so you can always customise how the values are compared
* Also works with value classes
*/
def eqTo[T](value: T)(implicit $eq: Equality[T], $vce: ValueClassExtractor[T]): T = {
val extractedValue = $vce.extract(value)
ThatMatchers.argThat(new ArgumentMatcher[T] {
override def matches(v: T): Boolean = $eq.areEqual(extractedValue.asInstanceOf[T], v)
override def toString: String = s"eqTo($value)"
* Creates a matcher that delegates on {{org.scalactic.Equality}} so you can always customise how the values are compared
* Also works with value classes
*/
def eqTo[T](value: T, others: T*)(implicit $eq: Equality[T], $vce: ValueClassExtractor[T]): T = {
val rawValues: Seq[T] = Seq(value) ++ others
JavaMatchers.argThat(new ArgumentMatcher[T] {
override def matches(v: T): Boolean = v match {
case a: mutable.WrappedArray[_] if rawValues.length == a.length =>
(rawValues zip a) forall {
case (expected, got) => $eq.areEqual(expected.asInstanceOf[T], got)
}
case other =>
$eq.areEqual($vce.extract(value).asInstanceOf[T], other)
}
override def toString: String = s"eqTo(${rawValues.mkString(", ")})"
})
value
}

/**
* It was intended to be used instead of eqTo when the argument is a value class,
* but eqTo now supports value classes so it is not needed anymore
*/
* It was intended to be used instead of eqTo when the argument is a value class,
* but eqTo now supports value classes so it is not needed anymore
*/
@deprecated("Use 'eqTo' instead", since = "1.0.2")
def eqToVal[T](value: T)(implicit $eq: Equality[T], $vce: ValueClassExtractor[T]): T = eqTo(value)
}
73 changes: 73 additions & 0 deletions core/src/test/scala/user/org/mockito/IdiomaticMockitoTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,26 @@ class IdiomaticMockitoTest extends WordSpec with Matchers with IdiomaticMockito
mock2.iHaveDefaultArgs() was called
}
}

"work with varargs" in {
val foo = mock[FooWithVarArg]

foo.bar("cow", "blue")
foo.bar("cow", "blue") was called

foo.bar("cow")
foo.bar("cow") was called
}

"work with varargs (value class)" in {
val foo = mock[ValueClassWithVarArg]

foo.bar(Bread("Baguette"), Bread("Arepa"))
foo.bar(Bread("Baguette"), Bread("Arepa")) was called

foo.bar(Bread("Baguette"))
foo.bar(Bread("Baguette")) was called
}
}

"mix arguments and raw parameters" should {
Expand All @@ -477,6 +497,59 @@ class IdiomaticMockitoTest extends WordSpec with Matchers with IdiomaticMockito
org.doSomethingWithThisIntAndString(1, "meh") shouldBe "mocked!"
org.doSomethingWithThisIntAndString(1, eqTo("meh")) was called
}

"work with multiple param list" in {
val foo = mock[FooWithSecondParameterList]
val cheese = Cheese("Gouda")

foo.bar("cow")(cheese)

foo.bar("cow")(cheese) was called
foo.bar("cow")(*) was called
}

"work with varargs and multiple param lists" in {
val foo = mock[FooWithVarArgAndSecondParameterList]
val cheese = Cheese("Gouda")

foo.bar("cow")(cheese)
foo.bar("cow")(cheese) was called
foo.bar("cow")(*) was called

foo.bar(endsWith("w"))(*) was called
foo.bar(startsWith("c"))(*) was called
foo.bar(contains("ow"))(*) was called
foo.bar(argMatching({ case "cow" => }))(*) was called
foo.bar(argThat((v: String) => v == "cow", "some desc"))(*) was called

foo.bar("cow", "blue")(cheese)
foo.bar("cow", "blue")(cheese) was called
foo.bar(eqTo("cow", "blue"))(*) was called
foo.bar(*)(*) wasCalled twice
}

"work with multiple param list (value class)" in {
val foo = mock[ValueClassWithSecondParameterList]
val cheese = Cheese("Gouda")

foo.bar(Bread("Baguette"))(cheese)

foo.bar(Bread("Baguette"))(cheese) was called
foo.bar(Bread("Baguette"))(*) was called
}

"work with varargs and multiple param lists (value class)" in {
val foo = mock[ValueClassWithVarArgAndSecondParameterList]
val cheese = Cheese("Gouda")

foo.bar(Bread("Baguette"))(cheese)
foo.bar(Bread("Baguette"))(cheese) was called
foo.bar(Bread("Baguette"))(*) was called

foo.bar(Bread("Baguette"), Bread("Arepa"))(cheese)
foo.bar(Bread("Baguette"), Bread("Arepa"))(cheese) was called
foo.bar(eqTo(Bread("Baguette"), Bread("Arepa")))(*) was called
}
}

"value class matchers" should {
Expand Down
26 changes: 26 additions & 0 deletions core/src/test/scala/user/org/mockito/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package user.org

package object mockito {
case class Bread(name: String) extends AnyVal
case class Cheese(name: String)

trait FooWithVarArg {
def bar(bells: String*)
}
trait FooWithSecondParameterList {
def bar(bell: String)(cheese: Cheese)
}
trait FooWithVarArgAndSecondParameterList {
def bar(bells: String*)(cheese: Cheese)
}

trait ValueClassWithVarArg {
def bar(bread: Bread*)
}
trait ValueClassWithSecondParameterList {
def bar(bread: Bread)(cheese: Cheese)
}
trait ValueClassWithVarArgAndSecondParameterList {
def bar(breads: Bread*)(cheese: Cheese)
}
}
Loading