Skip to content

Commit 76ae241

Browse files
authored
Merge pull request #39 from mockito/smart-deep-stub
DeepStubs play nicely with ScalaMockitoSession/MockitoFixture
2 parents ac5ae92 + 4bea9ca commit 76ae241

11 files changed

+193
-106
lines changed

README.md

+13-8
Original file line numberDiff line numberDiff line change
@@ -249,14 +249,19 @@ Check the [tests](https://github.com/mockito/mockito-scala/blob/master/core/src/
249249

250250
## Default Answers
251251
We defined a new type `org.mockito.DefaultAnswer` which is used to configure the default behaviour of a mock when a non-stubbed invocation
252-
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,
253-
mockito-scala will return a "Smart Null", which is basically a mock of the type returned by the called method.
254-
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
255-
throw a different exception with a hint of the non-stubbed method call (including its params) that returned this Smart Null,
256-
this will make it much easier to find and fix a non-stubbed call
257-
258-
Most of the Answers defined in `org.mockito.Answers` have it's counterpart as a `org.mockito.DefaultAnswer`, and on top of that
259-
we also provide `org.mockito.ReturnsEmptyValues` which will try its best to return an empty object for well known types,
252+
is made on it.
253+
254+
The object `org.mockito.DefaultAnswers` contains each one of the provided ones
255+
256+
All the mocks created will use `ReturnsSmartNulls` by default, this is different to the Java version, which returns null for any non-primitive or non-final class.
257+
258+
A "Smart Null", is nothing else than a mock of the type returned by the called method.
259+
The main advantage of doing that is that if the code tries to call any method on this mock, instead of failing with a NPE the mock will
260+
throw a different exception with a hint of the non-stubbed method that was called (including its params),
261+
this should make much easier the task of finding and fixing non-stubbed calls
262+
263+
Most of the Answers defined in `org.mockito.Answers` have it's counterpart in `org.mockito.DefaultAnswers`, and on top of that
264+
we also provide `ReturnsEmptyValues` which will try its best to return an empty object for well known types,
260265
i.e. `Nil` for `List`, `None` for `Option` etc.
261266
This DefaultAnswer is not part of the default behaviour as we think a SmartNull is better, to explain why, let's imagine we
262267
have the following code.
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package org.mockito
22

3-
import java.lang.reflect.Modifier.{isAbstract, isFinal}
3+
import java.lang.reflect.Modifier.isAbstract
44

55
import org.mockito.exceptions.base.MockitoException
6-
import org.mockito.exceptions.verification.SmartNullPointerException
7-
import org.mockito.internal.util.ObjectMethodsGuru.isToStringMethod
86
import org.mockito.invocation.InvocationOnMock
97
import org.mockito.stubbing.Answer
108
import org.mockito.Answers._
@@ -27,43 +25,18 @@ trait DefaultAnswer extends Answer[Any] with Function[InvocationOnMock, Option[A
2725

2826
object DefaultAnswer {
2927
implicit val defaultAnswer: DefaultAnswer = ReturnsSmartNulls
30-
}
3128

32-
object ReturnsDefaults extends DefaultAnswer {
33-
override def apply(invocation: InvocationOnMock): Option[Any] = Option(RETURNS_DEFAULTS.answer(invocation))
29+
def apply(from: Answer[_]): DefaultAnswer = new DecoratedAnswer(from)
3430
}
3531

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))
32+
class DecoratedAnswer(from: Answer[_]) extends DefaultAnswer {
33+
override def apply(invocation: InvocationOnMock): Option[Any] = Option(from.answer(invocation))
6534
}
6635

36+
object ReturnsDefaults extends DecoratedAnswer(RETURNS_DEFAULTS)
37+
object ReturnsDeepStubs extends DecoratedAnswer(RETURNS_DEEP_STUBS)
38+
object CallsRealMethods extends DecoratedAnswer(CALLS_REAL_METHODS)
39+
object ReturnsSmartNulls extends DecoratedAnswer(RETURNS_SMART_NULLS)
6740
object ReturnsEmptyValues extends DefaultAnswer {
6841
private val javaEmptyValuesAndPrimitives = new ReturnsMoreEmptyValues
6942

@@ -88,3 +61,14 @@ object ReturnsEmptyValues extends DefaultAnswer {
8861
override def apply(invocation: InvocationOnMock): Option[Any] =
8962
Option(javaEmptyValuesAndPrimitives.answer(invocation)).orElse(emptyValues.get(invocation.getMethod.getReturnType))
9063
}
64+
65+
/**
66+
* Simple object to act as an 'enum' of DefaultAnswers
67+
*/
68+
object DefaultAnswers {
69+
val ReturnsDefaults: DefaultAnswer = org.mockito.ReturnsDefaults
70+
val ReturnsDeepStubs: DefaultAnswer = org.mockito.ReturnsDeepStubs
71+
val CallsRealMethods: DefaultAnswer = org.mockito.CallsRealMethods
72+
val ReturnsSmartNulls: DefaultAnswer = org.mockito.ReturnsSmartNulls
73+
val ReturnsEmptyValues: DefaultAnswer = org.mockito.ReturnsEmptyValues
74+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ trait IdiomaticMockito extends MockCreator {
1313

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

16-
override def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: Answer[_]): T = MockitoSugar.mock[T](defaultAnswer)
16+
override def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: DefaultAnswer): T = MockitoSugar.mock[T](defaultAnswer)
1717

1818
override def mock[T <: AnyRef: ClassTag: TypeTag](implicit defaultAnswer: DefaultAnswer): T =
1919
MockitoSugar.mock[T]

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

+11-9
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,20 @@ import scala.reflect.runtime.universe.TypeTag
2727

2828
private[mockito] trait MockCreator {
2929
def mock[T <: AnyRef: ClassTag: TypeTag](implicit defaultAnswer: DefaultAnswer): T
30-
def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: Answer[_]): T
30+
def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: Answer[_]): T = mock[T](DefaultAnswer(defaultAnswer))
31+
def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: DefaultAnswer): T
3132
def mock[T <: AnyRef: ClassTag: TypeTag](mockSettings: MockSettings): T
3233
def mock[T <: AnyRef: ClassTag: TypeTag](name: String)(implicit defaultAnswer: DefaultAnswer): T
3334

3435
def spy[T](realObj: T): T
3536
def spyLambda[T <: AnyRef: ClassTag](realObj: T): T
37+
38+
/**
39+
* Delegates to <code>Mockito.withSettings()</code>, it's only here to expose the full Mockito API
40+
*/
41+
def withSettings(implicit defaultAnswer: DefaultAnswer): MockSettings =
42+
Mockito.withSettings().defaultAnswer(defaultAnswer)
43+
3644
}
3745

3846
//noinspection MutatorLikeMethodIsParameterless
@@ -115,8 +123,8 @@ private[mockito] trait MockitoEnhancer extends MockCreator {
115123
* <code>verify(aMock).iHaveSomeDefaultArguments("I'm not gonna pass the second argument", "default value")</code>
116124
* as the value for the second parameter would have been null...
117125
*/
118-
override def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: Answer[_]): T =
119-
mock(withSettings(defaultAnswer.lift))
126+
override def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: DefaultAnswer): T =
127+
mock(withSettings(defaultAnswer))
120128

121129
/**
122130
* Delegates to <code>Mockito.mock(type: Class[T], mockSettings: MockSettings)</code>
@@ -208,12 +216,6 @@ private[mockito] trait MockitoEnhancer extends MockCreator {
208216
*/
209217
def mockingDetails(toInspect: AnyRef): MockingDetails = Mockito.mockingDetails(toInspect)
210218

211-
/**
212-
* Delegates to <code>Mockito.withSettings()</code>, it's only here to expose the full Mockito API
213-
*/
214-
def withSettings(implicit defaultAnswer: DefaultAnswer): MockSettings =
215-
Mockito.withSettings().defaultAnswer(defaultAnswer)
216-
217219
/**
218220
* Delegates to <code>Mockito.verifyNoMoreInteractions(Object... mocks)</code>, but ignores the default stubs that
219221
* deal with default argument values

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

+64-24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package org.mockito
22

3-
import org.mockito.MockitoScalaSession.UnexpectedInvocationsMockListener
4-
import org.mockito.exceptions.misusing.UnexpectedInvocationException
3+
import org.mockito.MockitoScalaSession.{MockitoScalaSessionListener, UnexpectedInvocations}
4+
import org.mockito.exceptions.misusing.{UnexpectedInvocationException, UnnecessaryStubbingException}
5+
import org.mockito.internal.stubbing.StubbedInvocationMatcher
56
import org.mockito.invocation.{DescribedInvocation, Invocation, Location}
67
import org.mockito.listeners.MockCreationListener
78
import org.mockito.mock.MockCreationSettings
@@ -13,7 +14,7 @@ import scala.collection.mutable
1314
import scala.collection.JavaConverters._
1415

1516
class MockitoScalaSession(name: String, strictness: Strictness, logger: MockitoSessionLogger) {
16-
private val listener = new UnexpectedInvocationsMockListener
17+
private val listener = new MockitoScalaSessionListener
1718
private val mockitoSession = Mockito.mockitoSession().name(name).logger(logger).strictness(strictness).startMocking()
1819

1920
Mockito.framework().addListener(listener)
@@ -22,16 +23,17 @@ class MockitoScalaSession(name: String, strictness: Strictness, logger: MockitoS
2223
try {
2324
t.fold {
2425
mockitoSession.finishMocking()
25-
listener.reportUnStubbedCalls().reportUnexpectedInvocations()
26+
listener.reportIssues().foreach(_.report())
2627
} {
2728
case e: NullPointerException =>
2829
mockitoSession.finishMocking(e)
29-
val unStubbedCalls = listener.reportUnStubbedCalls()
30-
if (unStubbedCalls.nonEmpty)
31-
throw new UnexpectedInvocationException(s"""A NullPointerException was thrown, check if maybe related to
32-
|$unStubbedCalls""".stripMargin,
33-
e)
34-
else throw e
30+
listener.reportIssues().foreach {
31+
case unStubbedCalls: UnexpectedInvocations if unStubbedCalls.nonEmpty =>
32+
throw new UnexpectedInvocationException(s"""A NullPointerException was thrown, check if maybe related to
33+
|$unStubbedCalls""".stripMargin,
34+
e)
35+
case _ => throw e
36+
}
3537
case other =>
3638
mockitoSession.finishMocking(other)
3739
throw other
@@ -63,7 +65,11 @@ object MockitoScalaSession {
6365
override def getLocation: Location = SyntheticLocation
6466
}
6567

66-
case class UnexpectedInvocations(invocations: Set[Invocation]) {
68+
trait Reporter {
69+
def report(): Unit
70+
}
71+
72+
case class UnexpectedInvocations(invocations: Set[Invocation]) extends Reporter {
6773
def nonEmpty: Boolean = invocations.nonEmpty
6874

6975
override def toString: String =
@@ -80,25 +86,59 @@ object MockitoScalaSession {
8086
|Please make sure you aren't missing any stubbing or that your code actually does what you want""".stripMargin
8187
} else "No unexpected invocations found"
8288

83-
def reportUnexpectedInvocations(): Unit =
84-
if (nonEmpty) throw new UnexpectedInvocationException(toString)
89+
def report(): Unit = if (nonEmpty) throw new UnexpectedInvocationException(toString)
8590
}
8691

87-
class UnexpectedInvocationsMockListener extends MockCreationListener {
88-
def reportUnStubbedCalls(): UnexpectedInvocations =
89-
UnexpectedInvocations(
90-
mocks
91-
.map(MockitoSugar.mockingDetails)
92-
.flatMap(_.getInvocations.asScala)
93-
.filter(_.stubInfo() == null)
94-
.filterNot(_.isVerified)
95-
.filterNot(_.getMethod.getName.contains("$default$"))
96-
.toSet
92+
case class UnusedStubbings(stubbings: Set[StubbedInvocationMatcher]) extends Reporter {
93+
def nonEmpty: Boolean = stubbings.nonEmpty
94+
95+
override def toString: String =
96+
if (nonEmpty) {
97+
val locations = stubbings.zipWithIndex
98+
.map {
99+
case (stubbing, idx) => s"${idx + 1}. $stubbing ${stubbing.getLocation}"
100+
}
101+
.mkString("\n")
102+
s"""Unnecessary stubbings detected.
103+
|
104+
|Clean & maintainable test code requires zero unnecessary code.
105+
|Following stubbings are unnecessary (click to navigate to relevant line of code):
106+
|$locations
107+
|Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.""".stripMargin
108+
} else "No unexpected invocations found"
109+
110+
def report(): Unit = if (nonEmpty) throw new UnnecessaryStubbingException(toString)
111+
}
112+
113+
class MockitoScalaSessionListener extends MockCreationListener {
114+
def reportIssues(): Seq[Reporter] = {
115+
val mockDetails = mocks.toSet.map(MockitoSugar.mockingDetails)
116+
117+
val stubbings = mockDetails
118+
.flatMap(_.getStubbings.asScala)
119+
.collect {
120+
case s: StubbedInvocationMatcher => s
121+
}
122+
123+
val invocations = mockDetails.flatMap(_.getInvocations.asScala)
124+
125+
val unexpectedInvocations = invocations
126+
.filterNot(_.isVerified)
127+
.filterNot(_.getMethod.getName.contains("$default$"))
128+
.filterNot(i => stubbings.exists(_.matches(i)))
129+
130+
val unusedStubbings = stubbings.filterNot(sm => invocations.exists(sm.matches)).filter(!_.wasUsed())
131+
132+
Seq(
133+
UnexpectedInvocations(unexpectedInvocations),
134+
UnusedStubbings(unusedStubbings)
97135
)
136+
}
98137

99138
private val mocks = mutable.Set.empty[AnyRef]
100139

101-
override def onMockCreated(mock: AnyRef, settings: MockCreationSettings[_]): Unit = mocks += mock
140+
override def onMockCreated(mock: AnyRef, settings: MockCreationSettings[_]): Unit =
141+
if (!settings.isLenient) mocks += mock
102142
}
103143
}
104144

core/src/main/scala/org/mockito/integrations/scalatest/ResetMocksAfterEachTest.scala

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package org.mockito.integrations.scalatest
33
import java.util.concurrent.ConcurrentHashMap
44

55
import org.mockito.{DefaultAnswer, MockCreator, MockitoSugar, MockSettings}
6-
import org.mockito.stubbing.Answer
76
import org.scalatest.{Outcome, TestSuite}
87

98
import scala.collection.JavaConverters._
@@ -37,7 +36,7 @@ trait ResetMocksAfterEachTest extends TestSuite with MockCreator { self: MockCre
3736
abstract override def mock[T <: AnyRef: ClassTag: TypeTag](implicit defaultAnswer: DefaultAnswer): T =
3837
addMock(super.mock[T])
3938

40-
abstract override def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: Answer[_]): T =
39+
abstract override def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: DefaultAnswer): T =
4140
addMock(super.mock[T](defaultAnswer))
4241

4342
abstract override def mock[T <: AnyRef: ClassTag: TypeTag](mockSettings: MockSettings): T =

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

-6
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,4 @@ package object mockito {
112112
i.getArgument[P9](9),
113113
i.getArgument[P10](10)
114114
))
115-
116-
implicit class AnswerOps[T](val a: Answer[T]) extends AnyVal {
117-
def lift: DefaultAnswer = new DefaultAnswer {
118-
override def apply(invocation: InvocationOnMock): Option[Any] = Option(a.answer(invocation))
119-
}
120-
}
121115
}

core/src/test/scala-2.11/org/mockito/MockitoSugarTest_211.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class MockitoSugarTest_211 extends WordSpec with MockitoSugar with scalatest.Mat
1717

1818
aMock.traitMethod() shouldBe 69
1919

20-
verify(aMock).traitMethod(0, null)
20+
verify(aMock).traitMethod(0, "")
2121
}
2222
}
2323
}

core/src/test/scala/org/mockito/DefaultAnswerTest.scala

+5-9
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,11 @@ class DefaultAnswerTest
9595

9696
smartNull should not be null
9797

98-
val throwable: SmartNullPointerException = the[SmartNullPointerException] thrownBy {
98+
val throwable = the[SmartNullPointerException] thrownBy {
9999
smartNull.callMeMaybe()
100100
}
101101

102-
throwable.getMessage shouldBe
103-
s"""You have a NullPointerException because this method call was *not* stubbed correctly:
104-
|[foo.userClass(42);] on the Mock [$aMock]""".stripMargin
102+
throwable.getMessage should include("You have a NullPointerException here:")
105103
}
106104

107105
"return a smart standard monad" in {
@@ -113,9 +111,7 @@ class DefaultAnswerTest
113111
smartNull.isEmpty
114112
}
115113

116-
throwable.getMessage shouldBe
117-
s"""You have a NullPointerException because this method call was *not* stubbed correctly:
118-
|[foo.returnsList();] on the Mock [$aMock]""".stripMargin
114+
throwable.getMessage should include("You have a NullPointerException here:")
119115
}
120116

121117
"return a default value for primitives" in {
@@ -138,7 +134,7 @@ class DefaultAnswerTest
138134

139135
"ReturnsEmptyValues" should {
140136
"return a default value for primitives" in {
141-
val primitives = mock[Primitives](ReturnsEmptyValues)
137+
val primitives = mock[Primitives](DefaultAnswers.ReturnsEmptyValues)
142138

143139
primitives.barByte shouldBe 0.toByte
144140
primitives.barBoolean shouldBe false
@@ -151,7 +147,7 @@ class DefaultAnswerTest
151147
}
152148

153149
"return the empty values for known classes" in {
154-
val aMock = mock[KnownTypes](ReturnsEmptyValues)
150+
val aMock = mock[KnownTypes](DefaultAnswers.ReturnsEmptyValues)
155151

156152
aMock.returnsOption shouldBe None
157153
aMock.returnsList shouldBe List.empty

0 commit comments

Comments
 (0)