Skip to content

Commit c45ebb6

Browse files
authored
Merge pull request #30 from mockito/misc-refactor
Misc refactor
2 parents 83b9669 + d0627d4 commit c45ebb6

20 files changed

+790
-185
lines changed

README.md

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ The library has independent developers, release cycle and versioning from core m
2323
* Repositories: [Maven Central](https://search.maven.org/search?q=mockito-scala) or [JFrog's Bintray](https://bintray.com/mockito/maven/mockito-scala)
2424

2525

26+
## Note: For more examples and use cases than the ones shown below, please refer to the library's [tests](https://github.com/mockito/mockito-scala/blob/master/core/src/test)
27+
2628
## Getting started
2729

2830
## `org.mockito.MockitoSugar`
@@ -98,7 +100,9 @@ aMock.stringArgument("it worked!")
98100

99101
verify(aMock).stringArgument(captor)
100102

101-
captor <-> "it worked!"
103+
captor <-> "it worked!"
104+
//or
105+
captor shouldHave "it worked!"
102106
```
103107

104108
As you can see there is no need to call `capture()` nor `getValue` anymore (although they're still there if you need them)
@@ -110,20 +114,20 @@ Both `Captor[T]` and `ValCaptor[T]` return an instance of `ArgCaptor[T]` so the
110114
## `org.mockito.MockitoScalaSession`
111115

112116
This is a wrapper around `org.mockito.MockitoSession`, it's main purpose (on top of having a Scala API)
113-
is to filter out the `$default$` stubbings so they are not wrongly reported when we use Strict Stubs
117+
is to improve the search of mis-used mocks and unexpected invocations to reduce debugging effort when something doesn't work
114118

115-
To use it just create an instance of it before your test code and call `finishMocking()` when your test is done, e.g.
119+
To use it just wrap your code with it, e.g.
116120
```scala
117-
val session = MockitoScalaSession()
118-
119-
val foo = mock[Foo]
120-
when(foo.bar("pepe")) thenReturn "mocked"
121-
foo.bar("pepe") shouldBe "mocked"
122-
123-
session.finishMocking()
121+
MockitoScalaSession().run {
122+
val foo = mock[Foo]
123+
when(foo.bar("pepe")) thenReturn "mocked"
124+
foo.bar("pepe") shouldBe "mocked"
125+
}
124126
```
127+
That's it! that block of code will execute within a session which will take care of checking the use of the framework and,
128+
if the test fails, it will try to find out if the failure could be related to a mock being used incorrectly
125129

126-
## `org.mockito.integrations.scalatest.MockitoFixture`
130+
## MockitoFixture
127131

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

@@ -137,15 +141,23 @@ the mockito-scala API available in one go, i.e.
137141
class MyTest extends WordSpec with MockitoFixture
138142
```
139143

144+
In case you want to use the Idiomatic Syntax just do
145+
146+
```scala
147+
class MyTest extends WordSpec with IdiomaticMockitoFixture
148+
```
149+
140150
## `org.mockito.integrations.scalatest.ResetMocksAfterEachTest`
141151

142152
Inspired by [this](https://stackoverflow.com/questions/51387234/is-there-a-per-test-non-specific-mock-reset-pattern-using-scalaplayspecmockito) StackOverflow question,
143153
mockito-scala provides this trait that helps to automatically reset any existent mock after each test is run
144154
The trait has to be mixed **after** `org.mockito.MockitoSugar` in order to work, otherwise your test will not compile
145155
The code shown in the StackOverflow question would look like this if using this mechanism
146156

157+
NOTE: MockitoFixture and ResetMocksAfterEachTest are mutually exclusive, so don't expect them to work together
158+
147159
```scala
148-
TestClass extends PlaySpec with MockitoSugar with ResetMocksAfterEachTest
160+
class MyTest extends PlaySpec with MockitoSugar with ResetMocksAfterEachTest
149161

150162
private val foo = mock[Foo]
151163

@@ -206,6 +218,56 @@ As you can see the new syntax reads a bit more natural, also notice you can use
206218

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

221+
## Default Answers
222+
We defined a new type `org.mockito.DefaultAnswer` which is used to configure the default behaviour of a mock when a non-stubbed invocation
223+
is made on it, the default behaviour is different to the Java version, instead of returning null for any non-primitive or non-final class,
224+
mockito-scala will return a "Smart Null", which is basically a mock of the type returned by the called method.
225+
The main advantage of this is that if the code tries to call any method on this mock, instead of failing with a NPE we will
226+
throw a different exception with a hint of the non-stubbed method call (including its params) that returned this Smart Null,
227+
this will make it much easier to find and fix a non-stubbed call
228+
229+
Most of the Answers defined in `org.mockito.Answers` have it's counterpart as a `org.mockito.DefaultAnswer`, and on top of that
230+
we also provide `org.mockito.ReturnsEmptyValues` which will try its best to return an empty object for well known types,
231+
i.e. `Nil` for `List`, `None` for `Option` etc.
232+
This DefaultAnswer is not part of the default behaviour as we think a SmartNull is better, to explain why, let's imagine we
233+
have the following code.
234+
235+
```scala
236+
class UserRepository {
237+
def find(id: Int): Option[User]
238+
}
239+
class UserController(userRepository: UserRepository) {
240+
def get(userId: Int): Option[Json] = userRepository.find(userId).map(_.toJson)
241+
}
242+
243+
class UserControllerTest extends WordSpec with IdiomaticMockito {
244+
245+
"get" should {
246+
"return the expected json" in {
247+
val repo = mock[UserRepository]
248+
val testObj = new UserController(repo)
249+
250+
testObj.get(123) shouldBe Some(Json(....)) //overly simplified for clarity
251+
}
252+
}
253+
}
254+
```
255+
256+
Now, in that example that test could fail in 3 different ways
257+
258+
1) With the standard implementation of Mockito, the mock would return null and we would get a NullPointerException, which we all agree it's far from ideal, as it's hard to know where did it happen in non trivial code
259+
2) With the default/empty values, we would get a `None`, so the final result would be `None` and we will get an assertion error as `None` is not `Some(Json(....))`, but I'm not sure how much improvement over the NPE this would be, because in a non-trivial method we may have many dependencies returning `Option` and it could be hard to track down which one is returning `None` and why
260+
3) With a smart-null, we would return a `mock[Option]` and as soon as our code calls to `.map()` that mock would fail with an exception telling you what non-stubbed method was called and on which mock (in the example would say something you called the `find` method on some `mock of type UserRepository`)
261+
262+
And that's why we use option 3 as default
263+
264+
Of course you can override the default behaviour, for this you have 2 options
265+
266+
1) If you wanna do it just for a particular mock, you can, at creation time do `mock[MyType](MyDefaultAnswer)`
267+
2) If you wanna do it for all the mocks in a test, you can define an `implicit`, i.e. `implicit val defaultAnswer: DefaultAnswer = MyDefaultAnswer`
268+
269+
DefaultAnswers are also composable, so for example if you wanted empty values first and then smart nulls you could do `implicit val defaultAnswer: DefaultAnswer = ReturnsEmptyValues orElse ReturnsSmartNulls`
270+
209271
## Experimental features
210272

211273
* **by-name** arguments is currently an experimental feature as the implementation is a bit hacky and it gave some people problems

core/src/main/java/org/mockito/MockitoEnhancerUtil.java

Lines changed: 0 additions & 70 deletions
This file was deleted.

core/src/main/scala-2.11/org/mockito/MockitoSugar.scala

Lines changed: 0 additions & 10 deletions
This file was deleted.

core/src/main/scala-2.12/org/mockito/MockitoSugar.scala

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package org.mockito
2+
3+
import java.lang.reflect.Modifier.{isAbstract, isFinal}
4+
5+
import org.mockito.exceptions.base.MockitoException
6+
import org.mockito.exceptions.verification.SmartNullPointerException
7+
import org.mockito.internal.util.ObjectMethodsGuru.isToStringMethod
8+
import org.mockito.invocation.InvocationOnMock
9+
import org.mockito.stubbing.Answer
10+
import org.mockito.Answers._
11+
import org.mockito.internal.stubbing.defaultanswers.ReturnsMoreEmptyValues
12+
13+
import scala.concurrent.Future
14+
import scala.util.{Failure, Try}
15+
16+
trait DefaultAnswer extends Answer[Any] with Function[InvocationOnMock, Option[Any]] { self =>
17+
override def answer(invocation: InvocationOnMock): Any =
18+
if (invocation.getMethod.getName.contains("$default$") && !isAbstract(invocation.getMethod.getModifiers))
19+
invocation.callRealMethod()
20+
else
21+
apply(invocation).orNull
22+
23+
def orElse(next: DefaultAnswer): DefaultAnswer = new DefaultAnswer {
24+
override def apply(invocation: InvocationOnMock): Option[Any] = self(invocation).orElse(next(invocation))
25+
}
26+
}
27+
28+
object DefaultAnswer {
29+
implicit val defaultAnswer: DefaultAnswer = ReturnsSmartNulls
30+
}
31+
32+
object ReturnsDefaults extends DefaultAnswer {
33+
override def apply(invocation: InvocationOnMock): Option[Any] = Option(RETURNS_DEFAULTS.answer(invocation))
34+
}
35+
36+
object ReturnsSmartNulls extends DefaultAnswer {
37+
override def apply(invocation: InvocationOnMock): Option[Any] = Option(RETURNS_DEFAULTS.answer(invocation)).orElse {
38+
val returnType = invocation.getMethod.getReturnType
39+
40+
if (!returnType.isPrimitive && !isFinal(returnType.getModifiers))
41+
Some(Mockito.mock(returnType, ThrowsSmartNullPointer(invocation)))
42+
else
43+
None
44+
}
45+
46+
private case class ThrowsSmartNullPointer(unStubbedInvocation: InvocationOnMock) extends Answer[Any] {
47+
48+
override def answer(currentInvocation: InvocationOnMock): Any =
49+
if (isToStringMethod(currentInvocation.getMethod))
50+
s"""SmartNull returned by this un-stubbed method call on a mock:
51+
|${unStubbedInvocation.toString}""".stripMargin
52+
else
53+
throw new SmartNullPointerException(
54+
s"""You have a NullPointerException because this method call was *not* stubbed correctly:
55+
|[$unStubbedInvocation] on the Mock [${unStubbedInvocation.getMock}]""".stripMargin)
56+
}
57+
}
58+
59+
object ReturnsDeepStubs extends DefaultAnswer {
60+
override def apply(invocation: InvocationOnMock): Option[Any] = Option(RETURNS_DEEP_STUBS.answer(invocation))
61+
}
62+
63+
object CallsRealMethods extends DefaultAnswer {
64+
override def apply(invocation: InvocationOnMock): Option[Any] = Option(CALLS_REAL_METHODS.answer(invocation))
65+
}
66+
67+
object ReturnsEmptyValues extends DefaultAnswer {
68+
private val javaEmptyValuesAndPrimitives = new ReturnsMoreEmptyValues
69+
70+
private[mockito] lazy val emptyValues: Map[Class[_], AnyRef] = Map(
71+
classOf[Option[_]] -> Option.empty,
72+
classOf[List[_]] -> List.empty,
73+
classOf[Set[_]] -> Set.empty,
74+
classOf[Seq[_]] -> Seq.empty,
75+
classOf[Iterable[_]] -> Iterable.empty,
76+
classOf[Traversable[_]] -> Traversable.empty,
77+
classOf[IndexedSeq[_]] -> IndexedSeq.empty,
78+
classOf[Iterator[_]] -> Iterator.empty,
79+
classOf[Stream[_]] -> Stream.empty,
80+
classOf[Vector[_]] -> Vector.empty,
81+
classOf[Try[_]] -> Failure(new MockitoException("Auto stub provided by mockito-scala")),
82+
classOf[Future[_]] -> Future.failed(new MockitoException("Auto stub provided by mockito-scala")),
83+
classOf[BigDecimal] -> BigDecimal(0),
84+
classOf[BigInt] -> BigInt(0),
85+
classOf[StringBuilder] -> StringBuilder.newBuilder
86+
)
87+
88+
override def apply(invocation: InvocationOnMock): Option[Any] =
89+
Option(javaEmptyValuesAndPrimitives.answer(invocation)).orElse(emptyValues.get(invocation.getMethod.getReturnType))
90+
}

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
package org.mockito
22

3-
import org.mockito.stubbing.{ Answer, OngoingStubbing }
4-
import MockitoSugar.{ verify, _ }
3+
import org.mockito.stubbing.{Answer, OngoingStubbing}
4+
import org.mockito.MockitoSugar.{verify, _}
55

66
import scala.reflect.ClassTag
77
import scala.reflect.runtime.universe.TypeTag
88

99
trait IdiomaticMockito extends MockCreator {
1010

11-
override def mock[T <: AnyRef: ClassTag: TypeTag](name: String): T = MockitoSugar.mock[T](name)
11+
override def mock[T <: AnyRef: ClassTag: TypeTag](name: String)(implicit defaultAnswer: DefaultAnswer): T =
12+
MockitoSugar.mock[T](name)
1213

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

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

17-
override def mock[T <: AnyRef: ClassTag: TypeTag]: T = MockitoSugar.mock[T]
18+
override def mock[T <: AnyRef: ClassTag: TypeTag](implicit defaultAnswer: DefaultAnswer): T =
19+
MockitoSugar.mock[T]
1820

1921
override def spy[T](realObj: T): T = MockitoSugar.spy(realObj)
2022

@@ -207,8 +209,14 @@ trait IdiomaticMockito extends MockCreator {
207209
}
208210

209211
object InOrder {
210-
def apply(mocks: AnyRef*)(verifications: Option[InOrder] => Unit): Unit = verifications(Some(Mockito.inOrder(mocks: _*)))
212+
def apply(mocks: AnyRef*)(verifications: Option[InOrder] => Unit): Unit =
213+
verifications(Some(Mockito.inOrder(mocks: _*)))
211214
}
212215

213216
def *[T]: T = ArgumentMatchersSugar.any[T]
214217
}
218+
219+
/**
220+
* Simple object to allow the usage of the trait without mixing it in
221+
*/
222+
object IdiomaticMockito extends IdiomaticMockito

0 commit comments

Comments
 (0)