Skip to content

Commit bde072d

Browse files
authored
Merge pull request #107 from mockito/cats-integration
Cats integration
2 parents 78f0454 + b223bdd commit bde072d

File tree

11 files changed

+376
-35
lines changed

11 files changed

+376
-35
lines changed

README.md

+63
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ The library has independent developers, release cycle and versioning from core m
2323
* Artifact identifier: "org.mockito:mockito-scala_[scala-version]:[version]"
2424
* Artifact identifier: "org.mockito:mockito-scala-scalatest_[scala-version]:[version]"
2525
* Artifact identifier: "org.mockito:mockito-scala-specs2_[scala-version]:[version]"
26+
* Artifact identifier: "org.mockito:mockito-scala-cats_[scala-version]:[version]"
2627
* Latest version - see [release notes](/docs/release-notes.md)
2728
* Repositories: [Maven Central](https://search.maven.org/search?q=mockito-scala) or [JFrog's Bintray](https://bintray.com/mockito/maven/mockito-scala)
2829

@@ -519,6 +520,68 @@ If you want to customise the print of any type you just need to declare your `Pr
519520
}
520521
```
521522
523+
## Cats integration
524+
By adding the module `mockito-scala-cats` 2 new traits are available, `IdiomaticMockitoCats` and `MockitoCats` which are meant to be mixed-in in
525+
tests that use `IdiomaticMockito` and `MockitoSugar` respectively.
526+
Please look at the [tests](/cats/src/test) for more detailed examples
527+
528+
### MockitoCats
529+
This traits adds `whenF()` which allows stubbing methods that return an Applicative (or an ApplicativeError) to be stubbed by just providing
530+
the content of said applicative (or the error).
531+
So for
532+
```scala
533+
trait Foo {
534+
def returnsOption[T](v: T): Option[T]
535+
def returnsMT[M[_], T](v: T): M[T]
536+
}
537+
// We can now write
538+
val aMock = mock[Foo]
539+
whenF(aMock.returnsOption(*)) thenReturn "mocked!"
540+
whenF(aMock.returnsMT[Future, String](*)) thenReturn "mocked!"
541+
// Rather than
542+
when(aMock.returnsOption(*)) thenReturn Some("mocked!")
543+
when(aMock.returnsMT[Future, String](*)) thenReturn Future.successful("mocked!")
544+
545+
//We could also do stubbings in a single line if that's all we need from the mock
546+
val inlineMock: Foo = whenF(mock[Foo].returnsOption(*)) thenReturn "mocked!"
547+
548+
// For errors we can do
549+
type ErrorOr[A] = Either[Error, A]
550+
val failingMock: Foo = whenF(mock[Foo].returnsMT[ErrorOr, MyClass](*)) thenFailWith Error("error")
551+
//Rather than
552+
val failingMock: Foo = when(mock[Foo].returnsMT[ErrorOr, MyClass](*)) thenReturn Left(Error("error"))
553+
```
554+
555+
The trait also provides and implicit conversion from `cats.Eq` to `scalactic.Equality` so if you have an implicit `cats.Eq` instance in scope,
556+
it will be automatically used by the `eqTo` matcher.
557+
558+
### IdiomaticMockitoCats
559+
560+
Similar to `MockitoCats` but for the idiomatic syntax (including the conversion from `cats.Eq` to `scalactic.Equality`), so the code would look like
561+
562+
```scala
563+
trait Foo {
564+
def returnsOption[T](v: T): Option[T]
565+
def returnsMT[M[_], T](v: T): M[T]
566+
}
567+
// We can now write
568+
val aMock = mock[Foo]
569+
aMock.returnsOption(*) shouldReturnF "mocked!"
570+
aMock.returnsMT[Future, String](*) shouldReturnF "mocked!"
571+
// Rather than
572+
aMock.returnsOption(*) shouldReturn Some("mocked!")
573+
aMock.returnsMT[Future, String](*) shouldReturn Future.successful("mocked!")
574+
575+
//We could also do stubbings in a single line if that's all we need from the mock
576+
val inlineMock: Foo = mock[Foo].returnsOption(*) shouldReturnF "mocked!"
577+
578+
// For errors we can do
579+
type ErrorOr[A] = Either[Error, A]
580+
val failingMock: Foo = mock[Foo].returnsMT[ErrorOr, MyClass](*) shouldFailWith Error("error")
581+
//Rather than
582+
val failingMock: Foo = mock[Foo].returnsMT[ErrorOr, MyClass](*) shouldReturn Left(Error("error"))
583+
```
584+
522585
## Notes
523586
524587
### Dead code warning

build.sbt

+15-1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,20 @@ lazy val specs2 = (project in file("specs2"))
8989
libraryDependencies += "org.hamcrest" % "hamcrest-core" % "1.3" % "provided",
9090
)
9191

92+
lazy val cats = (project in file("cats"))
93+
.dependsOn(core)
94+
.dependsOn(common % "compile-internal, test-internal")
95+
.dependsOn(macroSub % "compile-internal, test-internal")
96+
.settings(
97+
name := "mockito-scala-cats",
98+
commonSettings,
99+
publishSettings,
100+
libraryDependencies ++= Seq(
101+
"org.typelevel" %% "cats-core" % "2.0.0-M1" % "provided",
102+
"org.scalatest" %% "scalatest" % "3.0.8-RC2" % "test"
103+
),
104+
)
105+
92106
lazy val common = (project in file("common"))
93107
.dependsOn(macroCommon)
94108
.settings(
@@ -161,4 +175,4 @@ lazy val root = (project in file("."))
161175
.settings(
162176
publish := {},
163177
publishLocal := {}
164-
) aggregate (core, scalatest, specs2)
178+
) aggregate (core, scalatest, specs2, cats)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.mockito.cats
2+
3+
import cats.implicits._
4+
import cats.{Applicative, ApplicativeError}
5+
import org.mockito.stubbing.OngoingStubbing
6+
7+
case class CatsStubbing[F[_], T](delegate: OngoingStubbing[F[T]]) {
8+
9+
def thenReturn(value: T)(implicit a: Applicative[F]): CatsStubbing[F, T] = delegate thenReturn value.pure[F]
10+
11+
def thenFailWith[E](error: E)(implicit ae: ApplicativeError[F, E]): CatsStubbing[F, T] = delegate thenReturn error.raiseError[F, T]
12+
13+
def getMock[M]: M = delegate.getMock[M]
14+
}
15+
16+
object CatsStubbing {
17+
implicit def toCatsFirstStubbing[F[_], T](v: OngoingStubbing[F[T]]): CatsStubbing[F, T] = CatsStubbing(v)
18+
19+
implicit def toMock[F[_], T, M](s: CatsStubbing[F, T]): M = s.getMock[M]
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.mockito.cats
2+
3+
import cats.Eq
4+
import cats.implicits._
5+
import org.mockito._
6+
import org.scalactic.Equality
7+
8+
import scala.reflect.ClassTag
9+
10+
class EqToEquality[T: ClassTag: Eq] extends Equality[T] {
11+
override def areEqual(a: T, b: Any): Boolean = clazz[T].isInstance(b) && a === b.asInstanceOf[T]
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.mockito.cats
2+
3+
import cats.{ Applicative, ApplicativeError, Eq }
4+
import org.mockito._
5+
import org.mockito.cats.IdiomaticMockitoCats.{ ReturnActions, ThrowActions }
6+
import org.scalactic.Equality
7+
8+
import scala.reflect.ClassTag
9+
10+
trait IdiomaticMockitoCats extends IdiomaticMockito {
11+
12+
implicit class StubbingOps[F[_], T](stubbing: F[T]) {
13+
14+
def shouldReturnF: ReturnActions[F, T] = macro WhenMacro.shouldReturn[T]
15+
def mustReturnF: ReturnActions[F, T] = macro WhenMacro.shouldReturn[T]
16+
def returnsF: ReturnActions[F, T] = macro WhenMacro.shouldReturn[T]
17+
18+
def shouldFailWith: ThrowActions[F, T] = macro WhenMacro.shouldThrow[T]
19+
def mustFailWith: ThrowActions[F, T] = macro WhenMacro.shouldThrow[T]
20+
def failsWith: ThrowActions[F, T] = macro WhenMacro.shouldThrow[T]
21+
}
22+
23+
implicit def catsEquality[T: ClassTag: Eq]: Equality[T] = new EqToEquality[T]
24+
}
25+
26+
object IdiomaticMockitoCats extends IdiomaticMockitoCats {
27+
28+
class ReturnActions[F[_], T](os: CatsStubbing[F, T]) {
29+
def apply(value: T)(implicit a: Applicative[F]): CatsStubbing[F, T] = os thenReturn value
30+
}
31+
32+
class ThrowActions[F[_], T](os: CatsStubbing[F, T]) {
33+
def apply[E](error: E)(implicit ae: ApplicativeError[F, E]): CatsStubbing[F, T] = os thenFailWith error
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.mockito.cats
2+
3+
import cats.Eq
4+
import org.mockito._
5+
import org.scalactic.Equality
6+
7+
import scala.reflect.ClassTag
8+
9+
trait MockitoCats extends MockitoSugar {
10+
11+
def whenF[F[_], T](methodCall: F[T]): CatsStubbing[F, T] = Mockito.when(methodCall)
12+
13+
implicit def catsEquality[T: ClassTag: Eq]: Equality[T] = new EqToEquality[T]
14+
}
15+
16+
object MockitoCats extends MockitoCats
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package org.mockito.cats
2+
3+
import cats.Eq
4+
import cats.implicits._
5+
import org.mockito.{ ArgumentMatchersSugar, IdiomaticMockito }
6+
import org.scalatest.{ EitherValues, Matchers, OptionValues, WordSpec }
7+
8+
class IdiomaticMockitoCatsTest
9+
extends WordSpec
10+
with Matchers
11+
with IdiomaticMockito
12+
with ArgumentMatchersSugar
13+
with IdiomaticMockitoCats
14+
with EitherValues
15+
with OptionValues {
16+
17+
"mock[T]" should {
18+
"stub full applicative" in {
19+
val aMock = mock[Foo]
20+
21+
aMock.returnsOptionString(*) shouldReturnF "mocked!"
22+
23+
aMock.returnsOptionString("hello").value shouldBe "mocked!"
24+
}
25+
26+
"stub specific applicative" in {
27+
val aMock = mock[Foo]
28+
29+
aMock.returnsOptionT("hello") shouldReturnF "mocked!"
30+
31+
aMock.returnsOptionT("hello").value shouldBe "mocked!"
32+
}
33+
34+
"stub generic applicative" in {
35+
val aMock = mock[Foo]
36+
37+
aMock.returnsMT[Option, String]("hello") shouldReturnF "mocked!"
38+
39+
aMock.returnsMT[Option, String]("hello").value shouldBe "mocked!"
40+
}
41+
42+
"work with value classes" in {
43+
val aMock = mock[Foo]
44+
45+
aMock.returnsMT[Option, ValueClass](ValueClass("hi")) shouldReturnF ValueClass("mocked!")
46+
47+
aMock.returnsMT[Option, ValueClass](ValueClass("hi")).value shouldBe ValueClass("mocked!")
48+
}
49+
50+
"create and stub in one line" in {
51+
val aMock: Foo = mock[Foo].returnsOptionString(*) shouldReturnF "mocked!"
52+
53+
aMock.returnsOptionString("hello").value shouldBe "mocked!"
54+
}
55+
56+
"raise errors" in {
57+
type ErrorOr[A] = Either[Error, A]
58+
val aMock = mock[Foo]
59+
60+
aMock.returnsMT[ErrorOr, ValueClass](ValueClass("hi")) shouldReturnF ValueClass("mocked!")
61+
aMock.returnsMT[ErrorOr, ValueClass](ValueClass("bye")) shouldFailWith Error("error")
62+
63+
aMock.returnsMT[ErrorOr, ValueClass](ValueClass("hi")).right.value shouldBe ValueClass("mocked!")
64+
aMock.returnsMT[ErrorOr, ValueClass](ValueClass("bye")).left.value shouldBe Error("error")
65+
}
66+
67+
"work with cats Eq" in {
68+
implicit val stringEq: Eq[ValueClass] = Eq.instance((x: ValueClass, y: ValueClass) => x.s.toLowerCase == y.s.toLowerCase)
69+
val aMock = mock[Foo]
70+
71+
aMock.returnsOptionT(ValueClass("HoLa")) shouldReturnF ValueClass("Mocked!")
72+
73+
aMock.returnsOptionT(ValueClass("HOLA")).value should ===(ValueClass("mocked!"))
74+
}
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package org.mockito.cats
2+
3+
import cats.Eq
4+
import cats.implicits._
5+
import org.mockito.{ ArgumentMatchersSugar, MockitoSugar }
6+
import org.scalatest.{ EitherValues, Matchers, OptionValues, WordSpec }
7+
8+
class MockitoCatsTest
9+
extends WordSpec
10+
with Matchers
11+
with MockitoSugar
12+
with ArgumentMatchersSugar
13+
with MockitoCats
14+
with EitherValues
15+
with OptionValues {
16+
17+
"mock[T]" should {
18+
"stub full applicative" in {
19+
val aMock = mock[Foo]
20+
21+
whenF(aMock.returnsOptionString(*)) thenReturn "mocked!"
22+
23+
aMock.returnsOptionString("hello").value shouldBe "mocked!"
24+
}
25+
26+
"stub specific applicative" in {
27+
val aMock = mock[Foo]
28+
29+
whenF(aMock.returnsOptionT("hello")) thenReturn "mocked!"
30+
31+
aMock.returnsOptionT("hello").value shouldBe "mocked!"
32+
}
33+
34+
"stub generic applicative" in {
35+
val aMock = mock[Foo]
36+
37+
whenF(aMock.returnsMT[Option, String]("hello")) thenReturn "mocked!"
38+
39+
aMock.returnsMT[Option, String]("hello").value shouldBe "mocked!"
40+
}
41+
42+
"work with value classes" in {
43+
val aMock = mock[Foo]
44+
45+
whenF(aMock.returnsMT[Option, ValueClass](eqTo(ValueClass("hi")))) thenReturn ValueClass("mocked!")
46+
47+
aMock.returnsMT[Option, ValueClass](ValueClass("hi")).value shouldBe ValueClass("mocked!")
48+
}
49+
50+
"create and stub in one line" in {
51+
val aMock: Foo = whenF(mock[Foo].returnsOptionString(*)) thenReturn "mocked!"
52+
53+
aMock.returnsOptionString("hello").value shouldBe "mocked!"
54+
}
55+
56+
"raise errors" in {
57+
type ErrorOr[A] = Either[Error, A]
58+
val aMock = mock[Foo]
59+
60+
whenF(aMock.returnsMT[ErrorOr, ValueClass](ValueClass("hi"))) thenReturn ValueClass("mocked!")
61+
whenF(aMock.returnsMT[ErrorOr, ValueClass](ValueClass("bye"))) thenFailWith Error("error")
62+
63+
aMock.returnsMT[ErrorOr, ValueClass](ValueClass("hi")).right.value shouldBe ValueClass("mocked!")
64+
aMock.returnsMT[ErrorOr, ValueClass](ValueClass("bye")).left.value shouldBe Error("error")
65+
}
66+
67+
"work with cats Eq" in {
68+
implicit val stringEq: Eq[ValueClass] = Eq.instance((x: ValueClass, y: ValueClass) => x.s.toLowerCase == y.s.toLowerCase)
69+
val aMock = mock[Foo]
70+
71+
whenF(aMock.returnsOptionT(eqTo(ValueClass("HoLa")))) thenReturn ValueClass("Mocked!")
72+
73+
aMock.returnsOptionT(ValueClass("HOLA")).value should ===(ValueClass("mocked!"))
74+
}
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.mockito
2+
3+
package object cats {
4+
5+
case class Error(e: String)
6+
7+
case class ValueClass(s: String) extends AnyVal
8+
9+
trait Foo {
10+
def returnsOptionString(v: String): Option[String]
11+
12+
def returnsOptionT[T](v: T): Option[T]
13+
14+
def returnsMT[M[_], T](v: T): M[T]
15+
}
16+
17+
}

core/src/main/scala/org/mockito/IdiomaticMockitoBase.scala

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package org.mockito
22

33
import org.mockito.WhenMacro._
4-
import org.mockito.stubbing.ScalaOngoingStubbing
4+
import org.mockito.stubbing.{ ScalaFirstStubbing, ScalaOngoingStubbing }
55
import org.mockito.verification.VerificationMode
66

77
import scala.concurrent.duration.Duration
@@ -64,11 +64,19 @@ object IdiomaticMockitoBase {
6464
override def verificationMode: VerificationMode = Mockito.timeout(d.toMillis).only
6565
}
6666
}
67+
68+
class ReturnActions[T](os: ScalaFirstStubbing[T]) {
69+
def apply(value: T, values: T*): ScalaOngoingStubbing[T] = os thenReturn (value, values: _*)
70+
}
71+
72+
class ThrowActions[T](os: ScalaFirstStubbing[T]) {
73+
def apply[E <: Throwable](e: E*): ScalaOngoingStubbing[T] = os thenThrow (e: _*)
74+
}
6775
}
6876

6977
trait IdiomaticMockitoBase extends MockitoEnhancer {
7078

71-
import IdiomaticMockitoBase._
79+
import org.mockito.IdiomaticMockitoBase._
7280

7381
type Verification
7482

0 commit comments

Comments
 (0)