Skip to content

Cats integration #107

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 11 commits into from
May 12, 2019
16 changes: 15 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,20 @@ lazy val specs2 = (project in file("specs2"))
libraryDependencies += "org.hamcrest" % "hamcrest-core" % "1.3" % "provided",
)

lazy val cats = (project in file("cats"))
.dependsOn(core)
.dependsOn(common % "compile-internal, test-internal")
.dependsOn(macroSub % "compile-internal, test-internal")
.settings(
name := "mockito-scala-cats",
commonSettings,
publishSettings,
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "2.0.0-M1" % "provided",
"org.scalatest" %% "scalatest" % "3.0.8-RC2" % "test"
),
)

lazy val common = (project in file("common"))
.dependsOn(macroCommon)
.settings(
Expand Down Expand Up @@ -161,4 +175,4 @@ lazy val root = (project in file("."))
.settings(
publish := {},
publishLocal := {}
) aggregate (core, scalatest, specs2)
) aggregate (core, scalatest, specs2, cats)
20 changes: 20 additions & 0 deletions cats/src/main/scala/org/mockito/cats/CatsStubbing.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.mockito.cats

import cats.implicits._
import cats.{Applicative, ApplicativeError}
import org.mockito.stubbing.OngoingStubbing

case class CatsStubbing[F[_], T](delegate: OngoingStubbing[F[T]]) {

def thenReturn(value: T)(implicit a: Applicative[F]): CatsStubbing[F, T] = delegate thenReturn value.pure[F]

def thenFailWith[E](error: E)(implicit ae: ApplicativeError[F, E]): CatsStubbing[F, T] = delegate thenReturn error.raiseError[F, T]

def getMock[M]: M = delegate.getMock[M]
}

object CatsStubbing {
implicit def toCatsFirstStubbing[F[_], T](v: OngoingStubbing[F[T]]): CatsStubbing[F, T] = CatsStubbing(v)

implicit def toMock[F[_], T, M](s: CatsStubbing[F, T]): M = s.getMock[M]
}
12 changes: 12 additions & 0 deletions cats/src/main/scala/org/mockito/cats/EqToEquality.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.mockito.cats

import cats.Eq
import cats.implicits._
import org.mockito._
import org.scalactic.Equality

import scala.reflect.ClassTag

class EqToEquality[T: ClassTag: Eq] extends Equality[T] {
override def areEqual(a: T, b: Any): Boolean = clazz[T].isInstance(b) && a === b.asInstanceOf[T]
}
35 changes: 35 additions & 0 deletions cats/src/main/scala/org/mockito/cats/IdiomaticMockitoCats.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.mockito.cats

import cats.{ Applicative, ApplicativeError, Eq }
import org.mockito._
import org.mockito.cats.IdiomaticMockitoCats.{ ReturnActions, ThrowActions }
import org.scalactic.Equality

import scala.reflect.ClassTag

trait IdiomaticMockitoCats extends IdiomaticMockito {

implicit class StubbingOps[F[_], T](stubbing: F[T]) {

def shouldReturnF: ReturnActions[F, T] = macro WhenMacro.shouldReturn[T]
def mustReturnF: ReturnActions[F, T] = macro WhenMacro.shouldReturn[T]
def returnsF: ReturnActions[F, T] = macro WhenMacro.shouldReturn[T]

def shouldFailWith: ThrowActions[F, T] = macro WhenMacro.shouldThrow[T]
def mustFailWith: ThrowActions[F, T] = macro WhenMacro.shouldThrow[T]
def failsWith: ThrowActions[F, T] = macro WhenMacro.shouldThrow[T]
}

implicit def catsEquality[T: ClassTag: Eq]: Equality[T] = new EqToEquality[T]
}

object IdiomaticMockitoCats extends IdiomaticMockitoCats {

class ReturnActions[F[_], T](os: CatsStubbing[F, T]) {
def apply(value: T)(implicit a: Applicative[F]): CatsStubbing[F, T] = os thenReturn value
}

class ThrowActions[F[_], T](os: CatsStubbing[F, T]) {
def apply[E](error: E)(implicit ae: ApplicativeError[F, E]): CatsStubbing[F, T] = os thenFailWith error
}
}
16 changes: 16 additions & 0 deletions cats/src/main/scala/org/mockito/cats/MockitoCats.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.mockito.cats

import cats.Eq
import org.mockito._
import org.scalactic.Equality

import scala.reflect.ClassTag

trait MockitoCats extends MockitoSugar {

def whenF[F[_], T](methodCall: F[T]): CatsStubbing[F, T] = Mockito.when(methodCall)

implicit def catsEquality[T: ClassTag: Eq]: Equality[T] = new EqToEquality[T]
}

object MockitoCats extends MockitoCats
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.mockito.cats

import cats.Eq
import cats.implicits._
import org.mockito.{ ArgumentMatchersSugar, IdiomaticMockito }
import org.scalatest.{ EitherValues, Matchers, OptionValues, WordSpec }

class IdiomaticMockitoCatsTest
extends WordSpec
with Matchers
with IdiomaticMockito
with ArgumentMatchersSugar
with IdiomaticMockitoCats
with EitherValues
with OptionValues {

"mock[T]" should {
"stub full applicative" in {
val aMock = mock[Foo]

aMock.returnsOptionString(*) shouldReturnF "mocked!"

aMock.returnsOptionString("hello").value shouldBe "mocked!"
}

"stub specific applicative" in {
val aMock = mock[Foo]

aMock.returnsOptionT("hello") shouldReturnF "mocked!"

aMock.returnsOptionT("hello").value shouldBe "mocked!"
}

"stub generic applicative" in {
val aMock = mock[Foo]

aMock.returnsMT[Option, String]("hello") shouldReturnF "mocked!"

aMock.returnsMT[Option, String]("hello").value shouldBe "mocked!"
}

"work with value classes" in {
val aMock = mock[Foo]

aMock.returnsMT[Option, ValueClass](eqTo(ValueClass("hi"))) shouldReturnF ValueClass("mocked!")

aMock.returnsMT[Option, ValueClass](ValueClass("hi")).value shouldBe ValueClass("mocked!")
}

"create and stub in one line" in {
val aMock: Foo = mock[Foo].returnsOptionString(*) shouldReturnF "mocked!"

aMock.returnsOptionString("hello").value shouldBe "mocked!"
}

"raise errors" in {
type ErrorOr[A] = Either[Error, A]
val aMock = mock[Foo]

aMock.returnsMT[ErrorOr, ValueClass](eqTo(ValueClass("hi"))) shouldReturnF ValueClass("mocked!")
aMock.returnsMT[ErrorOr, ValueClass](ValueClass("bye")) shouldFailWith Error("error")

aMock.returnsMT[ErrorOr, ValueClass](ValueClass("hi")).right.value shouldBe ValueClass("mocked!")
aMock.returnsMT[ErrorOr, ValueClass](ValueClass("bye")).left.value shouldBe Error("error")
}

"work with cats Eq" in {
implicit val stringEq: Eq[ValueClass] = Eq.instance((x: ValueClass, y: ValueClass) => x.s.toLowerCase == y.s.toLowerCase)
val aMock = mock[Foo]

aMock.returnsOptionT(eqTo(ValueClass("HoLa"))) shouldReturnF ValueClass("Mocked!")

aMock.returnsOptionT(ValueClass("HOLA")) should ===(Some(ValueClass("mocked!")))
}
}
}
76 changes: 76 additions & 0 deletions cats/src/test/scala/org/mockito/cats/MockitoCatsTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.mockito.cats

import cats.Eq
import cats.implicits._
import org.mockito.{ ArgumentMatchersSugar, MockitoSugar }
import org.scalatest.{ EitherValues, Matchers, OptionValues, WordSpec }

class MockitoCatsTest
extends WordSpec
with Matchers
with MockitoSugar
with ArgumentMatchersSugar
with MockitoCats
with EitherValues
with OptionValues {

"mock[T]" should {
"stub full applicative" in {
val aMock = mock[Foo]

whenF(aMock.returnsOptionString(*)) thenReturn "mocked!"

aMock.returnsOptionString("hello").value shouldBe "mocked!"
}

"stub specific applicative" in {
val aMock = mock[Foo]

whenF(aMock.returnsOptionT("hello")) thenReturn "mocked!"

aMock.returnsOptionT("hello").value shouldBe "mocked!"
}

"stub generic applicative" in {
val aMock = mock[Foo]

whenF(aMock.returnsMT[Option, String]("hello")) thenReturn "mocked!"

aMock.returnsMT[Option, String]("hello").value shouldBe "mocked!"
}

"work with value classes" in {
val aMock = mock[Foo]

whenF(aMock.returnsMT[Option, ValueClass](eqTo(ValueClass("hi")))) thenReturn ValueClass("mocked!")

aMock.returnsMT[Option, ValueClass](ValueClass("hi")).value shouldBe ValueClass("mocked!")
}

"create and stub in one line" in {
val aMock: Foo = whenF(mock[Foo].returnsOptionString(*)) thenReturn "mocked!"

aMock.returnsOptionString("hello").value shouldBe "mocked!"
}

"raise errors" in {
type ErrorOr[A] = Either[Error, A]
val aMock = mock[Foo]

whenF(aMock.returnsMT[ErrorOr, ValueClass](eqTo(ValueClass("hi")))) thenReturn ValueClass("mocked!")
whenF(aMock.returnsMT[ErrorOr, ValueClass](ValueClass("bye"))) thenFailWith Error("error")

aMock.returnsMT[ErrorOr, ValueClass](ValueClass("hi")).right.value shouldBe ValueClass("mocked!")
aMock.returnsMT[ErrorOr, ValueClass](ValueClass("bye")).left.value shouldBe Error("error")
}

"work with cats Eq" in {
implicit val stringEq: Eq[ValueClass] = Eq.instance((x: ValueClass, y: ValueClass) => x.s.toLowerCase == y.s.toLowerCase)
val aMock = mock[Foo]

whenF(aMock.returnsOptionT(eqTo(ValueClass("HoLa")))) thenReturn ValueClass("Mocked!")

aMock.returnsOptionT(ValueClass("HOLA")) should ===(Some(ValueClass("mocked!")))
}
}
}
17 changes: 17 additions & 0 deletions cats/src/test/scala/org/mockito/cats/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.mockito

package object cats {

case class Error(e: String)

case class ValueClass(s: String) extends AnyVal

trait Foo {
def returnsOptionString(v: String): Option[String]

def returnsOptionT[T](v: T): Option[T]

def returnsMT[M[_], T](v: T): M[T]
}

}
12 changes: 10 additions & 2 deletions core/src/main/scala/org/mockito/IdiomaticMockitoBase.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.mockito

import org.mockito.WhenMacro._
import org.mockito.stubbing.ScalaOngoingStubbing
import org.mockito.stubbing.{ ScalaFirstStubbing, ScalaOngoingStubbing }
import org.mockito.verification.VerificationMode

import scala.concurrent.duration.Duration
Expand Down Expand Up @@ -64,11 +64,19 @@ object IdiomaticMockitoBase {
override def verificationMode: VerificationMode = Mockito.timeout(d.toMillis).only
}
}

class ReturnActions[T](os: ScalaFirstStubbing[T]) {
def apply(value: T, values: T*): ScalaOngoingStubbing[T] = os thenReturn (value, values: _*)
}

class ThrowActions[T](os: ScalaFirstStubbing[T]) {
def apply[E <: Throwable](e: E*): ScalaOngoingStubbing[T] = os thenThrow (e: _*)
}
}

trait IdiomaticMockitoBase extends MockitoEnhancer {

import IdiomaticMockitoBase._
import org.mockito.IdiomaticMockitoBase._

type Verification

Expand Down
Loading