Skip to content

Commit

Permalink
Move matchers from cats into common, add smart constructors (#260) [c…
Browse files Browse the repository at this point in the history
…i skip]

* Smart constructor for EqTo that returns ArgumentMatcher.

A subtype won't get picked up by the cats syntax machinery.

* Move matchers from cats into common and add tests.
  • Loading branch information
dangerousben authored Jul 9, 2020
1 parent 50bad8b commit febdec6
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 26 deletions.
15 changes: 10 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ lazy val commonSettings =
),
scalacOptions ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, v)) if v <= 12 =>
case Some((2, 11)) =>
Seq("-Xsource:2.12", "-Ypartial-unification")
case Some((2, 12)) =>
Seq("-Ypartial-unification")
case _ =>
Nil
Expand Down Expand Up @@ -86,7 +88,7 @@ lazy val specs2 = (project in file("specs2"))

lazy val cats = (project in file("cats"))
.dependsOn(core)
.dependsOn(common % "compile-internal, test-internal")
.dependsOn(common % "compile-internal, test-internal, test->test")
.dependsOn(macroSub % "compile-internal, test-internal")
.settings(
name := "mockito-scala-cats",
Expand Down Expand Up @@ -118,8 +120,11 @@ lazy val common = (project in file("common"))
.dependsOn(macroCommon)
.settings(
commonSettings,
libraryDependencies ++= Dependencies.commonLibraries,
libraryDependencies += Dependencies.scalaReflection(scalaVersion.value),
libraryDependencies ++= Dependencies.commonLibraries ++ Seq(
Dependencies.scalaReflection(scalaVersion.value),
Dependencies.catsLaws % "test",
Dependencies.scalacheck % "test"
),
publish := {},
publishLocal := {},
publishArtifact := false
Expand Down Expand Up @@ -186,4 +191,4 @@ lazy val root = (project in file("."))
.settings(
publish := {},
publishLocal := {}
) aggregate (core, scalatest, specs2, cats, scalaz)
) aggregate (common, core, scalatest, specs2, cats, scalaz)
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,16 @@ package org.mockito.cats

import cats._
import org.mockito.ArgumentMatcher
import org.mockito.internal.matchers.And

object AnyArgumentMatcher extends ArgumentMatcher[Any] {
override def matches(a: Any) = true
}

case class MappedArgumentMatcher[A, B](fa: ArgumentMatcher[A], f: B => A) extends ArgumentMatcher[B] {
override def matches(b: B) = fa.matches(f(b))
}

case class ProductArgumentMatcher[A, B](fa: ArgumentMatcher[A], fb: ArgumentMatcher[B]) extends ArgumentMatcher[(A, B)] {
override def matches(ab: (A, B)) = ab match { case (a, b) => fa.matches(a) && fb.matches(b) }
}
import org.mockito.matchers._

trait ArgumentMatcherInstances {
implicit val argumentMatcherInstance: ContravariantMonoidal[ArgumentMatcher] with MonoidK[ArgumentMatcher] =
new ContravariantMonoidal[ArgumentMatcher] with MonoidK[ArgumentMatcher] {
override def unit = narrow(AnyArgumentMatcher)
override def empty[A] = narrow(AnyArgumentMatcher)
override def contramap[A, B](fa: ArgumentMatcher[A])(f: B => A) = MappedArgumentMatcher(fa, f)
override def product[A, B](fa: ArgumentMatcher[A], fb: ArgumentMatcher[B]) = ProductArgumentMatcher(fa, fb)
override def combineK[A](x: ArgumentMatcher[A], y: ArgumentMatcher[A]) = new And(x, y).asInstanceOf[ArgumentMatcher[A]]
override def unit = narrow(AnyArg)
override def empty[A] = narrow(AnyArg)
override def contramap[A, B](fa: ArgumentMatcher[A])(f: B => A) = Transformed(fa)(f)
override def product[A, B](fa: ArgumentMatcher[A], fb: ArgumentMatcher[B]) = ProductOf(fa, fb)
override def combineK[A](x: ArgumentMatcher[A], y: ArgumentMatcher[A]) = AllOf(x, y)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ import cats.laws.discipline.arbitrary._
import cats.laws.discipline.eq._
import org.mockito.{ ArgumentMatcher, ArgumentMatchers, ArgumentMatchersSugar, IdiomaticMockito }
import org.mockito.internal.matchers._
import org.mockito.matchers.{ EqTo, Generators }
import org.scalacheck.Arbitrary
import org.scalatest.matchers.should.Matchers
import org.scalatest.funsuite.AnyFunSuiteLike
import org.scalatest.prop.Configuration
import org.typelevel.discipline.scalatest.FunSuiteDiscipline

class ArgumentMatcherInstancesTest extends AnyFunSuiteLike with FunSuiteDiscipline with Configuration with ArgumentMatchersSugar with IdiomaticMockito with Matchers {
import Generators._

implicit def eqArgumentMatcherExhaustive[A: ExhaustiveCheck]: Eq[ArgumentMatcher[A]] =
Eq.instance((f, g) => ExhaustiveCheck[A].allValues.forall(a => f.matches(a) == g.matches(a)))

implicit def arbArgumentMatcher[A](implicit a: Arbitrary[A => Boolean]): Arbitrary[ArgumentMatcher[A]] =
Arbitrary(a.arbitrary.map(p => new ArgumentMatcher[A] { def matches(a: A) = p(a) }))

checkAll("ArgumentMatcher[MiniInt]", ContravariantMonoidalTests[ArgumentMatcher].contravariantMonoidal[MiniInt, MiniInt, MiniInt])
checkAll("ArgumentMatcher[MiniInt]", MonoidKTests[ArgumentMatcher].monoidK[MiniInt])

Expand Down Expand Up @@ -76,4 +76,13 @@ class ArgumentMatcherInstancesTest extends AnyFunSuiteLike with FunSuiteDiscipli

aMock.returnsOptionString("prefix-middle-suffix") shouldBe Some("mocked!")
}

test("EqTo works with cats syntax") {
val aMock = mock[Foo]

val matcher = (EqTo("foo"), EqTo(new Integer(42))).tupled
aMock.takesTuple(argThat(matcher)) returns "mocked!"

aMock.takesTuple("foo", 42) shouldBe "mocked!"
}
}
23 changes: 23 additions & 0 deletions common/src/main/scala/org/mockito/matchers/AllOf.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.mockito
package matchers

/** Combine multiple matchers using AND
*/
case class AllOf[A] private (matchers: List[ArgumentMatcher[A]]) extends ArgumentMatcher[A] {
override def matches(a: A) = matchers.forall(_.matches(a))

override def toString =
matchers match {
case Nil => "<any>"
case matcher :: Nil => matcher.toString
case _ => matchers.mkString("allOf(", ", ", ")")
}
}

object AllOf {
def apply[A](matchers: ArgumentMatcher[A]*): ArgumentMatcher[A] =
new AllOf(matchers.flatMap {
case AllOf(ms) => ms
case m => List(m)
}.toList)
}
6 changes: 6 additions & 0 deletions common/src/main/scala/org/mockito/matchers/EqTo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ case class EqTo[T: Equality: ValueClassExtractor](value: T)(implicit $pt: Pretti

override def toString: String = $pt(value)
}

object EqTo {
// Smart constructor to return ArgumentMatcher[T] rather than a subtype
def apply[T: Equality: ValueClassExtractor](value: T)(implicit $pt: Prettifier): ArgumentMatcher[T] =
new EqTo(value)
}
14 changes: 14 additions & 0 deletions common/src/main/scala/org/mockito/matchers/ProductOf.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.mockito
package matchers

/** The product (2-tuple) of two matchers
*/
case class ProductOf[A, B] private (ma: ArgumentMatcher[A], mb: ArgumentMatcher[B]) extends ArgumentMatcher[(A, B)] {
override def matches(ab: (A, B)) = ab match { case (a, b) => ma.matches(a) && mb.matches(b) }
override def toString = s"productOf($ma, $mb)"
}

object ProductOf {
def apply[A, B](ma: ArgumentMatcher[A], mb: ArgumentMatcher[B]): ArgumentMatcher[(A, B)] =
new ProductOf(ma, mb)
}
16 changes: 16 additions & 0 deletions common/src/main/scala/org/mockito/matchers/Transformed.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.mockito
package matchers

/** Matcher tranformed from one type to another with a function to modify the input
*
* Technically this is 'contramapped' but that seemed like an unnecessarily jargony name.
*/
case class Transformed[A, B] private (ma: ArgumentMatcher[A])(f: B => A) extends ArgumentMatcher[B] {
override def matches(b: B) = ma.matches(f(b))
override def toString = s"transformed($ma: $f)"
}

object Transformed {
def apply[A, B](ma: ArgumentMatcher[A])(f: B => A): ArgumentMatcher[B] =
new Transformed(ma)(f)
}
7 changes: 7 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,7 @@
package org.mockito

package object matchers {
private val AnyArgMatcher: ArgumentMatcher[Any] = AllOf[Any]()

def AnyArg[A]: ArgumentMatcher[A] = AnyArgMatcher.asInstanceOf[ArgumentMatcher[A]]
}
9 changes: 9 additions & 0 deletions common/src/test/scala/org/mockito/matchers/Generators.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.mockito
package matchers

import org.scalacheck.Arbitrary

object Generators {
implicit def arbArgumentMatcher[A](implicit a: Arbitrary[A => Boolean]): Arbitrary[ArgumentMatcher[A]] =
Arbitrary(a.arbitrary.map(p => new ArgumentMatcher[A] { def matches(a: A) = p(a) }))
}
60 changes: 60 additions & 0 deletions common/src/test/scala/org/mockito/matchers/MatcherProps.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.mockito
package matchers

import cats.laws.discipline.MiniInt
import cats.laws.discipline.arbitrary._
import org.mockito.internal.matchers._
import org.scalacheck._

import Arbitrary.arbitrary
import Gen._
import Prop._

class MatcherProps extends Properties("matchers") {
import Generators._

property("AllOf") = forAll(chooseNum(0, 8))(length =>
forAll(listOfN(length, arbitrary[ArgumentMatcher[MiniInt]]), arbitrary[MiniInt]) {
case (matchers, value) =>
val allOf = AllOf(matchers: _*)
val stringRep = allOf.toString

classify(allOf.matches(value), "matches", "doesn't match") {
(allOf.matches(value) ?= matchers.forall(_.matches(value))) :| "matches all underlying" &&
matchers.iff {
case Nil => stringRep ?= "<any>"
case matcher :: Nil => stringRep ?= matcher.toString()
case _ => stringRep ?= s"allOf(${matchers.mkString(", ")})"
} :| "renders to string correctly"

}
}
)

property("ProductOf") = forAll { (ma: ArgumentMatcher[MiniInt], mb: ArgumentMatcher[String], a: MiniInt, b: String) =>
val productOf = ProductOf(ma, mb)
val product = (a, b)

val maMatches = ma.matches(a)
val mbMatches = mb.matches(b)
val productMatches = productOf.matches(product)

classify(productMatches, "matches", "doesn't match") {
all(
(productMatches ==> maMatches) :| "ma matches if product does",
(productMatches ==> mbMatches) :| "mb matches if product does",
((maMatches && mbMatches) ==> productMatches) :| "product matches if both ma and mb do",
(productOf.toString ?= s"productOf($ma, $mb)") :| "renders to string correctly"
)
}
}

property("Transformed") = forAll { (ma: ArgumentMatcher[String], f: MiniInt => String, value: MiniInt) =>
val transformed = Transformed(ma)(f)
val matches = transformed.matches(value)
classify(matches, "matches", "doesn't match") {
(matches ?= ma.matches(f(value))) :| "matches if underlying matches transfomed value" &&
(transformed.toString ?= s"transformed($ma: $f)") :| "renders to string correctly"
}
}
}
2 changes: 2 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ object Dependencies {
"ru.vyarus" % "generics-resolver" % "3.0.2",
)

val scalacheck = "org.scalacheck" %% "scalacheck" % "1.14.3"

val scalatest = "org.scalatest" %% "scalatest" % scalatestVersion

val specs2 = Seq(
Expand Down

0 comments on commit febdec6

Please sign in to comment.