Skip to content

Commit aa7a4c2

Browse files
committed
New default answer to better handle scala empty values, default argument synthetic methods and smart-nulls mocks
1 parent 83b9669 commit aa7a4c2

File tree

2 files changed

+167
-0
lines changed

2 files changed

+167
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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.stubbing.defaultanswers.ReturnsMoreEmptyValues
8+
import org.mockito.internal.util.ObjectMethodsGuru.isToStringMethod
9+
import org.mockito.invocation.InvocationOnMock
10+
import org.mockito.stubbing.Answer
11+
12+
import scala.util.{ Failure, Try }
13+
14+
object ScalaDefaultAnswer extends Answer[Any] {
15+
16+
private val delegate = new ReturnsMoreEmptyValues
17+
18+
override def answer(invocation: InvocationOnMock): Any =
19+
if (invocation.getMethod.getName.contains("$default$") && !isAbstract(invocation.getMethod.getModifiers))
20+
invocation.callRealMethod()
21+
else
22+
Option(delegate.answer(invocation))
23+
.orElse(emptyValues.get(invocation.getMethod.getReturnType))
24+
.orElse(smartNull(invocation))
25+
.orNull
26+
27+
private def smartNull(invocation: InvocationOnMock): Option[Any] = {
28+
val returnType = invocation.getMethod.getReturnType
29+
30+
if (!returnType.isPrimitive && !isFinal(returnType.getModifiers))
31+
Some(Mockito.mock(returnType, ThrowsSmartNullPointer(invocation)))
32+
else
33+
None
34+
}
35+
36+
private case class ThrowsSmartNullPointer(unStubbedInvocation: InvocationOnMock) extends Answer[Any] {
37+
38+
override def answer(currentInvocation: InvocationOnMock): Any =
39+
if (isToStringMethod(currentInvocation.getMethod))
40+
s"""SmartNull returned by this un-stubbed method call on a mock:
41+
|${unStubbedInvocation.toString}""".stripMargin
42+
else
43+
throw new SmartNullPointerException(
44+
s"""You have a NullPointerException because this method call was *not* stubbed correctly:
45+
|[$unStubbedInvocation] on the Mock [${unStubbedInvocation.getMock}]""".stripMargin)
46+
}
47+
48+
private[mockito] lazy val emptyValues: Map[Class[_], AnyRef] = Map(
49+
classOf[Option[_]] -> Option.empty,
50+
classOf[List[_]] -> List.empty,
51+
classOf[Set[_]] -> Set.empty,
52+
classOf[Seq[_]] -> Seq.empty,
53+
classOf[Iterable[_]] -> Iterable.empty,
54+
classOf[Traversable[_]] -> Traversable.empty,
55+
classOf[IndexedSeq[_]] -> IndexedSeq.empty,
56+
classOf[Iterator[_]] -> Iterator.empty,
57+
classOf[Stream[_]] -> Stream.empty,
58+
classOf[Vector[_]] -> Vector.empty,
59+
classOf[Try[_]] -> Failure(new MockitoException("Auto stub provided by mockito-scala")),
60+
classOf[BigDecimal] -> BigDecimal(0),
61+
classOf[BigInt] -> BigInt(0),
62+
classOf[StringBuilder] -> StringBuilder.newBuilder
63+
)
64+
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package org.mockito
2+
3+
import org.scalatest
4+
import org.mockito.exceptions.verification.SmartNullPointerException
5+
import org.mockito.ScalaDefaultAnswerTest._
6+
import org.mockito.exceptions.base.MockitoException
7+
import org.scalatest.{EitherValues, FlatSpec, TryValues}
8+
9+
import scala.util.{Success, Try}
10+
11+
object ScalaDefaultAnswerTest {
12+
class Foo {
13+
def bar(a: String) = "bar"
14+
15+
def baz(a: String = "default"): String = a
16+
17+
def some(i: Int): Option[String] = None
18+
19+
def valueClass: ValueClass = ValueClass(42)
20+
21+
def userClass(v: Int = 42): Bar = new Bar
22+
}
23+
24+
case class ValueClass(v: Int) extends AnyVal
25+
26+
class Bar {
27+
def callMeMaybe(): Unit = ()
28+
}
29+
30+
class KnownTypes {
31+
def returnsOption: Option[String] = Some("not mocked!")
32+
def returnsList: List[String] = List("not mocked!")
33+
def returnsSet: Set[String] = Set("not mocked!")
34+
def returnsSeq: Seq[String] = Seq("not mocked!")
35+
def returnsIterable: Iterable[String] = Iterable("not mocked!")
36+
def returnsTraversable: Traversable[String] = Traversable("not mocked!")
37+
def returnsIndexedSeq: IndexedSeq[String] = IndexedSeq("not mocked!")
38+
def returnsIterator: Iterator[String] = Iterator("not mocked!")
39+
def returnsStream: Stream[String] = Stream("not mocked!")
40+
def returnsVector: Vector[String] = Vector("not mocked!")
41+
def returnsEither: Either[Boolean, String] = Right("not mocked!")
42+
def returnsTry: Try[String] = Success("not mocked!")
43+
def returnsBigDecimal: BigDecimal = BigDecimal(42)
44+
def returnsBigInt: BigInt = BigInt(42)
45+
def returnsStringBuilder: StringBuilder = StringBuilder.newBuilder.append("not mocked!")
46+
}
47+
}
48+
49+
class ScalaDefaultAnswerTest extends FlatSpec with scalatest.Matchers with IdiomaticMockito with TryValues with EitherValues {
50+
51+
trait Setup {
52+
val aMock: Foo = mock[Foo](ScalaDefaultAnswer)
53+
}
54+
55+
it should "call real method for default arguments" in new Setup {
56+
aMock baz ()
57+
58+
aMock wasCalled on baz "default"
59+
}
60+
61+
it should "return an empty instance for a known class" in new Setup {
62+
aMock.some(42) shouldBe empty
63+
}
64+
65+
it should "return a smart null for unknown cases" in new Setup {
66+
val smartNull: Bar = aMock.userClass()
67+
68+
smartNull should not be null
69+
70+
val throwable: SmartNullPointerException = the[SmartNullPointerException] thrownBy {
71+
smartNull.callMeMaybe()
72+
}
73+
74+
throwable.getMessage shouldBe
75+
s"""You have a NullPointerException because this method call was *not* stubbed correctly:
76+
|[foo.userClass(42);] on the Mock [$aMock]""".stripMargin
77+
}
78+
79+
it should "return a smart value for value classes" in new Setup {
80+
aMock.valueClass.v shouldBe 0
81+
}
82+
83+
it should "return the empty values for known classes" in {
84+
val aMock = mock[KnownTypes](ScalaDefaultAnswer)
85+
86+
aMock.returnsOption shouldBe None
87+
aMock.returnsList shouldBe List.empty
88+
aMock.returnsSet shouldBe Set.empty
89+
aMock.returnsSeq shouldBe Seq.empty
90+
aMock.returnsIterable shouldBe Iterable.empty
91+
aMock.returnsTraversable shouldBe Traversable.empty
92+
aMock.returnsIndexedSeq shouldBe IndexedSeq.empty
93+
aMock.returnsIterator shouldBe Iterator.empty
94+
aMock.returnsStream shouldBe Stream.empty
95+
aMock.returnsVector shouldBe Vector.empty
96+
aMock.returnsTry.failure.exception shouldBe a[MockitoException]
97+
aMock.returnsTry.failure.exception.getMessage shouldBe "Auto stub provided by mockito-scala"
98+
aMock.returnsBigDecimal shouldBe BigDecimal(0)
99+
aMock.returnsBigInt shouldBe BigInt(0)
100+
aMock.returnsStringBuilder shouldBe StringBuilder.newBuilder
101+
}
102+
}

0 commit comments

Comments
 (0)