Skip to content

Commit 398e0f2

Browse files
authored
Introduce static singleton object spying with real-method-call and strict-stub default behaviour (#392)
* The reworking of discovering the version supports resolving the `version.properties` file, when referencing this project i.e. `mockito-scala`, from another project in the developer's workspace * Found the right places for tests: 1. Includes auto-verification within a session for unnecessary stubs; strictness is applied by default via implicits 2. Proof that real methods are called when not stubbed * Refactored/de-duped the implementation * New SBT/Scala version and/or additional checks cause CircleCI to fail; `sbt test` runs green locally. * Added convenience implicits and documentation explaining the intended use
1 parent afc0822 commit 398e0f2

File tree

5 files changed

+170
-41
lines changed

5 files changed

+170
-41
lines changed

build.sbt

Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,31 @@
11
import scala.io.Source
22
import scala.language.postfixOps
3-
import scala.util.Try
3+
import sbt.io.Using
44

55
val currentScalaVersion = "2.13.6"
66

7-
ThisBuild / scalaVersion := currentScalaVersion
7+
inThisBuild(
8+
Seq(
9+
scalaVersion := currentScalaVersion,
10+
//Load version from the file so that Gradle/Shipkit and SBT use the same version
11+
version := sys.env
12+
.get("PROJECT_VERSION")
13+
.filter(_.trim.nonEmpty)
14+
.orElse {
15+
lazy val VersionRE = """^version=(.+)$""".r
16+
Using.file(Source.fromFile)(baseDirectory.value / "version.properties") {
17+
_.getLines.collectFirst { case VersionRE(v) => v }
18+
}
19+
}
20+
.map { _.replace(".*", "-SNAPSHOT") }
21+
.get
22+
)
23+
)
824

925
lazy val commonSettings =
1026
Seq(
1127
organization := "org.mockito",
1228
//Load version from the file so that Gradle/Shipkit and SBT use the same version
13-
version := {
14-
val versionFromEnv = System.getenv("PROJECT_VERSION")
15-
if (versionFromEnv != null && versionFromEnv.trim().nonEmpty) {
16-
versionFromEnv
17-
} else {
18-
val pattern = """^version=(.+)$""".r
19-
val source = Source.fromFile("version.properties")
20-
val version = Try(source.getLines.collectFirst { case pattern(v) =>
21-
v
22-
}.get)
23-
source.close
24-
version.get.replace(".*", "-SNAPSHOT")
25-
}
26-
},
2729
crossScalaVersions := Seq(currentScalaVersion, "2.12.14", "2.11.12"),
2830
scalafmtOnCompile := true,
2931
scalacOptions ++= Seq(
@@ -163,29 +165,17 @@ lazy val core = (project in file("core"))
163165
//TODO remove when we remove the deprecated classes in org.mockito.integrations.Dependencies.scalatest
164166
libraryDependencies += Dependencies.scalatest % "provided",
165167
// include the macro classes and resources in the main jar
166-
mappings in (Compile, packageBin) ++= mappings
167-
.in(macroSub, Compile, packageBin)
168-
.value,
168+
Compile / packageBin / mappings ++= (macroSub / Compile / packageBin / mappings).value,
169169
// include the macro sources in the main source jar
170-
mappings in (Compile, packageSrc) ++= mappings
171-
.in(macroSub, Compile, packageSrc)
172-
.value,
170+
Compile / packageSrc / mappings ++= (macroSub / Compile / packageSrc / mappings).value,
173171
// include the common classes and resources in the main jar
174-
mappings in (Compile, packageBin) ++= mappings
175-
.in(common, Compile, packageBin)
176-
.value,
172+
Compile / packageBin / mappings ++= (common / Compile / packageBin / mappings).value,
177173
// include the common sources in the main source jar
178-
mappings in (Compile, packageSrc) ++= mappings
179-
.in(common, Compile, packageSrc)
180-
.value,
174+
Compile / packageSrc / mappings ++= (common / Compile / packageSrc / mappings).value,
181175
// include the common classes and resources in the main jar
182-
mappings in (Compile, packageBin) ++= mappings
183-
.in(macroCommon, Compile, packageBin)
184-
.value,
176+
Compile / packageBin / mappings ++= (macroCommon / Compile / packageBin / mappings).value,
185177
// include the common sources in the main source jar
186-
mappings in (Compile, packageSrc) ++= mappings
187-
.in(macroCommon, Compile, packageSrc)
188-
.value
178+
Compile / packageSrc / mappings ++= (macroCommon / Compile / packageSrc / mappings).value
189179
)
190180

191181
lazy val macroSub = (project in file("macro"))

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

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import org.mockito.mock.MockCreationSettings
2727
import org.mockito.stubbing._
2828
import org.mockito.verification.{ VerificationAfterDelay, VerificationMode, VerificationWithTimeout }
2929
import org.scalactic.{ Equality, Prettifier }
30-
3130
import scala.collection.JavaConverters._
3231
import scala.reflect.ClassTag
3332
import scala.reflect.runtime.universe.WeakTypeTag
@@ -627,14 +626,29 @@ private[mockito] trait MockitoEnhancer extends MockCreator {
627626
/**
628627
* Mocks the specified object only for the context of the block
629628
*/
630-
def withObjectMocked[O <: AnyRef: ClassTag](block: => Any)(implicit defaultAnswer: DefaultAnswer, $pt: Prettifier): Unit = {
629+
def withObjectMocked[O <: AnyRef: ClassTag](block: => Any)(implicit defaultAnswer: DefaultAnswer, $pt: Prettifier): Unit =
630+
withObject[O](_ => withSettings(defaultAnswer), block)
631+
632+
/**
633+
* Spies the specified object only for the context of the block
634+
*
635+
* Automatically pulls in [[org.mockito.LeniencySettings#strictStubs strict stubbing]] behaviour via implicits.
636+
* To override this default (strict) behaviour, bring lenient settings into implicit scope;
637+
* see [[org.mockito.leniency]] for details
638+
*/
639+
def withObjectSpied[O <: AnyRef: ClassTag](block: => Any)(implicit leniency: LeniencySettings, $pt: Prettifier): Unit = {
640+
val settings = leniency(Mockito.withSettings().defaultAnswer(CALLS_REAL_METHODS))
641+
withObject[O](settings.spiedInstance(_), block)
642+
}
643+
644+
private[mockito] def withObject[O <: AnyRef: ClassTag](settings: O => MockSettings, block: => Any)(implicit $pt: Prettifier) = {
631645
val objectClass = clazz[O]
632646
objectClass.synchronized {
633647
val moduleField = objectClass.getDeclaredField("MODULE$")
634648
val realImpl: O = moduleField.get(null).asInstanceOf[O]
635649

636650
val threadAwareMock = createMock(
637-
withSettings(defaultAnswer),
651+
settings(realImpl),
638652
(settings: MockCreationSettings[O], pt: Prettifier) => ThreadAwareMockHandler(settings, realImpl)(pt)
639653
)
640654

@@ -645,6 +659,20 @@ private[mockito] trait MockitoEnhancer extends MockCreator {
645659
}
646660
}
647661

662+
trait LeniencySettings {
663+
def apply(settings: MockSettings): MockSettings
664+
}
665+
666+
object LeniencySettings {
667+
implicit val strictStubs: LeniencySettings = new LeniencySettings {
668+
override def apply(settings: MockSettings): MockSettings = settings
669+
}
670+
671+
val lenientStubs: LeniencySettings = new LeniencySettings {
672+
override def apply(settings: MockSettings): MockSettings = settings.lenient()
673+
}
674+
}
675+
648676
private[mockito] trait Verifications {
649677

650678
/**

common/src/main/scala/org/mockito/mockito.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,4 +613,27 @@ package object mockito {
613613
new Equality[T] with Serializable {
614614
override def areEqual(a: T, b: Any): Boolean = Equality.default[T].areEqual(a, b)
615615
}
616+
617+
/**
618+
* Implicit [[org.mockito.LeniencySettings LeniencySettings]] provided here for convenience
619+
*
620+
* Neither are in implicit scope as is; pull one or the other in to activate the respective semantics, for
621+
* example:
622+
*
623+
* {{{
624+
* import org.mockito.leniency.lenient
625+
*
626+
* withObjectSpied[SomeObject.type] {
627+
* SomeObject.getExternalThing returns "external-thing"
628+
* SomeObject.getOtherThing returns "other-thing"
629+
* SomeObject.getExternalThing should be("external-thing")
630+
* }
631+
* }}}
632+
*
633+
* Note: strict stubbing is active by default via [[org.mockito.LeniencySettings#strictStubs strictStubs]]
634+
*/
635+
object leniency {
636+
implicit val strict: LeniencySettings = LeniencySettings.strictStubs
637+
implicit val lenient: LeniencySettings = LeniencySettings.lenientStubs
638+
}
616639
}

scalatest/src/test/scala/user/org/mockito/IdiomaticStubbingTest.scala

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import org.mockito.{ ArgumentMatchersSugar, IdiomaticStubbing }
77
import org.scalatest.matchers.should.Matchers
88
import org.scalatest.wordspec.AnyWordSpec
99
import user.org.mockito.matchers.{ ValueCaseClassInt, ValueCaseClassString, ValueClass }
10-
1110
import scala.collection.parallel.immutable
1211
import scala.concurrent.{ Await, Future }
1312
import scala.util.Random
@@ -246,8 +245,8 @@ class IdiomaticStubbingTest extends AnyWordSpec with Matchers with ArgumentMatch
246245
}
247246
}
248247

249-
"doStub" should {
250-
"stub a spy that would fail if the real impl is called" in {
248+
"spy" should {
249+
"stub a function that would fail if the real impl is called" in {
251250
val aSpy = spy(new Org)
252251

253252
an[IllegalArgumentException] should be thrownBy {
@@ -264,7 +263,7 @@ class IdiomaticStubbingTest extends AnyWordSpec with Matchers with ArgumentMatch
264263
}
265264
}
266265

267-
"stub a spy with an answer" in {
266+
"stub a function with an answer" in {
268267
val aSpy = spy(new Org)
269268

270269
((i: Int) => i * 10 + 2) willBe answered by aSpy.doSomethingWithThisInt(*)
@@ -295,6 +294,49 @@ class IdiomaticStubbingTest extends AnyWordSpec with Matchers with ArgumentMatch
295294
org.doSomethingWithThisIntAndStringAndBoolean(1, "2", v3 = true) shouldBe "not mocked"
296295
org.doSomethingWithThisIntAndStringAndBoolean(1, "2", v3 = false) shouldBe ""
297296
}
297+
298+
"stub an object method" in {
299+
FooObject.simpleMethod shouldBe "not mocked!"
300+
301+
withObjectSpied[FooObject.type] {
302+
FooObject.simpleMethod returns "spied!"
303+
FooObject.simpleMethod shouldBe "spied!"
304+
}
305+
306+
FooObject.simpleMethod shouldBe "not mocked!"
307+
}
308+
309+
"call real object method when not stubbed" in {
310+
val now = FooObject.stateDependantMethod
311+
withObjectSpied[FooObject.type] {
312+
FooObject.simpleMethod returns s"spied!"
313+
FooObject.simpleMethod shouldBe s"spied!"
314+
FooObject.stateDependantMethod shouldBe now
315+
}
316+
}
317+
318+
"be thread safe" when {
319+
"always stubbing object methods" in {
320+
immutable.ParSeq.range(1, 100).foreach { i =>
321+
withObjectSpied[FooObject.type] {
322+
FooObject.simpleMethod returns s"spied!-$i"
323+
FooObject.simpleMethod shouldBe s"spied!-$i"
324+
}
325+
}
326+
}
327+
328+
"intermittently stubbing object methods" in {
329+
val now = FooObject.stateDependantMethod
330+
immutable.ParSeq.range(1, 100).foreach { i =>
331+
if (i % 2 == 0)
332+
withObjectSpied[FooObject.type] {
333+
FooObject.stateDependantMethod returns i
334+
FooObject.stateDependantMethod shouldBe i
335+
}
336+
else FooObject.stateDependantMethod shouldBe now
337+
}
338+
}
339+
}
298340
}
299341

300342
"mock" should {

scalatest/src/test/scala/user/org/mockito/MockitoScalaSessionTest.scala

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,5 +388,51 @@ class MockitoScalaSessionTest extends AnyWordSpec with IdiomaticMockito with Mat
388388

389389
thrown.getMessage should include("You have a NullPointerException here:")
390390
}
391+
392+
"verify object spies" when {
393+
394+
"successfully for uncalled lenient stubs" in {
395+
MockitoScalaSession().run {
396+
import org.mockito.leniency.lenient
397+
398+
withObjectSpied[FooObject.type] {
399+
FooObject.stateDependantMethod returns 1234L
400+
FooObject.simpleMethod returns s"spied!"
401+
FooObject.simpleMethod shouldBe s"spied!"
402+
}
403+
}
404+
}
405+
406+
"unsuccessfully for uncalled strict stubs" in {
407+
val thrown = the[UnnecessaryStubbingException] thrownBy {
408+
MockitoScalaSession().run {
409+
import org.mockito.leniency.strict
410+
411+
withObjectSpied[FooObject.type] {
412+
FooObject.stateDependantMethod returns 1234L
413+
FooObject.simpleMethod returns s"spied!"
414+
FooObject.simpleMethod shouldBe s"spied!"
415+
}
416+
}
417+
}
418+
419+
thrown.getMessage should include("Unnecessary stubbings detected")
420+
}
421+
422+
"unsuccessfully by default (strict) for uncalled stubs" in {
423+
424+
val thrown = the[UnnecessaryStubbingException] thrownBy {
425+
MockitoScalaSession().run {
426+
withObjectSpied[FooObject.type] {
427+
FooObject.stateDependantMethod returns 1234L
428+
FooObject.simpleMethod returns s"spied!"
429+
FooObject.simpleMethod shouldBe s"spied!"
430+
}
431+
}
432+
}
433+
434+
thrown.getMessage should include("Unnecessary stubbings detected")
435+
}
436+
}
391437
}
392438
}

0 commit comments

Comments
 (0)