Skip to content

Lenient spies and methods #80

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 4 commits into from
Feb 16, 2019
Merged
Show file tree
Hide file tree
Changes from 3 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
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The library has independent developers, release cycle and versioning from core m

* Artifact identifier: "org.mockito:mockito-scala_2.11:VERSION"
* Artifact identifier: "org.mockito:mockito-scala_2.12:VERSION"
* Artifact identifier: "org.mockito:mockito-scala_2.13.0-M2:VERSION"
* Artifact identifier: "org.mockito:mockito-scala_2.13.0-M5:VERSION"
* Latest version - see [release notes](/docs/release-notes.md)
* Repositories: [Maven Central](https://search.maven.org/search?q=mockito-scala) or [JFrog's Bintray](https://bintray.com/mockito/maven/mockito-scala)

Expand Down Expand Up @@ -144,6 +144,8 @@ Both `ArgCaptor[T]` and `ValCaptor[T]` return an instance of `Captor[T]` so the

## `org.mockito.MockitoScalaSession`

### Basic usage

This is a wrapper around `org.mockito.MockitoSession`, it's main purpose (on top of having a Scala API)
is to improve the search of mis-used mocks and unexpected invocations to reduce debugging effort when something doesn't work

Expand All @@ -158,6 +160,23 @@ MockitoScalaSession().run {
That's it! that block of code will execute within a session which will take care of checking the use of the framework and,
if the test fails, it will try to find out if the failure could be related to a mock being used incorrectly

### Leniency

If for some reason we want that a mock created within the scope of a session does not report failures for some or all methods we can specify leniency for it

For the whole mock or spy to be ignored by the session, so basically a mock/spy that behaves as if the session didn't exist at all, we can make it lenient, e.g.
```scala
val aMock = mock[Foo](withSettings.lenient())
val aSpy = spy(new Bar, lenient = true)
```

Now, if we just want to make one or more methods to be ignored by the session checks, we can make the method call lenient, this works as any other stubbing, so what it matters what matchers you define
```scala
aMock.myMethod(*) isLenient()
//or
when(aMock.myMethod(*)).isLenient()
```

## MockitoFixture

For a more detailed explanation read [this](https://medium.com/@bbonanno_83496/introduction-to-mockito-scala-part-3-383c3b2ed55f)
Expand Down
10 changes: 7 additions & 3 deletions common/src/main/scala/org/mockito/MockitoAPI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

package org.mockito

import org.mockito.Answers.CALLS_REAL_METHODS
import org.mockito.internal.ValueClassExtractor
import org.mockito.internal.configuration.plugins.Plugins.getMockMaker
import org.mockito.internal.creation.MockSettingsImpl
Expand All @@ -33,7 +34,7 @@ private[mockito] trait MockCreator {
def mock[T <: AnyRef: ClassTag: WeakTypeTag](mockSettings: MockSettings): T
def mock[T <: AnyRef: ClassTag: WeakTypeTag](name: String)(implicit defaultAnswer: DefaultAnswer): T

def spy[T <: AnyRef: ClassTag: WeakTypeTag](realObj: T): T
def spy[T <: AnyRef: ClassTag: WeakTypeTag](realObj: T, lenient: Boolean): T
def spyLambda[T <: AnyRef: ClassTag](realObj: T): T

/**
Expand Down Expand Up @@ -223,8 +224,11 @@ private[mockito] trait MockitoEnhancer extends MockCreator {
override def mock[T <: AnyRef: ClassTag: WeakTypeTag](name: String)(implicit defaultAnswer: DefaultAnswer): T =
mock(withSettings.name(name))

def spy[T <: AnyRef: ClassTag: WeakTypeTag](realObj: T): T =
mock[T](withSettings(DefaultAnswers.CallsRealMethods).spiedInstance(realObj))
def spy[T <: AnyRef: ClassTag: WeakTypeTag](realObj: T, lenient: Boolean = false): T = {
def mockSettings: MockSettings = Mockito.withSettings().defaultAnswer(CALLS_REAL_METHODS).spiedInstance(realObj)
val settings = if(lenient) mockSettings.lenient() else mockSettings
mock[T](settings)
}

/**
* Delegates to <code>Mockito.reset(T... mocks)</code>, but restores the default stubs that
Expand Down
14 changes: 14 additions & 0 deletions common/src/main/scala/org/mockito/mockito.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package org

import java.lang.reflect.Method
import java.util.Objects

import org.mockito.internal.ValueClassExtractor
import org.mockito.invocation.InvocationOnMock
import org.mockito.stubbing.Answer
import org.scalactic.Equality

import scala.reflect.ClassTag

Expand Down Expand Up @@ -108,4 +112,14 @@ package object mockito {
i.getArgument[P9](9),
i.getArgument[P10](10)
))

//Look at org.mockito.internal.invocation.InvocationMatcher#hasSameMethod
implicit def JavaMethodEquality(implicit $arrEq: Equality[Array[_]]): Equality[Method] = new Equality[Method] {
override def areEqual(m1: Method, b: Any): Boolean = b match {
case m2: Method =>
//m1.name could be null
Objects.equals(m1.getName, m2.getName) && $arrEq.areEqual(m1.getParameterTypes, m2.getParameterTypes)
case _ => false
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package org.mockito.stubbing

import org.mockito.internal.ValueClassExtractor
import org.mockito.{ clazz, functionToAnswer, invocationToAnswer }
import org.mockito.internal.stubbing.OngoingStubbingImpl
import org.mockito.internal.util.MockUtil.getMockSettings
import org.mockito.invocation.InvocationOnMock
import org.mockito.quality.Strictness.LENIENT
import org.mockito.{clazz, functionToAnswer, invocationToAnswer}

import scala.language.implicitConversions
import scala.reflect.ClassTag
Expand All @@ -13,6 +16,14 @@ object ScalaFirstStubbing {

case class ScalaFirstStubbing[T](delegate: OngoingStubbing[T])(implicit $vce: ValueClassExtractor[T]) {

//noinspection AccessorLikeMethodIsUnit
def isLenient(): Unit = {
delegate.asInstanceOf[OngoingStubbingImpl[T]].setStrictness(LENIENT)
delegate.thenAnswer(new Answer[T] {
override def answer(i: InvocationOnMock): T = getMockSettings(delegate.getMock).getDefaultAnswer.answer(i).asInstanceOf[T]
})
}

/**
* Sets consecutive return one or more values to be returned when the method is called. E.g:
* <pre class="code"><code class="scala">
Expand Down
6 changes: 4 additions & 2 deletions core/src/main/scala/org/mockito/IdiomaticMockito.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package org.mockito

import org.mockito.stubbing.{ DefaultAnswer, ScalaOngoingStubbing }
import org.mockito.MockitoSugar._
import org.mockito.VerifyMacro._
import org.mockito.WhenMacro._
import org.mockito.stubbing.{ DefaultAnswer, ScalaOngoingStubbing }

import scala.language.experimental.macros
import scala.reflect.ClassTag
Expand All @@ -22,7 +22,7 @@ trait IdiomaticMockito extends MockCreator {
override def mock[T <: AnyRef: ClassTag: WeakTypeTag](implicit defaultAnswer: DefaultAnswer): T =
MockitoSugar.mock[T]

override def spy[T <: AnyRef: ClassTag: WeakTypeTag](realObj: T): T = MockitoSugar.spy(realObj)
override def spy[T <: AnyRef: ClassTag: WeakTypeTag](realObj: T, lenient: Boolean = false): T = MockitoSugar.spy(realObj, lenient)

override def spyLambda[T <: AnyRef: ClassTag](realObj: T): T = MockitoSugar.spyLambda(realObj)

Expand Down Expand Up @@ -50,6 +50,8 @@ trait IdiomaticMockito extends MockCreator {

def wasCalled(t: OnlyOn)(implicit order: VerifyOrder): Unit = macro VerifyMacro.wasMacroOnlyOn[T]

//noinspection AccessorLikeMethodIsUnit
def isLenient(): Unit = macro WhenMacro.isLenient[T]
}

class Returned
Expand Down
39 changes: 30 additions & 9 deletions core/src/main/scala/org/mockito/MockitoScalaSession.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package org.mockito

import java.lang.reflect.Method

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
import org.mockito.quality.Strictness
import org.mockito.quality.Strictness.STRICT_STUBS
import org.mockito.quality.Strictness.{ LENIENT, STRICT_STUBS }
import org.mockito.session.MockitoSessionLogger
import org.scalactic.Equality

import scala.collection.JavaConverters._
import scala.collection.mutable
Expand All @@ -19,7 +22,8 @@ class MockitoScalaSession(name: String, strictness: Strictness, logger: MockitoS

Mockito.framework().addListener(listener)

def finishMocking(t: Option[Throwable] = None): Unit =
def finishMocking(t: Option[Throwable] = None): Unit = {
listener.cleanLenientStubs()
try {
t.fold {
mockitoSession.finishMocking()
Expand All @@ -41,6 +45,7 @@ class MockitoScalaSession(name: String, strictness: Strictness, logger: MockitoS
} finally {
Mockito.framework().removeListener(listener)
}
}

def run[T](block: => T): T =
try {
Expand Down Expand Up @@ -106,34 +111,50 @@ object MockitoScalaSession {
}

class MockitoScalaSessionListener(strictness: Strictness) extends MockCreationListener {
def reportIssues(): Seq[Reporter] = {
val mockDetails = mocks.toSet.map(MockitoSugar.mockingDetails)
lazy val mockDetails: Set[MockingDetails] = mocks.toSet.map(MockitoSugar.mockingDetails)

val stubbings = mockDetails
lazy val stubbings: Set[StubbedInvocationMatcher] =
mockDetails
.flatMap(_.getStubbings.asScala)
.collect {
case s: StubbedInvocationMatcher => s
}

val invocations = mockDetails.flatMap(_.getInvocations.asScala)
lazy val invocations: Set[Invocation] = mockDetails.flatMap(_.getInvocations.asScala)

val unexpectedInvocations = invocations
def reportIssues(): Seq[Reporter] = {
val unexpectedInvocations: Set[Invocation] = invocations
.filterNot(_.isVerified)
.filterNot(_.getMethod.getName.contains("$default$"))
.filterNot(i => stubbings.exists(_.matches(i)))

val unusedStubbings = stubbings.filterNot(sm => invocations.exists(sm.matches)).filter(!_.wasUsed())
val unusedStubbings: Set[StubbedInvocationMatcher] = stubbings
.filterNot(sm => invocations.exists(sm.matches))
.filterNot(_.wasUsed())

Seq(
UnexpectedInvocations(unexpectedInvocations),
UnusedStubbings(unusedStubbings)
)
}

def cleanLenientStubs()(implicit $meq: Equality[Method]): Unit = {
val lenientStubbings = stubbings.filter(_.getStrictness == LENIENT)
stubbings
.filterNot(_.wasUsed())
.flatMap(s => lenientStubbings.find(l => $meq.areEqual(l.getMethod, s.getMethod)).map(s -> _))
.foreach {
case (stubbing, lenient) =>
stubbing.markStubUsed(new DescribedInvocation {
override def getLocation: Location = lenient.getLocation
})
}
}

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

override def onMockCreated(mock: AnyRef, settings: MockCreationSettings[_]): Unit =
if (!settings.isLenient && strictness != Strictness.LENIENT) mocks += mock
if (!settings.isLenient && strictness != LENIENT) mocks += mock
}
}

Expand Down
Loading