Skip to content

Add better support for ArgumentCaptor #20

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 4 commits into from
Aug 9, 2018
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
51 changes: 44 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,50 @@ Again, the companion object also extends the trait to allow the usage of the API
### Value Class Matchers

The matchers for the value classes always require the type to be explicit, apart from that, they should be used as any other matcher, e.g.
```scala
when(myObj.myMethod(anyVal[MyValueClass]) thenReturn "something"

myObj.myMethod(MyValueClass(456)) shouldBe "something"

verify(myObj).myMethod(eqToVal[MyValueClass](456))
```
when(myObj.myMethod(anyVal[MyValueClass]) thenReturn "something"

myObj.myMethod(MyValueClass(456)) shouldBe "something"

verify(myObj).myMethod(eqToVal[MyValueClass](456))
```

## Improved ArgumentCaptor

A new set of classes were added to make it easier, cleaner and more elegant to work with ArgumentCaptors, they also add
support to capture value classes without any annoying syntax

There is a new `trait org.mockito.captor.ArgCaptor[T]` that exposes a nicer API

Before:
```scala
val aMock = mock[Foo]
val captor = argumentCaptor[String]

aMock.stringArgument("it worked!")

verify(aMock).stringArgument(captor.capture())

captor.getValue shouldBe "it worked!"
```
Now:
```scala
val aMock = mock[Foo]
val captor = Captor[String]

aMock.stringArgument("it worked!")

verify(aMock).stringArgument(captor)

captor <-> "it worked!"
```

As you can see there is no need to call `capture()` nor `getValue` anymore (although they're still there if you need them)

There is another constructor `ValCaptor[T]` that should be used to capture value classes

Both `Captor[T]` and `ValCaptor[T]` return an instance of `ArgCaptor[T]` so the API is the same for both


## Experimental features

Expand All @@ -77,7 +114,7 @@ The matchers for the value classes always require the type to be explicit, apart
If you want to use it, you have to mix-in an extra trait (`org.mockito.ByNameExperimental`)
in your test class, after `org.mockito.MockitoSugar`, so your test file would look like

```
```scala
class MyTest extends WordSpec with MockitoSugar with ByNameExperimental
```

Expand Down
3 changes: 1 addition & 2 deletions core/src/main/scala/org/mockito/MockitoSugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,7 @@ trait MockitoSugar extends MockitoEnhancer with DoSomething with Verifications {
* It provides a nicer API as you can, for instance, do <code>argumentCaptor[SomeClass]</code>
* instead of <code>ArgumentCaptor.forClass(classOf[SomeClass])</code>
*/
def argumentCaptor[T <: AnyRef: ClassTag]: ArgumentCaptor[T] =
ArgumentCaptor.forClass(clazz)
def argumentCaptor[T: ClassTag]: ArgumentCaptor[T] = ArgumentCaptor.forClass(clazz)

/**
* Delegates to <code>Mockito.spy()</code>, it's only here to expose the full Mockito API
Expand Down
26 changes: 26 additions & 0 deletions core/src/main/scala/org/mockito/captor/Captor.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.mockito.captor

import org.mockito.MockitoSugar

import scala.collection.JavaConverters._
import scala.language.implicitConversions
import scala.reflect.ClassTag

class Captor[T: ClassTag] extends ArgCaptor[T] {

private val argumentCaptor = MockitoSugar.argumentCaptor[T]

override def capture: T = argumentCaptor.capture()

override def value: T = argumentCaptor.getValue

override def values: List[T] = argumentCaptor.getAllValues.asScala.toList
}

object Captor {
def apply[T: ClassTag]: ArgCaptor[T] = new Captor[T]
}

object ValCaptor {
def apply[T](implicit c: ArgCaptor[T]): ArgCaptor[T] = c
}
113 changes: 113 additions & 0 deletions core/src/test/scala/org/mockito/captor/ArgCaptorTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package org.mockito.captor

import org.mockito.MockitoSugar
import org.mockito.captor.ArgCaptorTest._
import org.scalatest.{Matchers, WordSpec}

object ArgCaptorTest {
case class Name(name: String) extends AnyVal

class Email(val email: String) extends AnyVal

class Foo {
def stringArgument(s: String): String = s

def intArgument(i: Int): Int = i

def complexArgument(m: Map[String, Int]): Map[String, Int] = m

def valueCaseClass(name: Name): String = name.name

def valueClass(email: Email): String = email.email
}
}

class ArgCaptorTest extends WordSpec with MockitoSugar with Matchers {

"Captor" should {

"capture a simple AnyRef argument" in {
val aMock = mock[Foo]
val captor = Captor[String]

aMock.stringArgument("it worked!")

verify(aMock).stringArgument(captor)

captor <-> "it worked!"
}

"capture a simple AnyVal argument" in {
val aMock = mock[Foo]
val captor = Captor[Int]

aMock.intArgument(42)

verify(aMock).intArgument(captor)

captor <-> 42
}

"capture a complex argument" in {
val aMock = mock[Foo]
val captor = Captor[Map[String, Int]]

aMock.complexArgument(Map("Works" -> 1))

verify(aMock).complexArgument(captor)

captor <-> Map("Works" -> 1)
}

"expose the captured value to use with custom matchers" in {
val aMock = mock[Foo]
val captor = Captor[String]

aMock.stringArgument("it worked!")

verify(aMock).stringArgument(captor)

captor.value shouldBe "it worked!"
}

"expose all the captured values to use with custom matchers" in {
val aMock = mock[Foo]
val captor = Captor[String]

aMock.stringArgument("it worked!")
aMock.stringArgument("it worked again!")

verify(aMock, times(2)).stringArgument(captor)

captor.values should contain only ("it worked!", "it worked again!")
}
}

"ValCaptor" should {
"work with value case classes" in {
val aMock = mock[Foo]
val captor = ValCaptor[Name]

aMock.valueCaseClass(Name("Batman"))

verify(aMock).valueCaseClass(captor)

captor <-> Name("Batman")
captor.value shouldBe Name("Batman")
captor.values should contain only Name("Batman")
}

"work with value non-case classes" in {
val aMock = mock[Foo]
val captor = ValCaptor[Email]

aMock.valueClass(new Email("[email protected]"))

verify(aMock).valueClass(captor)

captor <-> new Email("[email protected]")
captor.value shouldBe new Email("[email protected]")
captor.values should contain only new Email("[email protected]")
}
}
}
57 changes: 57 additions & 0 deletions macro/src/main/scala/org/mockito/captor/ArgCaptor.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.mockito.captor

import scala.language.experimental.macros
import scala.language.implicitConversions
import scala.reflect.macros.blackbox

trait ArgCaptor[T] {

def capture: T

def value: T

def values: List[T]

def <->(expectation: T): Unit =
if (expectation != value) throw new AssertionError(s"Got [$value] instead of [$expectation]")
}

object ArgCaptor {

implicit def asCapture[T](c: ArgCaptor[T]): T = c.capture

implicit def materializeValueClassCaptor[T]: ArgCaptor[T] = macro materializeValueClassCaptorMacro[T]

def materializeValueClassCaptorMacro[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[ArgCaptor[T]] = {
import c.universe._
val tpe = weakTypeOf[T]

val param = tpe.decls
.collectFirst {
case m: MethodSymbol if m.isPrimaryConstructor ⇒ m
}
.get
.paramLists
.head
.head

val paramType = tpe.decl(param.name).typeSignature.finalResultType

c.Expr[ArgCaptor[T]] {
q"""
new org.mockito.captor.ArgCaptor[$tpe] {

import scala.collection.JavaConverters._

private val argumentCaptor = org.mockito.ArgumentCaptor.forClass(classOf[$paramType])

override def capture: $tpe = new $tpe(argumentCaptor.capture())

override def value: $tpe = new $tpe(argumentCaptor.getValue)

override def values: List[$tpe] = argumentCaptor.getAllValues.asScala.map(v => new $tpe(v)).toList
}
"""
}
}
}