From 2103bf50398438bc1be5d65306807ef98978f134 Mon Sep 17 00:00:00 2001 From: Bruno Bonanno <241804+bbonanno@users.noreply.github.com> Date: Wed, 29 Aug 2018 18:39:43 +0100 Subject: [PATCH 1/6] Make consistent use of DefaultAnswer --- .../scala/org/mockito/DefaultAnswer.scala | 19 +++++++++---------- .../scala/org/mockito/IdiomaticMockito.scala | 2 +- .../main/scala/org/mockito/MockitoAPI.scala | 7 ++++--- .../scalatest/ResetMocksAfterEachTest.scala | 3 +-- core/src/main/scala/org/mockito/package.scala | 6 ------ 5 files changed, 15 insertions(+), 22 deletions(-) diff --git a/core/src/main/scala/org/mockito/DefaultAnswer.scala b/core/src/main/scala/org/mockito/DefaultAnswer.scala index d7f15814..e126194a 100644 --- a/core/src/main/scala/org/mockito/DefaultAnswer.scala +++ b/core/src/main/scala/org/mockito/DefaultAnswer.scala @@ -27,12 +27,19 @@ trait DefaultAnswer extends Answer[Any] with Function[InvocationOnMock, Option[A object DefaultAnswer { implicit val defaultAnswer: DefaultAnswer = ReturnsSmartNulls + + def apply(from: Answer[_]): DefaultAnswer = new DecoratedAnswer(from) } -object ReturnsDefaults extends DefaultAnswer { - override def apply(invocation: InvocationOnMock): Option[Any] = Option(RETURNS_DEFAULTS.answer(invocation)) +class DecoratedAnswer(from: Answer[_]) extends DefaultAnswer { + override def apply(invocation: InvocationOnMock): Option[Any] = Option(from.answer(invocation)) } + +object ReturnsDefaults extends DecoratedAnswer(RETURNS_DEFAULTS) +object ReturnsDeepStubs extends DecoratedAnswer(RETURNS_DEEP_STUBS) +object CallsRealMethods extends DecoratedAnswer(CALLS_REAL_METHODS) + object ReturnsSmartNulls extends DefaultAnswer { override def apply(invocation: InvocationOnMock): Option[Any] = Option(RETURNS_DEFAULTS.answer(invocation)).orElse { val returnType = invocation.getMethod.getReturnType @@ -56,14 +63,6 @@ object ReturnsSmartNulls extends DefaultAnswer { } } -object ReturnsDeepStubs extends DefaultAnswer { - override def apply(invocation: InvocationOnMock): Option[Any] = Option(RETURNS_DEEP_STUBS.answer(invocation)) -} - -object CallsRealMethods extends DefaultAnswer { - override def apply(invocation: InvocationOnMock): Option[Any] = Option(CALLS_REAL_METHODS.answer(invocation)) -} - object ReturnsEmptyValues extends DefaultAnswer { private val javaEmptyValuesAndPrimitives = new ReturnsMoreEmptyValues diff --git a/core/src/main/scala/org/mockito/IdiomaticMockito.scala b/core/src/main/scala/org/mockito/IdiomaticMockito.scala index 864c3d6d..bc5f08f5 100644 --- a/core/src/main/scala/org/mockito/IdiomaticMockito.scala +++ b/core/src/main/scala/org/mockito/IdiomaticMockito.scala @@ -13,7 +13,7 @@ trait IdiomaticMockito extends MockCreator { override def mock[T <: AnyRef: ClassTag: TypeTag](mockSettings: MockSettings): T = MockitoSugar.mock[T](mockSettings) - override def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: Answer[_]): T = MockitoSugar.mock[T](defaultAnswer) + override def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: DefaultAnswer): T = MockitoSugar.mock[T](defaultAnswer) override def mock[T <: AnyRef: ClassTag: TypeTag](implicit defaultAnswer: DefaultAnswer): T = MockitoSugar.mock[T] diff --git a/core/src/main/scala/org/mockito/MockitoAPI.scala b/core/src/main/scala/org/mockito/MockitoAPI.scala index d5e5da2d..0f86117e 100644 --- a/core/src/main/scala/org/mockito/MockitoAPI.scala +++ b/core/src/main/scala/org/mockito/MockitoAPI.scala @@ -27,7 +27,8 @@ import scala.reflect.runtime.universe.TypeTag private[mockito] trait MockCreator { def mock[T <: AnyRef: ClassTag: TypeTag](implicit defaultAnswer: DefaultAnswer): T - def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: Answer[_]): T + def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: Answer[_]): T = mock[T](DefaultAnswer(defaultAnswer)) + def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: DefaultAnswer): T def mock[T <: AnyRef: ClassTag: TypeTag](mockSettings: MockSettings): T def mock[T <: AnyRef: ClassTag: TypeTag](name: String)(implicit defaultAnswer: DefaultAnswer): T @@ -115,8 +116,8 @@ private[mockito] trait MockitoEnhancer extends MockCreator { * verify(aMock).iHaveSomeDefaultArguments("I'm not gonna pass the second argument", "default value") * as the value for the second parameter would have been null... */ - override def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: Answer[_]): T = - mock(withSettings(defaultAnswer.lift)) + override def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: DefaultAnswer): T = + mock(withSettings(defaultAnswer)) /** * Delegates to Mockito.mock(type: Class[T], mockSettings: MockSettings) diff --git a/core/src/main/scala/org/mockito/integrations/scalatest/ResetMocksAfterEachTest.scala b/core/src/main/scala/org/mockito/integrations/scalatest/ResetMocksAfterEachTest.scala index e83a8413..8d6962e0 100644 --- a/core/src/main/scala/org/mockito/integrations/scalatest/ResetMocksAfterEachTest.scala +++ b/core/src/main/scala/org/mockito/integrations/scalatest/ResetMocksAfterEachTest.scala @@ -3,7 +3,6 @@ package org.mockito.integrations.scalatest import java.util.concurrent.ConcurrentHashMap import org.mockito.{DefaultAnswer, MockCreator, MockitoSugar, MockSettings} -import org.mockito.stubbing.Answer import org.scalatest.{Outcome, TestSuite} import scala.collection.JavaConverters._ @@ -37,7 +36,7 @@ trait ResetMocksAfterEachTest extends TestSuite with MockCreator { self: MockCre abstract override def mock[T <: AnyRef: ClassTag: TypeTag](implicit defaultAnswer: DefaultAnswer): T = addMock(super.mock[T]) - abstract override def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: Answer[_]): T = + abstract override def mock[T <: AnyRef: ClassTag: TypeTag](defaultAnswer: DefaultAnswer): T = addMock(super.mock[T](defaultAnswer)) abstract override def mock[T <: AnyRef: ClassTag: TypeTag](mockSettings: MockSettings): T = diff --git a/core/src/main/scala/org/mockito/package.scala b/core/src/main/scala/org/mockito/package.scala index ba0899dd..c337d97a 100644 --- a/core/src/main/scala/org/mockito/package.scala +++ b/core/src/main/scala/org/mockito/package.scala @@ -112,10 +112,4 @@ package object mockito { i.getArgument[P9](9), i.getArgument[P10](10) )) - - implicit class AnswerOps[T](val a: Answer[T]) extends AnyVal { - def lift: DefaultAnswer = new DefaultAnswer { - override def apply(invocation: InvocationOnMock): Option[Any] = Option(a.answer(invocation)) - } - } } From e6859198b245aea134a1c4582978d216811dbe88 Mon Sep 17 00:00:00 2001 From: Bruno Bonanno <241804+bbonanno@users.noreply.github.com> Date: Thu, 30 Aug 2018 20:15:02 +0100 Subject: [PATCH 2/6] don't check unused stubs for lenient mocks --- .../main/scala/org/mockito/MockitoAPI.scala | 13 ++++++------ .../org/mockito/MockitoScalaSession.scala | 20 +++++++++---------- .../org/mockito/MockitoScalaSessionTest.scala | 12 +++++++++++ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/core/src/main/scala/org/mockito/MockitoAPI.scala b/core/src/main/scala/org/mockito/MockitoAPI.scala index 0f86117e..da7cb8f1 100644 --- a/core/src/main/scala/org/mockito/MockitoAPI.scala +++ b/core/src/main/scala/org/mockito/MockitoAPI.scala @@ -34,6 +34,13 @@ private[mockito] trait MockCreator { def spy[T](realObj: T): T def spyLambda[T <: AnyRef: ClassTag](realObj: T): T + + /** + * Delegates to Mockito.withSettings(), it's only here to expose the full Mockito API + */ + def withSettings(implicit defaultAnswer: DefaultAnswer): MockSettings = + Mockito.withSettings().defaultAnswer(defaultAnswer) + } //noinspection MutatorLikeMethodIsParameterless @@ -209,12 +216,6 @@ private[mockito] trait MockitoEnhancer extends MockCreator { */ def mockingDetails(toInspect: AnyRef): MockingDetails = Mockito.mockingDetails(toInspect) - /** - * Delegates to Mockito.withSettings(), it's only here to expose the full Mockito API - */ - def withSettings(implicit defaultAnswer: DefaultAnswer): MockSettings = - Mockito.withSettings().defaultAnswer(defaultAnswer) - /** * Delegates to Mockito.verifyNoMoreInteractions(Object... mocks), but ignores the default stubs that * deal with default argument values diff --git a/core/src/main/scala/org/mockito/MockitoScalaSession.scala b/core/src/main/scala/org/mockito/MockitoScalaSession.scala index 585ce090..5791b70e 100644 --- a/core/src/main/scala/org/mockito/MockitoScalaSession.scala +++ b/core/src/main/scala/org/mockito/MockitoScalaSession.scala @@ -85,16 +85,16 @@ object MockitoScalaSession { } class UnexpectedInvocationsMockListener extends MockCreationListener { - def reportUnStubbedCalls(): UnexpectedInvocations = - UnexpectedInvocations( - mocks - .map(MockitoSugar.mockingDetails) - .flatMap(_.getInvocations.asScala) - .filter(_.stubInfo() == null) - .filterNot(_.isVerified) - .filterNot(_.getMethod.getName.contains("$default$")) - .toSet - ) + def reportUnStubbedCalls(): UnexpectedInvocations = UnexpectedInvocations( + mocks + .map(MockitoSugar.mockingDetails) + .filterNot(_.getMockCreationSettings.isLenient) + .flatMap(_.getInvocations.asScala) + .filter(_.stubInfo() == null) + .filterNot(_.isVerified) + .filterNot(_.getMethod.getName.contains("$default$")) + .toSet + ) private val mocks = mutable.Set.empty[AnyRef] diff --git a/core/src/test/scala/org/mockito/MockitoScalaSessionTest.scala b/core/src/test/scala/org/mockito/MockitoScalaSessionTest.scala index dda6abc5..09e80ef0 100644 --- a/core/src/test/scala/org/mockito/MockitoScalaSessionTest.scala +++ b/core/src/test/scala/org/mockito/MockitoScalaSessionTest.scala @@ -168,6 +168,18 @@ class MockitoScalaSessionTest extends WordSpec with IdiomaticMockito with scalat } } } + + "don't check unused stubs for lenient mocks" in { + MockitoScalaSession().run { + val foo = mock[Foo](withSettings.lenient()) + + foo.bar("pepe") shouldReturn "mocked" + + foo.bar("pepe") + + foo.bar("paco") + } + } } } From f407dcaea5e743d5e40bc1d1936775544d902829 Mon Sep 17 00:00:00 2001 From: Bruno Bonanno <241804+bbonanno@users.noreply.github.com> Date: Sat, 1 Sep 2018 15:11:08 +0100 Subject: [PATCH 3/6] MockitoScalaSession plays nicely with DeepStubs --- .../org/mockito/MockitoScalaSession.scala | 86 ++++++++++++++----- .../org/mockito/MockitoScalaSessionTest.scala | 66 ++++++++++++-- 2 files changed, 124 insertions(+), 28 deletions(-) diff --git a/core/src/main/scala/org/mockito/MockitoScalaSession.scala b/core/src/main/scala/org/mockito/MockitoScalaSession.scala index 5791b70e..c7e39431 100644 --- a/core/src/main/scala/org/mockito/MockitoScalaSession.scala +++ b/core/src/main/scala/org/mockito/MockitoScalaSession.scala @@ -1,7 +1,8 @@ package org.mockito -import org.mockito.MockitoScalaSession.UnexpectedInvocationsMockListener -import org.mockito.exceptions.misusing.UnexpectedInvocationException +import org.mockito.MockitoScalaSession.{MockitoScalaSessionListener, UnexpectedInvocations} +import org.mockito.exceptions.misusing.{UnexpectedInvocationException, UnnecessaryStubbingException} +import org.mockito.internal.stubbing.StubbedInvocationMatcher import org.mockito.invocation.{DescribedInvocation, Invocation, Location} import org.mockito.listeners.MockCreationListener import org.mockito.mock.MockCreationSettings @@ -13,7 +14,7 @@ import scala.collection.mutable import scala.collection.JavaConverters._ class MockitoScalaSession(name: String, strictness: Strictness, logger: MockitoSessionLogger) { - private val listener = new UnexpectedInvocationsMockListener + private val listener = new MockitoScalaSessionListener private val mockitoSession = Mockito.mockitoSession().name(name).logger(logger).strictness(strictness).startMocking() Mockito.framework().addListener(listener) @@ -22,16 +23,17 @@ class MockitoScalaSession(name: String, strictness: Strictness, logger: MockitoS try { t.fold { mockitoSession.finishMocking() - listener.reportUnStubbedCalls().reportUnexpectedInvocations() + listener.reportIssues().foreach(_.report()) } { case e: NullPointerException => mockitoSession.finishMocking(e) - val unStubbedCalls = listener.reportUnStubbedCalls() - if (unStubbedCalls.nonEmpty) - throw new UnexpectedInvocationException(s"""A NullPointerException was thrown, check if maybe related to - |$unStubbedCalls""".stripMargin, - e) - else throw e + listener.reportIssues().foreach { + case unStubbedCalls: UnexpectedInvocations if unStubbedCalls.nonEmpty => + throw new UnexpectedInvocationException(s"""A NullPointerException was thrown, check if maybe related to + |$unStubbedCalls""".stripMargin, + e) + case _ => throw e + } case other => mockitoSession.finishMocking(other) throw other @@ -63,7 +65,11 @@ object MockitoScalaSession { override def getLocation: Location = SyntheticLocation } - case class UnexpectedInvocations(invocations: Set[Invocation]) { + trait Reporter { + def report(): Unit + } + + case class UnexpectedInvocations(invocations: Set[Invocation]) extends Reporter { def nonEmpty: Boolean = invocations.nonEmpty override def toString: String = @@ -80,25 +86,59 @@ object MockitoScalaSession { |Please make sure you aren't missing any stubbing or that your code actually does what you want""".stripMargin } else "No unexpected invocations found" - def reportUnexpectedInvocations(): Unit = - if (nonEmpty) throw new UnexpectedInvocationException(toString) + def report(): Unit = if (nonEmpty) throw new UnexpectedInvocationException(toString) } - class UnexpectedInvocationsMockListener extends MockCreationListener { - def reportUnStubbedCalls(): UnexpectedInvocations = UnexpectedInvocations( - mocks - .map(MockitoSugar.mockingDetails) - .filterNot(_.getMockCreationSettings.isLenient) - .flatMap(_.getInvocations.asScala) - .filter(_.stubInfo() == null) + case class UnusedStubbings(stubbings: Set[StubbedInvocationMatcher]) extends Reporter { + def nonEmpty: Boolean = stubbings.nonEmpty + + override def toString: String = + if (nonEmpty) { + val locations = stubbings.zipWithIndex + .map { + case (stubbing, idx) => s"${idx + 1}. $stubbing ${stubbing.getLocation}" + } + .mkString("\n") + s"""Unnecessary stubbings detected. + | + |Clean & maintainable test code requires zero unnecessary code. + |Following stubbings are unnecessary (click to navigate to relevant line of code): + |$locations + |Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.""".stripMargin + } else "No unexpected invocations found" + + def report(): Unit = if (nonEmpty) throw new UnnecessaryStubbingException(toString) + } + + class MockitoScalaSessionListener extends MockCreationListener { + def reportIssues(): Seq[Reporter] = { + val mockDetails = mocks.toSet.map(MockitoSugar.mockingDetails) + + val stubbings = mockDetails + .flatMap(_.getStubbings.asScala) + .collect { + case s: StubbedInvocationMatcher => s + } + + val invocations = mockDetails.flatMap(_.getInvocations.asScala) + + val unexpectedInvocations = invocations .filterNot(_.isVerified) .filterNot(_.getMethod.getName.contains("$default$")) - .toSet - ) + .filterNot(i => stubbings.exists(_.matches(i))) + + val unusedStubbings = stubbings.filterNot(sm => invocations.exists(sm.matches)).filter(!_.wasUsed()) + + Seq( + UnexpectedInvocations(unexpectedInvocations), + UnusedStubbings(unusedStubbings) + ) + } private val mocks = mutable.Set.empty[AnyRef] - override def onMockCreated(mock: AnyRef, settings: MockCreationSettings[_]): Unit = mocks += mock + override def onMockCreated(mock: AnyRef, settings: MockCreationSettings[_]): Unit = + if (!settings.isLenient) mocks += mock } } diff --git a/core/src/test/scala/org/mockito/MockitoScalaSessionTest.scala b/core/src/test/scala/org/mockito/MockitoScalaSessionTest.scala index 09e80ef0..a9f25c98 100644 --- a/core/src/test/scala/org/mockito/MockitoScalaSessionTest.scala +++ b/core/src/test/scala/org/mockito/MockitoScalaSessionTest.scala @@ -3,10 +3,10 @@ package org.mockito import org.scalatest import org.mockito.exceptions.misusing.{PotentialStubbingProblem, UnexpectedInvocationException, UnnecessaryStubbingException} import org.mockito.exceptions.verification.SmartNullPointerException -import org.scalatest.WordSpec +import org.scalatest.{OptionValues, WordSpec} //noinspection RedundantDefaultArgument -class MockitoScalaSessionTest extends WordSpec with IdiomaticMockito with scalatest.Matchers { +class MockitoScalaSessionTest extends WordSpec with IdiomaticMockito with scalatest.Matchers with OptionValues { class Foo { def bar(a: String) = "bar" @@ -19,11 +19,17 @@ class MockitoScalaSessionTest extends WordSpec with IdiomaticMockito with scalat } class Bar { - def callMeMaybe: Option[Boolean] = None + def callMeMaybe: Baz = ??? + def dontCallMe: Baz = ??? + } + + class Baz { + def callMe: Option[String] = ??? + def dontCallMe: Option[String] = ??? } final class BarFinal { - def callMeMaybe: Option[Boolean] = None + def callMeMaybe: Option[Boolean] = ??? } "MockitoScalaSession" should { @@ -74,7 +80,8 @@ class MockitoScalaSessionTest extends WordSpec with IdiomaticMockito with scalat } } - thrown.getMessage should startWith("You have a NullPointerException because this method call was *not* stubbed correctly") + thrown.getMessage should startWith( + "You have a NullPointerException because this method call was *not* stubbed correctly") } "check incorrect stubs after the expected one was called on a final class" in { @@ -180,6 +187,55 @@ class MockitoScalaSessionTest extends WordSpec with IdiomaticMockito with scalat foo.bar("paco") } } + + "work with nested deep stubs" in { + MockitoScalaSession().run { + val foo = mock[Foo](ReturnsDeepStubs) + + foo.userClass.callMeMaybe.callMe shouldReturn Some("my number") + + foo.userClass.callMeMaybe.callMe.value shouldBe "my number" + } + } + + "not fail if a final deep stub is called in a non stubbed method" in { + MockitoScalaSession().run { + val foo = mock[Foo](ReturnsDeepStubs) + + foo.userClass.callMeMaybe.callMe shouldReturn Some("my number") + + foo.userClass.callMeMaybe.callMe.value shouldBe "my number" + + foo.userClass.callMeMaybe.dontCallMe + + } + } + + "not fail if a nested deep stub is called in a non stubbed method" in { + MockitoScalaSession().run { + val foo = mock[Foo](ReturnsDeepStubs) + + foo.userClass.callMeMaybe.callMe shouldReturn Some("my number") + + foo.userClass.callMeMaybe.callMe.value shouldBe "my number" + + foo.userClass.dontCallMe + + } + } + + "fail if a nested deep stub is stubbed but not used" in { + val thrown = the[UnnecessaryStubbingException] thrownBy { + MockitoScalaSession().run { + val foo = mock[Foo](ReturnsDeepStubs) + + foo.userClass.callMeMaybe.callMe shouldReturn Some("my number") + + } + } + + thrown.getMessage should startWith("Unnecessary stubbings detected") + } } } From eabf1d63268ffe31344ac69f3461b50eeac0e779 Mon Sep 17 00:00:00 2001 From: Bruno Bonanno <241804+bbonanno@users.noreply.github.com> Date: Sat, 1 Sep 2018 15:28:32 +0100 Subject: [PATCH 4/6] Use standard Mockito SMART_NULL answer --- .../scala/org/mockito/DefaultAnswer.scala | 29 ++----------------- .../scala/org/mockito/DefaultAnswerTest.scala | 10 ++----- .../org/mockito/MockitoScalaSessionTest.scala | 5 ++-- .../scala/org/mockito/MockitoSugarTest.scala | 10 +++---- 4 files changed, 12 insertions(+), 42 deletions(-) diff --git a/core/src/main/scala/org/mockito/DefaultAnswer.scala b/core/src/main/scala/org/mockito/DefaultAnswer.scala index e126194a..85380c98 100644 --- a/core/src/main/scala/org/mockito/DefaultAnswer.scala +++ b/core/src/main/scala/org/mockito/DefaultAnswer.scala @@ -1,10 +1,8 @@ package org.mockito -import java.lang.reflect.Modifier.{isAbstract, isFinal} +import java.lang.reflect.Modifier.isAbstract import org.mockito.exceptions.base.MockitoException -import org.mockito.exceptions.verification.SmartNullPointerException -import org.mockito.internal.util.ObjectMethodsGuru.isToStringMethod import org.mockito.invocation.InvocationOnMock import org.mockito.stubbing.Answer import org.mockito.Answers._ @@ -35,33 +33,10 @@ class DecoratedAnswer(from: Answer[_]) extends DefaultAnswer { override def apply(invocation: InvocationOnMock): Option[Any] = Option(from.answer(invocation)) } - object ReturnsDefaults extends DecoratedAnswer(RETURNS_DEFAULTS) object ReturnsDeepStubs extends DecoratedAnswer(RETURNS_DEEP_STUBS) object CallsRealMethods extends DecoratedAnswer(CALLS_REAL_METHODS) - -object ReturnsSmartNulls extends DefaultAnswer { - override def apply(invocation: InvocationOnMock): Option[Any] = Option(RETURNS_DEFAULTS.answer(invocation)).orElse { - val returnType = invocation.getMethod.getReturnType - - if (!returnType.isPrimitive && !isFinal(returnType.getModifiers)) - Some(Mockito.mock(returnType, ThrowsSmartNullPointer(invocation))) - else - None - } - - private case class ThrowsSmartNullPointer(unStubbedInvocation: InvocationOnMock) extends Answer[Any] { - - override def answer(currentInvocation: InvocationOnMock): Any = - if (isToStringMethod(currentInvocation.getMethod)) - s"""SmartNull returned by this un-stubbed method call on a mock: - |${unStubbedInvocation.toString}""".stripMargin - else - throw new SmartNullPointerException( - s"""You have a NullPointerException because this method call was *not* stubbed correctly: - |[$unStubbedInvocation] on the Mock [${unStubbedInvocation.getMock}]""".stripMargin) - } -} +object ReturnsSmartNulls extends DecoratedAnswer(RETURNS_SMART_NULLS) object ReturnsEmptyValues extends DefaultAnswer { private val javaEmptyValuesAndPrimitives = new ReturnsMoreEmptyValues diff --git a/core/src/test/scala/org/mockito/DefaultAnswerTest.scala b/core/src/test/scala/org/mockito/DefaultAnswerTest.scala index 39ddaff5..6a37d2e8 100644 --- a/core/src/test/scala/org/mockito/DefaultAnswerTest.scala +++ b/core/src/test/scala/org/mockito/DefaultAnswerTest.scala @@ -95,13 +95,11 @@ class DefaultAnswerTest smartNull should not be null - val throwable: SmartNullPointerException = the[SmartNullPointerException] thrownBy { + val throwable = the[SmartNullPointerException] thrownBy { smartNull.callMeMaybe() } - throwable.getMessage shouldBe - s"""You have a NullPointerException because this method call was *not* stubbed correctly: - |[foo.userClass(42);] on the Mock [$aMock]""".stripMargin + throwable.getMessage should include("You have a NullPointerException here:") } "return a smart standard monad" in { @@ -113,9 +111,7 @@ class DefaultAnswerTest smartNull.isEmpty } - throwable.getMessage shouldBe - s"""You have a NullPointerException because this method call was *not* stubbed correctly: - |[foo.returnsList();] on the Mock [$aMock]""".stripMargin + throwable.getMessage should include("You have a NullPointerException here:") } "return a default value for primitives" in { diff --git a/core/src/test/scala/org/mockito/MockitoScalaSessionTest.scala b/core/src/test/scala/org/mockito/MockitoScalaSessionTest.scala index a9f25c98..c6f7fdb0 100644 --- a/core/src/test/scala/org/mockito/MockitoScalaSessionTest.scala +++ b/core/src/test/scala/org/mockito/MockitoScalaSessionTest.scala @@ -68,7 +68,7 @@ class MockitoScalaSessionTest extends WordSpec with IdiomaticMockito with scalat } } - thrown.getMessage should startWith("A NullPointerException was thrown, check if maybe related to") + thrown.getMessage should startWith("Unexpected invocations found") } "check SmartNull" in { @@ -80,8 +80,7 @@ class MockitoScalaSessionTest extends WordSpec with IdiomaticMockito with scalat } } - thrown.getMessage should startWith( - "You have a NullPointerException because this method call was *not* stubbed correctly") + thrown.getMessage should include("You have a NullPointerException here:") } "check incorrect stubs after the expected one was called on a final class" in { diff --git a/core/src/test/scala/org/mockito/MockitoSugarTest.scala b/core/src/test/scala/org/mockito/MockitoSugarTest.scala index 99e5c102..373da005 100644 --- a/core/src/test/scala/org/mockito/MockitoSugarTest.scala +++ b/core/src/test/scala/org/mockito/MockitoSugarTest.scala @@ -107,7 +107,7 @@ class MockitoSugarTest extends WordSpec with MockitoSugar with scalatest.Matcher when(aMock.iStartWithByNameArgs("arg1", "arg2")) thenReturn "mocked!" aMock.iStartWithByNameArgs("arg1", "arg2") shouldBe "mocked!" - aMock.iStartWithByNameArgs("arg111", "arg2") shouldBe null + aMock.iStartWithByNameArgs("arg111", "arg2") shouldBe "" verify(aMock).iStartWithByNameArgs("arg1", "arg2") verify(aMock).iStartWithByNameArgs("arg111", "arg2") @@ -119,7 +119,7 @@ class MockitoSugarTest extends WordSpec with MockitoSugar with scalatest.Matcher when(aMock.iHavePrimitiveByNameArgs(1, "arg2")) thenReturn "mocked!" aMock.iHavePrimitiveByNameArgs(1, "arg2") shouldBe "mocked!" - aMock.iHavePrimitiveByNameArgs(2, "arg2") shouldBe null + aMock.iHavePrimitiveByNameArgs(2, "arg2") shouldBe "" verify(aMock).iHavePrimitiveByNameArgs(1, "arg2") verify(aMock).iHavePrimitiveByNameArgs(2, "arg2") @@ -131,7 +131,7 @@ class MockitoSugarTest extends WordSpec with MockitoSugar with scalatest.Matcher when(aMock.iHaveFunction0Args(eqTo("arg1"), function0("arg2"))) thenReturn "mocked!" aMock.iHaveFunction0Args("arg1", () => "arg2") shouldBe "mocked!" - aMock.iHaveFunction0Args("arg1", () => "arg3") shouldBe null + aMock.iHaveFunction0Args("arg1", () => "arg3") shouldBe "" verify(aMock).iHaveFunction0Args(eqTo("arg1"), function0("arg2")) verify(aMock).iHaveFunction0Args(eqTo("arg1"), function0("arg3")) @@ -156,8 +156,8 @@ class MockitoSugarTest extends WordSpec with MockitoSugar with scalatest.Matcher reset(aMock) - aMock.bar shouldBe null - aMock.iHavePrimitiveByNameArgs(1, "arg2") shouldBe null + aMock.bar shouldBe "" + aMock.iHavePrimitiveByNameArgs(1, "arg2") shouldBe "" //to verify the reset mock handler still handles by-name params when(aMock.iHavePrimitiveByNameArgs(1, "arg2")) thenReturn "mocked!" From 6c0342d1c443c20c49ee9d90bdbb00e96021edfe Mon Sep 17 00:00:00 2001 From: Bruno Bonanno <241804+bbonanno@users.noreply.github.com> Date: Sat, 1 Sep 2018 15:41:31 +0100 Subject: [PATCH 5/6] Update README --- README.md | 21 ++++++++++++------- .../scala/org/mockito/DefaultAnswer.scala | 18 ++++++++++++---- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fedba292..275586d4 100644 --- a/README.md +++ b/README.md @@ -249,14 +249,19 @@ Check the [tests](https://github.com/mockito/mockito-scala/blob/master/core/src/ ## Default Answers We defined a new type `org.mockito.DefaultAnswer` which is used to configure the default behaviour of a mock when a non-stubbed invocation -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, -mockito-scala will return a "Smart Null", which is basically a mock of the type returned by the called method. -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 -throw a different exception with a hint of the non-stubbed method call (including its params) that returned this Smart Null, -this will make it much easier to find and fix a non-stubbed call - -Most of the Answers defined in `org.mockito.Answers` have it's counterpart as a `org.mockito.DefaultAnswer`, and on top of that -we also provide `org.mockito.ReturnsEmptyValues` which will try its best to return an empty object for well known types, +is made on it. + +The object `org.mockito.DefaultAnswers` contains each one of the provided ones + +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. + +A "Smart Null", is nothing else than a mock of the type returned by the called method. +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 +throw a different exception with a hint of the non-stubbed method that was called (including its params), +this should make much easier the task of finding and fixing non-stubbed calls + +Most of the Answers defined in `org.mockito.Answers` have it's counterpart in `org.mockito.DefaultAnswers`, and on top of that +we also provide `ReturnsEmptyValues` which will try its best to return an empty object for well known types, i.e. `Nil` for `List`, `None` for `Option` etc. This DefaultAnswer is not part of the default behaviour as we think a SmartNull is better, to explain why, let's imagine we have the following code. diff --git a/core/src/main/scala/org/mockito/DefaultAnswer.scala b/core/src/main/scala/org/mockito/DefaultAnswer.scala index 85380c98..f24f0fb2 100644 --- a/core/src/main/scala/org/mockito/DefaultAnswer.scala +++ b/core/src/main/scala/org/mockito/DefaultAnswer.scala @@ -33,11 +33,10 @@ class DecoratedAnswer(from: Answer[_]) extends DefaultAnswer { override def apply(invocation: InvocationOnMock): Option[Any] = Option(from.answer(invocation)) } -object ReturnsDefaults extends DecoratedAnswer(RETURNS_DEFAULTS) -object ReturnsDeepStubs extends DecoratedAnswer(RETURNS_DEEP_STUBS) -object CallsRealMethods extends DecoratedAnswer(CALLS_REAL_METHODS) +object ReturnsDefaults extends DecoratedAnswer(RETURNS_DEFAULTS) +object ReturnsDeepStubs extends DecoratedAnswer(RETURNS_DEEP_STUBS) +object CallsRealMethods extends DecoratedAnswer(CALLS_REAL_METHODS) object ReturnsSmartNulls extends DecoratedAnswer(RETURNS_SMART_NULLS) - object ReturnsEmptyValues extends DefaultAnswer { private val javaEmptyValuesAndPrimitives = new ReturnsMoreEmptyValues @@ -62,3 +61,14 @@ object ReturnsEmptyValues extends DefaultAnswer { override def apply(invocation: InvocationOnMock): Option[Any] = Option(javaEmptyValuesAndPrimitives.answer(invocation)).orElse(emptyValues.get(invocation.getMethod.getReturnType)) } + +/** + * Simple object to act as an 'enum' of DefaultAnswers + */ +object DefaultAnswers { + val ReturnsDefaults: DefaultAnswer = org.mockito.ReturnsDefaults + val ReturnsDeepStubs: DefaultAnswer = org.mockito.ReturnsDeepStubs + val CallsRealMethods: DefaultAnswer = org.mockito.CallsRealMethods + val ReturnsSmartNulls: DefaultAnswer = org.mockito.ReturnsSmartNulls + val ReturnsEmptyValues: DefaultAnswer = org.mockito.ReturnsEmptyValues +} From 4bea9ca69c7a1862e353bdf9be0c22cb6648af6f Mon Sep 17 00:00:00 2001 From: Bruno Bonanno <241804+bbonanno@users.noreply.github.com> Date: Sat, 1 Sep 2018 15:44:03 +0100 Subject: [PATCH 6/6] Fixup some tests --- .../test/scala-2.11/org/mockito/MockitoSugarTest_211.scala | 2 +- core/src/test/scala/org/mockito/DefaultAnswerTest.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/test/scala-2.11/org/mockito/MockitoSugarTest_211.scala b/core/src/test/scala-2.11/org/mockito/MockitoSugarTest_211.scala index bfd566fe..4182f7c2 100644 --- a/core/src/test/scala-2.11/org/mockito/MockitoSugarTest_211.scala +++ b/core/src/test/scala-2.11/org/mockito/MockitoSugarTest_211.scala @@ -17,7 +17,7 @@ class MockitoSugarTest_211 extends WordSpec with MockitoSugar with scalatest.Mat aMock.traitMethod() shouldBe 69 - verify(aMock).traitMethod(0, null) + verify(aMock).traitMethod(0, "") } } } diff --git a/core/src/test/scala/org/mockito/DefaultAnswerTest.scala b/core/src/test/scala/org/mockito/DefaultAnswerTest.scala index 6a37d2e8..86766b8a 100644 --- a/core/src/test/scala/org/mockito/DefaultAnswerTest.scala +++ b/core/src/test/scala/org/mockito/DefaultAnswerTest.scala @@ -134,7 +134,7 @@ class DefaultAnswerTest "ReturnsEmptyValues" should { "return a default value for primitives" in { - val primitives = mock[Primitives](ReturnsEmptyValues) + val primitives = mock[Primitives](DefaultAnswers.ReturnsEmptyValues) primitives.barByte shouldBe 0.toByte primitives.barBoolean shouldBe false @@ -147,7 +147,7 @@ class DefaultAnswerTest } "return the empty values for known classes" in { - val aMock = mock[KnownTypes](ReturnsEmptyValues) + val aMock = mock[KnownTypes](DefaultAnswers.ReturnsEmptyValues) aMock.returnsOption shouldBe None aMock.returnsList shouldBe List.empty