Skip to content

Advanced syntax #24

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 8 commits into from
Aug 15, 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
50 changes: 45 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,12 @@ The library has independent developers, release cycle and versioning from core m
* Repositories: [Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cmockito-scala_2.12) or [JFrog's Bintray](https://bintray.com/mockito/maven/mockito-scala)


For a more detailed explanation of the features please read [this](https://medium.com/@bbonanno_83496/introduction-to-mockito-scala-ede30769cbda) article series


## Getting started

Then mixin one (or both) of the following traits as required

## `org.mockito.MockitoSugar`

For a more detailed explanation read [this](https://medium.com/@bbonanno_83496/introduction-to-mockito-scala-ede30769cbda)

This trait wraps the API available on `org.mockito.Mockito` from the Java version, but it provides a more Scala-like syntax, mainly
* Fixes the compiler errors that sometimes occurred when using overloaded methods that use varargs like doReturn
* Eliminates the need to use `classOf[T]`
Expand All @@ -50,6 +47,8 @@ The companion object also extends the trait to allow the usage of the API withou

## `org.mockito.ArgumentMatchersSugar`

For a more detailed explanation read [this](https://medium.com/@bbonanno_83496/introduction-to-mockito-scala-part-2-ba1a79cc4c53)

This trait exposes all the existent `org.mockito.ArgumentMatchers` but again it gives them a more Scala-like syntax, mainly
* `eq` was renamed to `eqTo` to avoid clashing with the Scala `eq` operator for identity equality
* `any` resolves to the correct type most of the times, removing the need of using the likes of `anyString`, `anyInt`, etc
Expand All @@ -72,6 +71,8 @@ verify(myObj).myMethod(eqToVal[MyValueClass](456))

## Improved ArgumentCaptor

For a more detailed explanation read [this](https://medium.com/@bbonanno_83496/introduction-to-mockito-scala-part-3-383c3b2ed55f)

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

Expand Down Expand Up @@ -124,6 +125,8 @@ session.finishMocking()

## `org.mockito.integrations.scalatest.MockitoFixture`

For a more detailed explanation read [this](https://medium.com/@bbonanno_83496/introduction-to-mockito-scala-part-3-383c3b2ed55f)

If you mix-in this trait on your test class **after** your favourite Spec trait, you will get an automatic
`MockitoScalaSession` around each one of your tests, so **all** of them will run in **Strict Stub** mode.

Expand Down Expand Up @@ -154,6 +157,43 @@ The main advantage being we don't have to remember to reset each one of the mock
If for some reason we want to have a mock that is not reset automatically while using this trait, then it should be
created via the companion object of `org.mockito.MockitoSugar` so is not tracked by this mechanism

## Idiomatic Mockito

By adding the trait `org.mockito.IdiomaticMockito` you get access to some improved methods in the API

This API is heavily inspired on Scalatest's Matchers, so if have used them, you'll find it very familiar

Here we can see the old syntax on the left and the new one on the right

```scala
trait Foo {
def bar: String
def bar(v: Int): Int
}

val aMock = mock[Foo]

when(aMock.bar) thenReturn "mocked!" <=> aMock.bar shouldReturn "mocked!"
when(aMock.bar) thenReturn "mocked!" thenReturn "mocked again!" <=> aMock.bar shouldReturn "mocked!" andThen "mocked again!"
when(aMock.bar) thenCallRealMethod() <=> aMock.bar shouldCallRealMethod
when(aMock.bar).thenThrow[IllegalArgumentException] <=> aMock.bar.shouldThrow[IllegalArgumentException]
when(aMock.bar) thenThrow new IllegalArgumentException <=> aMock.bar shouldThrow new IllegalArgumentException
when(aMock.bar) thenAnswer(_ => "mocked!") <=> aMock.bar shouldAnswer "mocked!"
when(aMock.bar(any)) thenAnswer(_.getArgument[Int](0) * 10) <=> aMock.bar(*) shouldAnswer ((i: Int) => i * 10)

verifyZeroInteractions(aMock) <=> aMock was never called
verify(aMock).bar <=> aMock wasCalled on bar
verify(aMock, only).bar <=> aMock wasCalled onlyOn bar
verify(aMock, never).bar <=> aMock was never called on bar
verify(aMock, times(2)).bar <=> aMock wasCalled twiceOn bar
verify(aMock, times(6)).bar <=> aMock wasCalled sixTimesOn bar
verifyNoMoreInteractions(aMock) <=> aMock was never called again
```

As you can see the new syntax reads a bit more natural, also notice you can use `*` instead of `any[T]`

Check the [tests](https://github.com/mockito/mockito-scala/blob/master/core/src/test/scala/org/mockito/IdiomaticSyntaxTest.scala) for more examples

## Experimental features

* **by-name** arguments is currently an experimental feature as the implementation is a bit hacky and it gave some people problems
Expand Down
174 changes: 174 additions & 0 deletions core/src/main/scala/org/mockito/IdiomaticMockito.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package org.mockito

import org.mockito.stubbing.{Answer, OngoingStubbing}
import MockitoSugar.{verify, _}

import scala.reflect.ClassTag
import scala.reflect.runtime.universe.TypeTag

trait IdiomaticMockito {

def mock[T <: AnyRef: ClassTag: TypeTag](name: String): T = MockitoSugar.mock(name)

def mock[T <: AnyRef: ClassTag: TypeTag](mockSettings: MockSettings): T = MockitoSugar.mock(mockSettings)

def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: Answer[_]): T = MockitoSugar.mock(defaultAnswer)

def mock[T <: AnyRef: ClassTag: TypeTag]: T = MockitoSugar.mock

implicit class StubbingOps[T](stubbing: => T) {

def shouldReturn(value: T): OngoingStubbing[T] = when(stubbing) thenReturn value

def shouldCallRealMethod: OngoingStubbing[T] = when(stubbing) thenCallRealMethod ()

def shouldThrow[E <: Throwable: ClassTag]: OngoingStubbing[T] = when(stubbing) thenThrow clazz

def shouldThrow[E <: Throwable](e: E): OngoingStubbing[T] = when(stubbing) thenThrow e

def shouldAnswer(f: => T): OngoingStubbing[T] = when(stubbing) thenAnswer (_ => f)

def shouldAnswer[P0](f: P0 => T): OngoingStubbing[T] = when(stubbing) thenAnswer (i => f(i.getArgument[P0](0)))

def shouldAnswer[P0, P1](f: (P0, P1) => T): OngoingStubbing[T] =
when(stubbing) thenAnswer (i => f(i.getArgument[P0](0), i.getArgument[P1](1)))

def shouldAnswer[P0, P1, P2](f: (P0, P1, P2) => T): OngoingStubbing[T] =
when(stubbing) thenAnswer (i => f(i.getArgument[P0](0), i.getArgument[P1](1), i.getArgument[P2](2)))

def shouldAnswer[P0, P1, P2, P3](f: (P0, P1, P2, P3) => T): OngoingStubbing[T] =
when(stubbing) thenAnswer (i =>
f(i.getArgument[P0](0), i.getArgument[P1](1), i.getArgument[P2](2), i.getArgument[P3](3)))

def shouldAnswer[P0, P1, P2, P3, P4](f: (P0, P1, P2, P3, P4) => T): OngoingStubbing[T] =
when(stubbing) thenAnswer (i =>
f(i.getArgument[P0](0), i.getArgument[P1](1), i.getArgument[P2](2), i.getArgument[P3](3), i.getArgument[P4](4)))

def shouldAnswer[P0, P1, P2, P3, P4, P5](f: (P0, P1, P2, P3, P4, P5) => T): OngoingStubbing[T] =
when(stubbing) thenAnswer (i =>
f(i.getArgument[P0](0),
i.getArgument[P1](1),
i.getArgument[P2](2),
i.getArgument[P3](3),
i.getArgument[P4](4),
i.getArgument[P5](5)))

def shouldAnswer[P0, P1, P2, P3, P4, P5, P6](f: (P0, P1, P2, P3, P4, P5, P6) => T): OngoingStubbing[T] =
when(stubbing) thenAnswer (i =>
f(i.getArgument[P0](0),
i.getArgument[P1](1),
i.getArgument[P2](2),
i.getArgument[P3](3),
i.getArgument[P4](4),
i.getArgument[P5](5),
i.getArgument[P6](6)))

def shouldAnswer[P0, P1, P2, P3, P4, P5, P6, P7](f: (P0, P1, P2, P3, P4, P5, P6, P7) => T): OngoingStubbing[T] =
when(stubbing) thenAnswer (i =>
f(
i.getArgument[P0](0),
i.getArgument[P1](1),
i.getArgument[P2](2),
i.getArgument[P3](3),
i.getArgument[P4](4),
i.getArgument[P5](5),
i.getArgument[P6](6),
i.getArgument[P7](7)
))

def shouldAnswer[P0, P1, P2, P3, P4, P5, P6, P7, P8](
f: (P0, P1, P2, P3, P4, P5, P6, P7, P8) => T): OngoingStubbing[T] =
when(stubbing) thenAnswer (i =>
f(
i.getArgument[P0](0),
i.getArgument[P1](1),
i.getArgument[P2](2),
i.getArgument[P3](3),
i.getArgument[P4](4),
i.getArgument[P5](5),
i.getArgument[P6](6),
i.getArgument[P7](7),
i.getArgument[P8](8)
))

def shouldAnswer[P0, P1, P2, P3, P4, P5, P6, P7, P8, P9](
f: (P0, P1, P2, P3, P4, P5, P6, P7, P8, P9) => T): OngoingStubbing[T] =
when(stubbing) thenAnswer (i =>
f(
i.getArgument[P0](0),
i.getArgument[P1](1),
i.getArgument[P2](2),
i.getArgument[P3](3),
i.getArgument[P4](4),
i.getArgument[P5](5),
i.getArgument[P6](6),
i.getArgument[P7](7),
i.getArgument[P8](8),
i.getArgument[P9](9)
))

def shouldAnswer[P0, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10](
f: (P0, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10) => T): OngoingStubbing[T] =
when(stubbing) thenAnswer (i =>
f(
i.getArgument[P0](0),
i.getArgument[P1](1),
i.getArgument[P2](2),
i.getArgument[P3](3),
i.getArgument[P4](4),
i.getArgument[P5](5),
i.getArgument[P6](6),
i.getArgument[P7](7),
i.getArgument[P8](8),
i.getArgument[P9](9),
i.getArgument[P10](10)
))

}

implicit class OngoingStubbingOps[T](ongoingStubbing: OngoingStubbing[T]) {
def andThen(value: T): OngoingStubbing[T] = ongoingStubbing thenReturn value
}

class On
class OnlyOn
class Never
//noinspection UnitMethodIsParameterless
case class NeverInstance[T <: AnyRef](mock: T) {
def called: Unit = verifyZeroInteractions(mock)
def called(on: On): T = verify(mock, MockitoSugar.never)
def called(again: Again): Unit = verifyNoMoreInteractions(mock)
}
class Again
case class Times(times: Int)

val on = new On
val onlyOn = new OnlyOn
val never = new Never
val again = new Again
val onceOn = Times(1)
val twiceOn = Times(2)
val thriceOn = Times(3)
val threeTimesOn = Times(3)
val fourTimesOn = Times(4)
val fiveTimesOn = Times(5)
val sixTimesOn = Times(6)
val sevenTimesOn = Times(7)
val eightTimesOn = Times(8)
val nineTimesOn = Times(9)
val tenTimesOn = Times(10)

implicit class VerificationOps[T <: AnyRef](mock: T) {

def wasCalled(on: On): T = verify(mock)

def wasCalled(t: Times): T = verify(mock, times(t.times))

def wasCalled(onlyOn: OnlyOn): T = verify(mock, only)

def was(n: Never): NeverInstance[T] = NeverInstance(mock)

}

def *[T]: T = ArgumentMatchersSugar.any[T]
}
6 changes: 1 addition & 5 deletions core/src/main/scala/org/mockito/MockitoSugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
package org.mockito

import org.mockito.MockitoEnhancerUtil.stubMock
import org.mockito.MockitoSugar.clazz
import org.mockito.stubbing.{ Answer, OngoingStubbing, Stubber }
import org.mockito.verification.{ VerificationMode, VerificationWithTimeout }

Expand Down Expand Up @@ -307,7 +306,4 @@ trait MockitoSugar extends MockitoEnhancer with DoSomething with Verifications {
/**
* Simple object to allow the usage of the trait without mixing it in
*/
object MockitoSugar extends MockitoSugar {
private[mockito] def clazz[T <: AnyRef](implicit classTag: ClassTag[T]) =
classTag.runtimeClass.asInstanceOf[Class[T]]
}
object MockitoSugar extends MockitoSugar
6 changes: 6 additions & 0 deletions core/src/main/scala/org/mockito/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org
import scala.reflect.ClassTag

package object mockito {
def clazz[T <: AnyRef](implicit classTag: ClassTag[T]): Class[T] = classTag.runtimeClass.asInstanceOf[Class[T]]
}
Loading