├── integration └── src │ └── test ├── project ├── build.properties └── plugins.sbt ├── core └── src │ ├── test │ └── scala │ │ ├── fixtures │ │ ├── ContextBound.scala │ │ ├── ClassWithoutTypeParameters.scala │ │ ├── SpecializedClass.scala │ │ ├── some │ │ │ └── other │ │ │ │ └── pkg │ │ │ │ └── SomeClass.scala │ │ ├── yet │ │ │ └── another │ │ │ │ └── pkg │ │ │ │ └── YetAnotherClass.scala │ │ ├── HigherOrderPolymorphicTrait.scala │ │ ├── SpecializedClass2.scala │ │ ├── PolymorphicClassWithParameters.scala │ │ ├── UserDatabase.scala │ │ ├── TestDefaultParameters.scala │ │ ├── PolymorphicTrait.scala │ │ ├── ContextBoundInheritance.scala │ │ ├── TestClass.scala │ │ ├── TestTrait.scala │ │ ├── ManyParamsTrait.scala │ │ └── ManyParamsClass.scala │ │ └── eu │ │ └── monniot │ │ └── scala3mock │ │ └── features │ │ ├── MatchersSuite.scala │ │ ├── ThrowSuite.scala │ │ ├── ReturnSuite.scala │ │ └── CallCountSuite.scala │ └── main │ └── scala │ └── eu │ └── monniot │ └── scala3mock │ ├── matchers │ ├── MatchAny.scala │ ├── Matchers.scala │ ├── ArgumentMatcher.scala │ ├── MatcherBase.scala │ ├── MatchEpsilon.scala │ └── MatchPredicate.scala │ ├── context │ ├── Call.scala │ ├── Mock.scala │ └── MockContext.scala │ ├── handlers │ ├── Handler.scala │ ├── Handlers.scala │ ├── UnorderedHandlers.scala │ └── CallHandler.scala │ ├── MockExpectationFailed.scala │ ├── ScalaMocks.scala │ ├── macros │ ├── PrintAst.scala │ ├── utils.scala │ ├── Mocks.scala │ └── WhenImpl.scala │ ├── Default.scala │ ├── withExpectations.scala │ └── functions │ ├── MockFunctions.scala │ ├── FakeFunction.scala │ └── FunctionAdapters.scala ├── website ├── static │ └── img │ │ ├── favicon.png │ │ └── logo.svg ├── sidebars.json ├── package.json ├── core │ └── Footer.js ├── siteConfig.js └── pages │ └── en │ └── index.js ├── .scalafmt.conf ├── scalatest └── src │ ├── test │ └── scala │ │ └── eu │ │ └── monniot │ │ └── scala3mock │ │ ├── mockable │ │ └── TestTrait.scala │ │ └── scalatest │ │ ├── BasicAsyncTests.scala │ │ ├── BasicTests.scala │ │ ├── TestSuiteRunner.scala │ │ ├── SuiteScopeMockTest.scala │ │ ├── SuiteScopePresetMockParallelTest.scala │ │ ├── WithFixtureTest.scala │ │ ├── FixtureContextTest.scala │ │ └── ErrorReportingTest.scala │ └── main │ └── scala │ └── eu │ └── monniot │ └── scala3mock │ └── scalatest │ ├── AsyncMockFactory.scala │ ├── MockFactory.scala │ └── BaseFactory.scala ├── .git-blame-ignore-revs ├── .gitignore ├── README.md ├── .scala-steward.conf ├── cats └── src │ ├── test │ └── scala │ │ └── eu │ │ └── monniot │ │ └── scala3mock │ │ └── cats │ │ └── CatsSuite.scala │ └── main │ └── scala │ └── eu │ └── monniot │ └── scala3mock │ └── cats │ └── ScalaMocks.scala ├── docs ├── user-guide │ ├── cats.md │ ├── faq.md │ ├── matching.md │ ├── features.md │ ├── advanced-topics.md │ └── scalatest.md └── getting-started.md ├── LICENSE └── .github └── workflows ├── release.yml └── checks.yml /integration/src/test: -------------------------------------------------------------------------------- 1 | ../../core/src/test -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.7 2 | -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/ContextBound.scala: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | trait ContextBound[T] {} 4 | -------------------------------------------------------------------------------- /website/static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmonniot/scala3mock/HEAD/website/static/img/favicon.png -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/ClassWithoutTypeParameters.scala: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | class ClassWithoutTypeParameters(i: Int, s: String) 4 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.8.3" 2 | runner.dialect = scala3 3 | fileOverride { 4 | "glob:**/*.sbt" { 5 | runner.dialect = scala212 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/SpecializedClass.scala: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | class SpecializedClass[@specialized T] { 4 | def identity(x: T) = x 5 | } 6 | -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/some/other/pkg/SomeClass.scala: -------------------------------------------------------------------------------- 1 | package fixtures.some.other.pkg 2 | 3 | class SomeClass { 4 | def m(x: Int) = x.toString 5 | } 6 | -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/yet/another/pkg/YetAnotherClass.scala: -------------------------------------------------------------------------------- 1 | package fixtures.yet.another.pkg 2 | 3 | class YetAnotherClass { 4 | def m(x: Int) = x.toString 5 | } 6 | -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/HigherOrderPolymorphicTrait.scala: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | trait HigherOrderPolymorphicTrait[F[_]] { 4 | 5 | def doSomething(param: Int): F[Boolean] 6 | 7 | } 8 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.8") 2 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") 3 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.2") 4 | -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/SpecializedClass2.scala: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | class SpecializedClass2[@specialized T1, @specialized T2] 4 | extends SpecializedClass[T1] { 5 | def identity2(x: T1, y: T2) = (x, y) 6 | } 7 | -------------------------------------------------------------------------------- /scalatest/src/test/scala/eu/monniot/scala3mock/mockable/TestTrait.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.mockable 2 | 3 | trait TestTrait { 4 | def noParamMethod(): String 5 | def oneParamMethod(param: Int): String 6 | def polymorphicMethod[T](x: List[T]): String 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/matchers/MatchAny.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.matchers 2 | 3 | class MatchAny extends MatcherBase: 4 | 5 | override def canEqual(that: Any) = true 6 | 7 | override def equals(that: Any) = true 8 | 9 | override def toString = "*" 10 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/matchers/Matchers.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.matchers 2 | 3 | object Matchers extends Matchers 4 | 5 | trait Matchers extends MatchPredicate: 6 | 7 | def * = MatchAny() 8 | 9 | extension (value: Double) def unary_~ = MatchEpsilon(value) 10 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.7.17 2 | d1f2a4f8d32f5706b23f7d85cc40712cbf4453a6 3 | 4 | # Scala Steward: Reformat with scalafmt 3.8.2 5 | 18cd2d2a33848f818916bff615ed2a7ccfa62156 6 | 7 | # Scala Steward: Reformat with scalafmt 3.8.3 8 | 57615e491e61d9996b7afb6b3d8707063a12c08b 9 | -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/PolymorphicClassWithParameters.scala: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | class PolymorphicClassWithParameters[F[_], E]( 4 | a: HigherOrderPolymorphicTrait[F], 5 | n: Int 6 | )(b: String) { 7 | 8 | def logic(stuff: String): F[Boolean] = a.doSomething(stuff.toInt) 9 | 10 | } 11 | -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/UserDatabase.scala: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | case class User(name: String, age: Int) 4 | case class Address(city: String, street: String) 5 | 6 | trait UserDatabase { 7 | def storeUser(user: User): String 8 | def addUserAddress(user: User, address: Address): String 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/matchers/ArgumentMatcher.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.matchers 2 | 3 | class ArgumentMatcher(template: Product) extends (Product => Boolean): 4 | 5 | def apply(args: Product) = template == args 6 | 7 | override def toString = template.productIterator.mkString("(", ", ", ")") 8 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/context/Call.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.context 2 | 3 | import eu.monniot.scala3mock.functions.FakeFunction 4 | 5 | case class Call(target: FakeFunction, arguments: Product): 6 | override def toString: String = 7 | s"$target${arguments.productIterator.mkString("(", ", ", ")")}" 8 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/handlers/Handler.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.handlers 2 | 3 | import eu.monniot.scala3mock.context.Call 4 | 5 | trait Handler: 6 | 7 | def handle(call: Call): Option[Any] 8 | 9 | def verify(call: Call): Boolean 10 | 11 | def isSatisfied: Boolean 12 | 13 | def reset(): Unit 14 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/matchers/MatcherBase.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.matchers 2 | 3 | /** Base trait of all ScalaMock argument matchers. 4 | * 5 | * If you want to write a custom matcher please extend the [[Matcher]] trait. 6 | */ 7 | trait MatcherBase extends Equals: 8 | override def toString: String = "Matcher" 9 | -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/TestDefaultParameters.scala: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | trait TestDefaultParameters { 4 | def foo(bar: Int = 9): String 5 | def foo2(b: Int = 1, c: Long): String 6 | 7 | def defaultAfterRegular(a: Int, b: Int = 0): String 8 | def multiParamList(a: Int, b: Int = 0)(c: Long, d: Long = 0): String 9 | 10 | def withTypeParam[A, B](a: A, b: B)(c: Long, d: Long = 0): String 11 | } 12 | -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/PolymorphicTrait.scala: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | trait PolymorphicTrait[T] { 4 | def method[U](x: Int, y: T, z: U): T 5 | 6 | trait Embedded[V] { 7 | trait ATrait[A, B] 8 | 9 | def innerTrait(t: T, v: V): ATrait[T, V] 10 | def outerTrait(t: T, v: V): PolymorphicTrait.this.ATrait[T, V] 11 | } 12 | 13 | trait ATrait[A, B] 14 | 15 | // def referenceEmbedded(): Embedded 16 | } 17 | -------------------------------------------------------------------------------- /website/sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "Overview": [ 4 | "getting-started" 5 | ], 6 | "User Guide": [ 7 | "user-guide/installation", 8 | "user-guide/features", 9 | "user-guide/matching", 10 | "user-guide/scalatest", 11 | "user-guide/cats", 12 | "user-guide/advanced-topics", 13 | "user-guide/faq" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/ContextBoundInheritance.scala: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | import eu.monniot.scala3mock.context.MockContext 4 | import eu.monniot.scala3mock.macros.mock 5 | 6 | trait ContextBoundInheritance[F[_]: Applicative] 7 | 8 | trait ContextBoundInheritanceChild[F[_]: App2] 9 | extends ContextBoundInheritance[F] 10 | 11 | trait App2[G[_]] extends Applicative[G] 12 | object App2 { 13 | given App2[List] = new App2[List]: 14 | override def pure[A](a: A): List[A] = List(a) 15 | } 16 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "scripts": { 4 | "examples": "docusaurus-examples", 5 | "start": "docusaurus-start", 6 | "build": "docusaurus-build", 7 | "publish-gh-pages": "docusaurus-publish", 8 | "write-translations": "docusaurus-write-translations", 9 | "version": "docusaurus-version", 10 | "rename-version": "docusaurus-rename-version" 11 | }, 12 | "devDependencies": { 13 | "docusaurus": "^1.14.7" 14 | } 15 | } -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/MockExpectationFailed.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock 2 | 3 | import eu.monniot.scala3mock.context.{Call, MockContext} 4 | import eu.monniot.scala3mock.functions.MockFunction1 5 | import eu.monniot.scala3mock.handlers.{CallHandler, Handler, UnorderedHandlers} 6 | 7 | import scala.annotation.unused 8 | import scala.collection.mutable.ListBuffer 9 | import scala.util.control.NonFatal 10 | 11 | class MockExpectationFailed(message: String, methodName: Option[String]) 12 | extends Throwable: 13 | override def getMessage(): String = message 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # sbt specific 5 | dist/* 6 | target/ 7 | lib_managed/ 8 | src_managed/ 9 | project/boot/ 10 | project/plugins/project/ 11 | project/local-plugins.sbt 12 | .history 13 | .ensime 14 | .ensime_cache/ 15 | .sbt-scripted/ 16 | local.sbt 17 | 18 | # Bloop 19 | .bsp 20 | 21 | # VS Code 22 | .vscode/ 23 | 24 | # Metals 25 | .bloop/ 26 | .metals/ 27 | metals.sbt 28 | 29 | # IDEA 30 | .idea 31 | .idea_modules 32 | /.worksheet/ 33 | 34 | # website 35 | *.afdesign 36 | npm_dependencies/ 37 | website/node_modules 38 | website/build 39 | website/yarn.lock 40 | website/i18n 41 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/matchers/MatchEpsilon.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.matchers 2 | 3 | import scala.math.abs 4 | 5 | /** Matcher that matches all numbers that are close to a given value */ 6 | class MatchEpsilon(value: Double) extends MatcherBase: 7 | 8 | override def canEqual(that: Any) = that.isInstanceOf[Number] 9 | 10 | override def equals(that: Any) = that match { 11 | case n: Number => abs(value - n.doubleValue) < MatchEpsilon.epsilon 12 | case _ => false 13 | } 14 | 15 | override def toString = "~" + value 16 | 17 | object MatchEpsilon: 18 | val epsilon = 0.001 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Deprecation notice: [ScalaMock](https://github.com/ScalaMock/ScalaMock) has added support for Scala 3 since version 6 and as a result it is recommended to use it instead of this library. 2 | 3 | ## Scala3Mock 4 | 5 | Heads on to https://francois.monniot.eu/scala3mock for the documentation. 6 | 7 | ### Licenses 8 | 9 | Adapted from 10 | https://github.com/OlegIlyenko/scala-icon/blob/master/scala-icon.svg for the scala icon.
11 | Under Creative Commons Attribution 4.0 International License. 12 | 13 | Adapted from https://github.com/paulbutcher/ScalaMock/blob/master/LICENCE.
14 | Under MIT License. Scala3Mock would not exists without their work ! 15 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.pin = [ 2 | # Pinning the dependencies that require 3.3+ to their last known version. 3 | # This is because the library rely on an experimental API (to create new classes) 4 | # and experimental APIs on 3.3+ now either require a non-stable compiler version 5 | # or to pass the `-experimental` config parameter. 6 | # This was discovered in https://github.com/fmonniot/scala3mock/pull/50 7 | { groupId = "org.scala-lang", artifactId="scala3-library", version = "3.2." }, 8 | { groupId = "org.typelevel", artifactId="cats-core", version = "2.9." }, 9 | { groupId = "org.scalameta", artifactId="munit", version = "1.0.0-M11" } 10 | ] 11 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/handlers/Handlers.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.handlers 2 | 3 | import scala.collection.mutable.ListBuffer 4 | 5 | abstract class Handlers extends Handler: 6 | 7 | def add(handler: Handler): Unit = handlers += handler 8 | def list: Iterable[Handler] = handlers 9 | 10 | def isSatisfied: Boolean = handlers forall (_.isSatisfied) 11 | 12 | override def toString: String = handlers 13 | .flatMap { h => 14 | scala.Predef.augmentString(h.toString).linesIterator.toArray.map { l => 15 | " " + l 16 | } 17 | } 18 | .mkString(s"$prefix {\n", "\n", "\n}") 19 | 20 | protected val handlers = new ListBuffer[Handler] 21 | 22 | protected val prefix: String 23 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/context/Mock.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.context 2 | 3 | import eu.monniot.scala3mock.functions.MockFunction 4 | 5 | import scala.annotation.unused 6 | 7 | private[scala3mock] trait Mock: 8 | 9 | // This is a hack to go around my inability to call methods defined on companion objects 10 | @unused 11 | protected def createMap( 12 | elems: (String, MockFunction)* 13 | ): Map[String, MockFunction] = Map.from(elems) 14 | 15 | def accessMockFunction(name: String): MockFunction 16 | 17 | object Mock: 18 | case object ReceiverIsNotAMock 19 | extends Throwable( 20 | "The object being mocked is not mockable. Use the mock[] function to create a mock." 21 | ) 22 | -------------------------------------------------------------------------------- /core/src/test/scala/eu/monniot/scala3mock/features/MatchersSuite.scala: -------------------------------------------------------------------------------- 1 | package features 2 | 3 | import eu.monniot.scala3mock.ScalaMocks 4 | 5 | class MatchersSuite extends munit.FunSuite with ScalaMocks { 6 | 7 | test("should let you use star for MatchAny") { 8 | withExpectations() { 9 | val intToStringMock = mockFunction[Int, String] 10 | 11 | intToStringMock.expects(*) 12 | assertEquals(intToStringMock(5), null) 13 | } 14 | } 15 | 16 | test("should let you use tilde for MatchEpsilon") { 17 | withExpectations() { 18 | val intToStringMock = mockFunction[Double, String] 19 | 20 | intToStringMock.expects(~2.0) 21 | assertEquals(intToStringMock(2.001), null) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cats/src/test/scala/eu/monniot/scala3mock/cats/CatsSuite.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.cats 2 | 3 | import cats.effect.SyncIO 4 | 5 | class CatsSuite extends munit.FunSuite with ScalaMocks { 6 | 7 | test("it should validate expectations after a lazy data type evaluated") { 8 | // An implicit assumption of this code block is that the expectations 9 | // are not validated on exit of the `withExpectations` function but when 10 | // the returned Monad is being evaluated. 11 | val fa = withExpectations() { 12 | val intToStringMock = mockFunction[Int, String] 13 | intToStringMock.expects(*) 14 | 15 | SyncIO(intToStringMock(2)) 16 | } 17 | 18 | assertEquals(fa.unsafeRunSync(), null) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/TestClass.scala: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | // Not the purest form, but who really care in a test ? 4 | trait Applicative[F[_]] { 5 | def pure[A](a: A): F[A] 6 | } 7 | 8 | object Applicative { 9 | def apply[F[_]](using f: Applicative[F]) = f 10 | given Applicative[List] = new Applicative[List]: 11 | override def pure[A](a: A): List[A] = List(a) 12 | } 13 | 14 | // This test case is representative of a common implementation class in the pure 15 | // functional programming world. I think. 16 | class TestClass[F[_]: Applicative](dep: String)(dep2: Int)(using User) { 17 | private def pm(x: Int) = x 18 | 19 | def a(x: Int, y: String): (Int, String) = x -> y 20 | def b(x: Int): F[Int] = Applicative[F].pure(x) 21 | } 22 | -------------------------------------------------------------------------------- /scalatest/src/test/scala/eu/monniot/scala3mock/scalatest/BasicAsyncTests.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.scalatest 2 | 3 | import eu.monniot.scala3mock.scalatest.AsyncMockFactory 4 | import eu.monniot.scala3mock.mockable.TestTrait 5 | import org.scalatest.flatspec.AsyncFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | import scala.concurrent.Future 8 | 9 | /** Tests for mock defined in test case scope 10 | */ 11 | class BasicAsyncTest extends AsyncFlatSpec with Matchers with AsyncMockFactory { 12 | 13 | it should "work in scalatest async test" in { 14 | val mockedTrait = mock[TestTrait] 15 | when(() => mockedTrait.noParamMethod()).expects().returning("42") 16 | Future.successful { 17 | mockedTrait.noParamMethod() shouldBe "42" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/ScalaMocks.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock 2 | 3 | import eu.monniot.scala3mock.context.MockContext 4 | 5 | object ScalaMocks extends ScalaMocks 6 | 7 | /** Helper trait that provide access to all components (mandatory or optional) 8 | * used by the library to build mocks. 9 | */ 10 | trait ScalaMocks 11 | extends functions.MockFunctions 12 | with macros.Mocks 13 | with matchers.Matchers: 14 | 15 | // apparently using export in 3.2.2 lose the default value of the 16 | // parameter. That might have been fixed in 3.3+, but we can't use 17 | // that version so for now we will duplicate the definition. 18 | def withExpectations[A](verifyAfterRun: Boolean = true)( 19 | f: MockContext ?=> A 20 | ): A = eu.monniot.scala3mock.withExpectations(verifyAfterRun)(f) 21 | -------------------------------------------------------------------------------- /docs/user-guide/cats.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Cats Integration 3 | --- 4 | 5 | Scala3Mock comes with a Cats integration that provides a `withExpectations` function that work with a `MonadError[?, Throwable]` instead of the simple value that the default library provide. This is pretty useful if your tests are defined in term of monads (Cats-Effect's `IO`, Scala's `Future` via alleycats, etc…). 6 | 7 | You'll need to add a new dependency to your **build.sbt**: 8 | ```scala 9 | libraryDependencies += "eu.monniot" %% "scala3mock-cats" % "@VERSION@" % Test 10 | ``` 11 | 12 | Once added, simply replace the `ScalaMocks` import with the Cats one: 13 | 14 | ```diff 15 | - import eu.monniot.scala3mock.ScalaMocks.* 16 | + import eu.monniot.scala3mock.cats.ScalaMocks.* 17 | ``` 18 | 19 | You can then use the library like usual, with the value passed to `withExpectations` being a monad instead of any values. 20 | 21 | ```scala mdoc 22 | import eu.monniot.scala3mock.cats.ScalaMocks.* 23 | import cats.effect.SyncIO 24 | 25 | val fa = withExpectations() { 26 | val fn = mockFunction[Int, String] 27 | fn.expects(*).returns("Hello reader") 28 | 29 | SyncIO(fn(2)) 30 | } 31 | 32 | fa.unsafeRunSync() 33 | ``` 34 | -------------------------------------------------------------------------------- /scalatest/src/test/scala/eu/monniot/scala3mock/scalatest/BasicTests.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.scalatest 2 | 3 | import eu.monniot.scala3mock.scalatest.MockFactory 4 | import eu.monniot.scala3mock.mockable.TestTrait 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | /** Tests for mock defined in test case scope 9 | * 10 | * Tests for issue #25 11 | */ 12 | class BasicTest extends AnyFlatSpec with Matchers with MockFactory { 13 | 14 | it should "allow to use mock defined in test case scope" in { 15 | val mockedTrait = mock[TestTrait] 16 | when(mockedTrait.oneParamMethod).expects(*).returning("one") 17 | when(mockedTrait.oneParamMethod).expects(2).returning("two") 18 | when(() => mockedTrait.noParamMethod()).expects().returning("yey") 19 | 20 | mockedTrait.oneParamMethod(1) shouldBe "one" 21 | mockedTrait.oneParamMethod(2) shouldBe "two" 22 | mockedTrait.noParamMethod() shouldBe "yey" 23 | } 24 | 25 | it should "use separate call logs for each test case" in { 26 | val mockedTrait = mock[TestTrait] 27 | when(mockedTrait.oneParamMethod).expects(3).returning("three") 28 | 29 | mockedTrait.oneParamMethod(3) shouldBe "three" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /scalatest/src/test/scala/eu/monniot/scala3mock/scalatest/TestSuiteRunner.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.scalatest 2 | 3 | import org.scalatest._ 4 | import org.scalatest.events.{Event, TestFailed} 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | import scala.language.postfixOps 8 | import scala.reflect.ClassTag 9 | 10 | trait TestSuiteRunner { this: Matchers => 11 | 12 | /** Executes single ScalaTest test case and returns its outcome (i.e. either 13 | * TestSucccess or TestFailure) 14 | */ 15 | def runTestCase[T <: Suite](suite: T): Event = { 16 | class TestReporter extends Reporter { 17 | var lastEvent: Option[Event] = None 18 | override def apply(e: Event): Unit = { lastEvent = Some(e) } 19 | } 20 | 21 | val reporter = new TestReporter 22 | suite.run(None, Args(reporter)) 23 | reporter.lastEvent.get 24 | } 25 | 26 | def getThrowable[ExnT <: Throwable: ClassTag](event: Event): ExnT = { 27 | event shouldBe a[TestFailed] 28 | 29 | val testCaseError = event.asInstanceOf[TestFailed].throwable.get 30 | testCaseError shouldBe a[ExnT] 31 | testCaseError.asInstanceOf[ExnT] 32 | } 33 | 34 | def getErrorMessage[ExnT <: Throwable: ClassTag](event: Event): String = { 35 | getThrowable[ExnT](event).getMessage() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Original work Copyright (c) 2011-2015 ScalaMock Contributors (https://github.com/paulbutcher/ScalaMock/graphs/contributors) 2 | Modified work Copyright (c) 2022-2023 Scala3Mock Contributors (https://github.com/fmonniot/scala3mock/graphs/contributors) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/macros/PrintAst.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.macros 2 | 3 | object PrintAst: 4 | 5 | import scala.quoted.* 6 | 7 | inline def apply[T](inline stuff: T) = 8 | ${ inspectExpr('stuff) } 9 | 10 | inline def apply[T] = 11 | ${ inspectType[T] } 12 | 13 | private def inspectExpr[T](x: Expr[T])(using Quotes, Type[T]): Expr[Any] = 14 | import quotes.reflect.* 15 | 16 | val term = x.asTerm 17 | println(s"===========Expression of type ${Type.show[T]}=========:") 18 | println() 19 | println(term.show(using Printer.TreeAnsiCode)) 20 | println() 21 | println(term.show(using Printer.TreeStructure)) 22 | println() 23 | println("===========================") 24 | 25 | x 26 | 27 | private def inspectType[T](using Type[T], Quotes): Expr[Any] = 28 | import quotes.reflect.* 29 | 30 | val typeRepr: TypeRepr = TypeRepr.of[T].dealias 31 | println(s"===========Type ${Type.show[T]}=========:") 32 | println() 33 | println(s"typeSymbol=${typeRepr.typeSymbol}; ") 34 | println() 35 | println(typeRepr.typeSymbol.tree.show(using Printer.TreeAnsiCode)) 36 | println() 37 | println(typeRepr.typeSymbol.tree.show(using Printer.TreeStructure)) 38 | println() 39 | println("===========================") 40 | 41 | '{ () } 42 | -------------------------------------------------------------------------------- /scalatest/src/test/scala/eu/monniot/scala3mock/scalatest/SuiteScopeMockTest.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.scalatest 2 | 3 | import eu.monniot.scala3mock.scalatest.MockFactory 4 | import eu.monniot.scala3mock.mockable.TestTrait 5 | 6 | import org.scalatest.ParallelTestExecution 7 | import org.scalatest.flatspec.AnyFlatSpec 8 | import org.scalatest.matchers.should.Matchers 9 | 10 | /** Tests for mocks defined in suite scope (i.e. outside test case scope). */ 11 | class SuiteScopeMockTest extends AnyFlatSpec with Matchers with MockFactory { 12 | 13 | val mockWithoutExpectationsPredefined = mock[TestTrait] 14 | 15 | it should "allow to use mock defined suite scope" in { 16 | when(mockWithoutExpectationsPredefined.oneParamMethod) 17 | .expects(1) 18 | .returning("one") 19 | when(mockWithoutExpectationsPredefined.oneParamMethod) 20 | .expects(2) 21 | .returning("two") 22 | 23 | mockWithoutExpectationsPredefined.oneParamMethod(1) shouldBe "one" 24 | mockWithoutExpectationsPredefined.oneParamMethod(2) shouldBe "two" 25 | } 26 | 27 | it should "allow to use mock defined suite scope in more than one test case" in { 28 | when(mockWithoutExpectationsPredefined.oneParamMethod) 29 | .expects(3) 30 | .returning("three") 31 | 32 | mockWithoutExpectationsPredefined.oneParamMethod(3) shouldBe "three" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/Default.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock 2 | 3 | /** Provides a default value for some type. This is used for mocked functions to 4 | * return something if no specific value have beeen provided. 5 | * 6 | * Note that `Any` doesn't have any default at the moment. If you decide to 7 | * implement `Default` for your own type, do note that the library's mock macro 8 | * currently doesn't support higher-kinded types and default to `null` for 9 | * those. If this is necessary for you, we welcome Pull Requests! 10 | */ 11 | trait Default[A]: 12 | val default: A 13 | 14 | object Default: 15 | 16 | def apply[A](using d: Default[A]): Default[A] = d 17 | 18 | // AnyVal 19 | given Default[Byte] with { val default: Byte = 0 } 20 | given Default[Short] with { val default: Short = 0 } 21 | given Default[Char] with { val default: Char = 0 } 22 | given Default[Int] with { val default = 0 } 23 | given Default[Long] with { val default = 0L } 24 | given Default[Float] with { val default = 0.0f } 25 | given Default[Double] with { val default = 0.0d } 26 | given Default[Boolean] with { val default = false } 27 | given Default[Unit] with { val default = () } 28 | 29 | // AnyRef 30 | given Default[java.io.OutputStream] with { 31 | val default = java.io.OutputStream.nullOutputStream() 32 | } 33 | given [Y]: Default[Y] with { val default = null.asInstanceOf[Y] } 34 | -------------------------------------------------------------------------------- /core/src/test/scala/eu/monniot/scala3mock/features/ThrowSuite.scala: -------------------------------------------------------------------------------- 1 | package features 2 | 3 | import eu.monniot.scala3mock.ScalaMocks 4 | 5 | class ThrowSuite extends munit.FunSuite with ScalaMocks { 6 | 7 | case class TestException() extends RuntimeException 8 | case class AnotherTestException() extends RuntimeException 9 | 10 | test("should throw what it is told to (throwing)") { 11 | withExpectations() { 12 | val noArgFunMock = mockFunction[String] 13 | 14 | noArgFunMock.expects().throwing(new TestException) 15 | intercept[TestException] { noArgFunMock() } 16 | } 17 | } 18 | 19 | test("throw what it is told to (throws)") { 20 | withExpectations() { 21 | val noArgFunMock = mockFunction[String] 22 | 23 | noArgFunMock.expects().throws(new TestException) 24 | intercept[TestException] { noArgFunMock() } 25 | } 26 | } 27 | 28 | test("throw computed exception") { 29 | withExpectations() { 30 | val intFunMock = mockFunction[Int, String] 31 | 32 | intFunMock 33 | .expects(*) 34 | .exactly(3) 35 | .onCall({ (arg) => 36 | if (arg == 1) throw new TestException() 37 | else if (arg == 2) throw new AnotherTestException() 38 | else "Foo" 39 | }) 40 | 41 | intercept[TestException] { intFunMock(1) } 42 | intercept[AnotherTestException] { intFunMock(2) } 43 | assertEquals(intFunMock(3), "Foo") 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/handlers/UnorderedHandlers.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.handlers 2 | 3 | import eu.monniot.scala3mock.context.Call 4 | import scala.util.control.NonLocalReturns.* 5 | 6 | class UnorderedHandlers(logging: Boolean = false) extends Handlers: 7 | 8 | def handle(call: Call): Option[Any] = this.synchronized { 9 | if logging then println(s"handling unordered call $call") 10 | 11 | findFirstOpt(handlers, _.handle(call)) 12 | } 13 | 14 | def verify(call: Call): Boolean = this.synchronized { 15 | if logging then println(s"verifying unordered call $call") 16 | 17 | handlers.find(_.verify(call)).isDefined 18 | } 19 | 20 | def reset(): Unit = handlers.foreach(_.reset()) 21 | 22 | protected val prefix = "inAnyOrder" 23 | 24 | /** Finds the first element of `iter` satisfying a predicate, if any. 25 | * 26 | * @param iter 27 | * the collection of elements 28 | * @param p 29 | * the predicate used to test elements. 30 | * @return 31 | * an option value containing the first element in the $coll that satisfies 32 | * `p.isDefined`, or `None` if none exists. 33 | */ 34 | private def findFirstOpt[A, B]( 35 | iter: IterableOnce[A], 36 | p: A => Option[B] 37 | ): Option[B] = { 38 | val it = iter.iterator 39 | while (it.hasNext) { 40 | val a = it.next() 41 | val mayB = p(a) 42 | if (mayB.isDefined) return mayB 43 | } 44 | None 45 | } 46 | -------------------------------------------------------------------------------- /scalatest/src/test/scala/eu/monniot/scala3mock/scalatest/SuiteScopePresetMockParallelTest.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.scalatest 2 | 3 | import eu.monniot.scala3mock.scalatest.MockFactory 4 | import eu.monniot.scala3mock.mockable.TestTrait 5 | 6 | import org.scalatest.ParallelTestExecution 7 | import org.scalatest.flatspec.AnyFlatSpec 8 | import org.scalatest.matchers.should.Matchers 9 | 10 | /** Tests for mocks defined in suite scope (i.e. outside test case scope) with 11 | * predefined expectations 12 | */ 13 | class SuiteScopePresetMockParallelTest 14 | extends AnyFlatSpec 15 | with Matchers 16 | with MockFactory { 17 | 18 | val mockWithExpectationsPredefined = mock[TestTrait] 19 | when(mockWithExpectationsPredefined.oneParamMethod) 20 | .expects(0) 21 | .returning("predefined") 22 | 23 | it should "allow to use mock defined suite scope with predefined expectations" in { 24 | when(mockWithExpectationsPredefined.oneParamMethod) 25 | .expects(1) 26 | .returning("one") 27 | 28 | mockWithExpectationsPredefined.oneParamMethod(0) shouldBe "predefined" 29 | mockWithExpectationsPredefined.oneParamMethod(1) shouldBe "one" 30 | } 31 | 32 | it should "keep predefined mock expectations" in { 33 | when(mockWithExpectationsPredefined.oneParamMethod) 34 | .expects(2) 35 | .returning("two") 36 | 37 | mockWithExpectationsPredefined.oneParamMethod(0) shouldBe "predefined" 38 | mockWithExpectationsPredefined.oneParamMethod(2) shouldBe "two" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/context/MockContext.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.context 2 | 3 | import eu.monniot.scala3mock.handlers.{CallHandler, Handlers, UnorderedHandlers} 4 | 5 | import scala.collection.mutable.ListBuffer 6 | 7 | trait MockContext: 8 | type ExpectationException <: Throwable 9 | def newExpectationException( 10 | message: String, 11 | methodName: Option[String] = None 12 | ): ExpectationException 13 | 14 | private[scala3mock] var callLog: ListBuffer[Call] = ListBuffer() 15 | private[scala3mock] var expectationContext: Handlers = UnorderedHandlers() 16 | // val mockNameGenerator: MockNameGenerator = new MockNameGenerator() 17 | 18 | def add[E <: CallHandler[_]](e: E): E = 19 | assert( 20 | expectationContext != null, 21 | "Null expectation context - missing withExpectations?" 22 | ) 23 | expectationContext.add(e) 24 | e 25 | 26 | def reportUnexpectedCall(call: Call) = 27 | throw newExpectationException( 28 | s"Unexpected call: $call\n\n${errorContext(callLog, expectationContext)}", 29 | Some(call.target.name) 30 | ) 31 | 32 | def reportUnsatisfiedExpectation( 33 | callLog: ListBuffer[Call], 34 | expectationContext: Handlers 35 | ) = 36 | throw newExpectationException( 37 | s"Unsatisfied expectation:\n\n${errorContext(callLog, expectationContext)}" 38 | ) 39 | 40 | def errorContext(callLog: ListBuffer[Call], expectationContext: Handlers) = 41 | s"Expected:\n$expectationContext\n\nActual:\n${callLog.mkString(" ", "\n ", "")}" 42 | -------------------------------------------------------------------------------- /website/core/Footer.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class Footer extends React.Component { 4 | docUrl(doc, language) { 5 | const baseUrl = this.props.config.baseUrl; 6 | const docsUrl = this.props.config.docsUrl; 7 | const docsPart = `${docsUrl ? `${docsUrl}/` : ''}`; 8 | const langPart = `${language ? `${language}/` : ''}`; 9 | return `${baseUrl}${docsPart}${langPart}${doc}`; 10 | } 11 | 12 | pageUrl(doc, language) { 13 | const baseUrl = this.props.config.baseUrl; 14 | return baseUrl + (language ? `${language}/` : '') + doc; 15 | } 16 | 17 | render() { 18 | return ( 19 | 44 | ); 45 | } 46 | } 47 | 48 | module.exports = Footer; -------------------------------------------------------------------------------- /scalatest/src/main/scala/eu/monniot/scala3mock/scalatest/AsyncMockFactory.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.scalatest 2 | 3 | import eu.monniot.scala3mock.context.{Mock, MockContext} 4 | import eu.monniot.scala3mock.macros.{MockImpl, WhenImpl} 5 | import eu.monniot.scala3mock.functions.* 6 | import eu.monniot.scala3mock.MockExpectationFailed 7 | import eu.monniot.scala3mock.handlers.UnorderedHandlers 8 | import scala.collection.mutable.ListBuffer 9 | import eu.monniot.scala3mock.context.Call 10 | 11 | import org.scalatest.{AsyncTestSuite, AsyncTestSuiteMixin, FutureOutcome} 12 | import scala.concurrent.Future 13 | import scala.util.Success 14 | import scala.util.Failure 15 | import scala.util.control.NonFatal 16 | import org.scalatest.Exceptional 17 | 18 | trait AsyncMockFactory extends AsyncTestSuiteMixin with BaseFactory: 19 | this: AsyncTestSuite => // To prevent users from using Sync with Async suites 20 | 21 | private def withExpectations[T](test: => Future[T]): Future[T] = 22 | initializeExpectations() 23 | val testResult = test.map { result => 24 | verifyExpectations() 25 | result 26 | } 27 | 28 | testResult onComplete { 29 | case Success(_) => () 30 | case Failure(_) => clearTestExpectations() 31 | } 32 | 33 | testResult 34 | 35 | abstract override def withFixture(test: NoArgAsyncTest): FutureOutcome = 36 | if (autoVerify) 37 | new FutureOutcome( 38 | withExpectations(super.withFixture(test).toFuture).recoverWith({ 39 | case NonFatal(ex) => Future.successful(Exceptional(ex)) 40 | }) 41 | ) 42 | else 43 | super.withFixture(test) 44 | -------------------------------------------------------------------------------- /scalatest/src/main/scala/eu/monniot/scala3mock/scalatest/MockFactory.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.scalatest 2 | 3 | import eu.monniot.scala3mock.context.{Mock, MockContext} 4 | import eu.monniot.scala3mock.macros.{MockImpl, WhenImpl} 5 | import eu.monniot.scala3mock.functions.* 6 | import eu.monniot.scala3mock.MockExpectationFailed 7 | import eu.monniot.scala3mock.handlers.UnorderedHandlers 8 | import scala.collection.mutable.ListBuffer 9 | import eu.monniot.scala3mock.context.Call 10 | import eu.monniot.scala3mock.macros.Mocks 11 | import scala.util.control.NonFatal 12 | import org.scalatest.TestSuite 13 | import org.scalatest.TestSuiteMixin 14 | import org.scalatest.Outcome 15 | import org.scalatest.Failed 16 | import org.scalatest.exceptions.TestFailedException 17 | import org.scalatest.exceptions.StackDepthException 18 | import eu.monniot.scala3mock.handlers.Handlers 19 | 20 | trait MockFactory extends TestSuiteMixin with BaseFactory: 21 | this: TestSuite => // To prevent users from using Async with non-Async suites 22 | 23 | private def withExpectations[T](name: String)(what: => T): T = 24 | try 25 | initializeExpectations() 26 | val result = what 27 | verifyExpectations() 28 | result 29 | catch 30 | case NonFatal(ex) => 31 | clearTestExpectations() 32 | throw ex 33 | 34 | abstract override def withFixture(test: NoArgTest): Outcome = 35 | if (autoVerify) 36 | withExpectations(test.name) { 37 | super.withFixture(test) match 38 | case Failed(throwable) => 39 | // Throw error that caused test failure to prevent hiding it by 40 | // "unsatisfied expectation" exception (see issue #72) 41 | throw throwable 42 | case outcome => outcome 43 | } 44 | else 45 | super.withFixture(test) 46 | -------------------------------------------------------------------------------- /scalatest/src/test/scala/eu/monniot/scala3mock/scalatest/WithFixtureTest.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.scalatest 2 | 3 | import eu.monniot.scala3mock.scalatest.MockFactory 4 | import eu.monniot.scala3mock.mockable.TestTrait 5 | 6 | import org.scalatest._ 7 | import org.scalatest.events.TestSucceeded 8 | import org.scalatest.flatspec.AnyFlatSpec 9 | import org.scalatest.funsuite.AnyFunSuite 10 | import org.scalatest.matchers.should.Matchers 11 | 12 | class WithFixtureTest extends AnyFlatSpec with Matchers with TestSuiteRunner { 13 | 14 | object EventLogger { 15 | var events: List[String] = List.empty 16 | def logEvent(event: String) = { events = events :+ event } 17 | } 18 | 19 | trait SuiteWrapper extends SuiteMixin with TestSuite { 20 | abstract override def withFixture(test: NoArgTest): Outcome = { 21 | EventLogger.logEvent("SuiteWrapper setup") 22 | val outcome = super.withFixture(test) 23 | EventLogger.logEvent("SuiteWrapper cleanup") 24 | outcome 25 | } 26 | } 27 | 28 | class TestedSuite 29 | extends AnyFunSuite 30 | with SuiteWrapper 31 | with MockFactory 32 | with Matchers { 33 | test("execute block of code") { 34 | val mockedTrait = mock[TestTrait] 35 | when(mockedTrait.oneParamMethod).expects(1).onCall { (arg: Int) => 36 | EventLogger.logEvent("mock method called") 37 | "one" 38 | } 39 | 40 | mockedTrait.oneParamMethod(1) shouldBe "one" 41 | } 42 | } 43 | 44 | it can "be mixed together with other traits which override withFixture" in { 45 | val outcome = runTestCase[TestedSuite](new TestedSuite) 46 | outcome shouldBe a[TestSucceeded] 47 | EventLogger.events shouldBe List( 48 | "SuiteWrapper setup", 49 | "mock method called", 50 | "SuiteWrapper cleanup" 51 | ) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/withExpectations.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock 2 | 3 | import eu.monniot.scala3mock.context.{Call, MockContext} 4 | import eu.monniot.scala3mock.functions.MockFunction1 5 | import eu.monniot.scala3mock.handlers.{CallHandler, Handler, UnorderedHandlers} 6 | 7 | import scala.annotation.unused 8 | import scala.collection.mutable.ListBuffer 9 | import scala.util.control.NonFatal 10 | 11 | // A standalone function to run a test with a mock context, asserting all expectations at the end. 12 | def withExpectations[A](verifyAfterRun: Boolean = true)( 13 | f: MockContext ?=> A 14 | ): A = 15 | 16 | val ctx = new MockContext: 17 | override type ExpectationException = MockExpectationFailed 18 | 19 | override def newExpectationException( 20 | message: String, 21 | methodName: Option[String] 22 | ): ExpectationException = 23 | new MockExpectationFailed(message, methodName) 24 | 25 | override def toString() = s"MockContext(callLog = $callLog)" 26 | 27 | def initializeExpectations(): Unit = 28 | val initialHandlers = new UnorderedHandlers 29 | 30 | ctx.callLog = new ListBuffer[Call] 31 | ctx.expectationContext = initialHandlers 32 | 33 | def verifyExpectations(): Unit = 34 | ctx.callLog foreach ctx.expectationContext.verify _ 35 | 36 | val oldCallLog = ctx.callLog 37 | val oldExpectationContext = ctx.expectationContext 38 | 39 | if !oldExpectationContext.isSatisfied then 40 | ctx.reportUnsatisfiedExpectation(oldCallLog, oldExpectationContext) 41 | 42 | try 43 | initializeExpectations() 44 | val result = f(using ctx) 45 | if verifyAfterRun then verifyExpectations() 46 | result 47 | catch 48 | case NonFatal(ex) => 49 | // do not verify expectations - just clear them. Throw original exception 50 | // see issue #72 51 | throw ex 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [main] 5 | tags: ["v*"] 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | - uses: actions/setup-java@v4 14 | with: 15 | distribution: 'temurin' 16 | java-version: '11' 17 | - uses: sbt/setup-sbt@v1 18 | # - uses: olafurpg/setup-gpg@v3 19 | - name: Publish ${{ github.ref }} 20 | run: sbt ci-release 21 | env: 22 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 23 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 24 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 25 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 26 | - name: Check git diff 27 | if: ${{ failure() }} 28 | run: git diff 29 | - name: Print published version 30 | if: github.ref == 'refs/heads/main' 31 | run: | 32 | VERSION=$(sbt -no-colors 'inspect version' | grep "Setting: java.lang.String" | cut -d '=' -f2 | tr -d ' ') 33 | echo '### Snapshot version' >> $GITHUB_STEP_SUMMARY 34 | echo "version: $VERSION" >> $GITHUB_STEP_SUMMARY 35 | 36 | website: 37 | runs-on: ubuntu-latest 38 | needs: publish 39 | # We only publish the website on release 40 | if: startsWith(github.event.ref, 'refs/tags/v') 41 | steps: 42 | - uses: actions/checkout@v4 43 | with: 44 | fetch-depth: 0 45 | - uses: actions/setup-java@v4 46 | with: 47 | distribution: 'temurin' 48 | java-version: '11' 49 | cache: 'sbt' 50 | - uses: sbt/setup-sbt@v1 51 | - run: sbt 'docs/docusaurusPublishGhpages' 52 | env: 53 | GIT_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }} 54 | - name: Check git diff 55 | if: ${{ failure() }} 56 | run: git diff 57 | -------------------------------------------------------------------------------- /scalatest/src/test/scala/eu/monniot/scala3mock/scalatest/FixtureContextTest.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.scalatest 2 | 3 | import eu.monniot.scala3mock.scalatest.MockFactory 4 | import eu.monniot.scala3mock.mockable.TestTrait 5 | 6 | import org.scalatest.flatspec.AnyFlatSpec 7 | import org.scalatest.matchers.should.Matchers 8 | 9 | /** Tests for mocks defined in fixture-contexts 10 | * 11 | * Tests for issue #25 12 | */ 13 | class FixtureContextTest extends AnyFlatSpec with Matchers with MockFactory { 14 | 15 | trait TestSetup { 16 | val mockedTrait = mock[TestTrait] 17 | val input = 1 18 | val output = "one" 19 | } 20 | 21 | trait TestSetupWithExpectationsPredefined extends TestSetup { 22 | when(mockedTrait.oneParamMethod).expects(input).returning(output) 23 | } 24 | 25 | trait TestSetupWithHandlerCalledDuringInitialization 26 | extends TestSetupWithExpectationsPredefined { 27 | mockedTrait.oneParamMethod(input) shouldBe output 28 | } 29 | 30 | it should "allow to use mock defined in fixture-context" in new TestSetup { 31 | when(mockedTrait.oneParamMethod).expects(input).returning(output) 32 | when(mockedTrait.oneParamMethod).expects(2).returning("two") 33 | 34 | mockedTrait.oneParamMethod(input) shouldBe output 35 | mockedTrait.oneParamMethod(2) shouldBe "two" 36 | } 37 | 38 | it should "allow to use mock defined in fixture-context with expectations predefined" in new TestSetupWithExpectationsPredefined { 39 | when(mockedTrait.oneParamMethod).expects(2).returning("two") 40 | 41 | mockedTrait.oneParamMethod(input) shouldBe output 42 | mockedTrait.oneParamMethod(2) shouldBe "two" 43 | } 44 | 45 | it should "allow mock defined in fixture-context to be used during context initialization" in new TestSetupWithHandlerCalledDuringInitialization { 46 | when(mockedTrait.oneParamMethod).expects(2).returning("two") 47 | 48 | mockedTrait.oneParamMethod(2) shouldBe "two" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/scala/eu/monniot/scala3mock/macros/utils.scala: -------------------------------------------------------------------------------- 1 | package eu.monniot.scala3mock.macros 2 | 3 | import scala.quoted.Quotes 4 | 5 | private[macros] object utils: 6 | 7 | def sortSymbolsViaSignature(using quotes: Quotes)( 8 | list: List[quotes.reflect.Symbol] 9 | ): List[quotes.reflect.Symbol] = 10 | import quotes.reflect.* 11 | list.sortBy { sym => 12 | val sig = sym.signature 13 | sig.resultSig + sig.paramSigs.map(_.toString()).mkString 14 | } 15 | 16 | def findMethodsToOverride(using quotes: Quotes)( 17 | sym: quotes.reflect.Symbol 18 | ): List[quotes.reflect.Symbol] = 19 | import quotes.reflect.* 20 | 21 | val objectMembers = Symbol.requiredClass("java.lang.Object").methodMembers 22 | val anyMembers = Symbol.requiredClass("scala.Any").methodMembers 23 | 24 | // First we refine the methods by removing the methods inherited from Object and Any 25 | val candidates = sym.methodMembers 26 | .filter { m => 27 | !(objectMembers.contains(m) || anyMembers.contains(m)) 28 | } 29 | .filterNot(_.flags.is(Flags.Private)) // Do not override private members 30 | 31 | // We then generate a list of methods to ignore for default values. We do this because the 32 | // compiler generate methods (following the `$default$` naming 33 | // scheme) to hold the default value of a parameter (and insert them automatically at call site). 34 | // We do not want to override those. 35 | val namesToIgnore = candidates 36 | .flatMap { sym => 37 | sym.paramSymss 38 | .filterNot(_.exists(_.isType)) 39 | .flatten 40 | .zipWithIndex 41 | .collect { 42 | case (parameter, position) 43 | if parameter.flags.is(Flags.HasDefault) => 44 | s"${sym.name}$$default$$${position + 1}" 45 | } 46 | } 47 | 48 | candidates.filterNot(m => namesToIgnore.contains(m.name)) 49 | end findMethodsToOverride 50 | -------------------------------------------------------------------------------- /core/src/test/scala/fixtures/TestTrait.scala: -------------------------------------------------------------------------------- 1 | package fixtures 2 | 3 | import some.other.pkg.SomeClass 4 | 5 | trait TestTrait { 6 | 7 | def nullary: String 8 | def noParams(): String 9 | def oneParam(x: Int): String 10 | def twoParams(x: Int, y: Double): String 11 | 12 | def overloaded(x: Int): String 13 | def overloaded(x: String): String 14 | def overloaded(x: Int, y: Double): String 15 | def overloaded[T](x: T): String 16 | 17 | def +(x: TestTrait): TestTrait 18 | 19 | def curried(x: Int)(y: Double): String 20 | def curriedFuncReturn(x: Int): Double => String 21 | 22 | def polymorphic[T](x: List[T]): String 23 | def polymorphicUnary[T1]: T1 24 | def polycurried[T1, T2](x: T1)(y: T2): (T1, T2) 25 | def polymorphicParam(x: (Int, Double)): String 26 | def repeatedParam(x: Int, ys: String*): String 27 | def byNameParam(x: => Int): String 28 | 29 | def implicitParam(x: Int)(implicit y: Double): String 30 | def usingParam(x: Int)(using y: Double): String 31 | def contextBound[T: ContextBound](x: T): String 32 | 33 | def upperBound[T <: Product](x: T): Int 34 | def lowerBound[T >: U, U](x: T, y: List[U]): String 35 | 36 | def withImplementation(x: Int) = x * x 37 | 38 | def referencesSomeOtherPackage(x: SomeClass): SomeClass 39 | def otherPackageUpperBound[T <: SomeClass](x: T): T 40 | def explicitPackageReference( 41 | x: yet.another.pkg.YetAnotherClass 42 | ): yet.another.pkg.YetAnotherClass 43 | def explicitPackageUpperBound[T <: yet.another.pkg.YetAnotherClass](x: T): T 44 | 45 | // doesn't seems to be possible with Scala 3. 46 | // We get an error saying "error overriding variable [...] cannot override a mutable variable" 47 | // var aVar: String 48 | // var concreteVar = "foo" 49 | 50 | // Here all we can do is making those non-abstract so the mock can compile. 51 | // By definition, there is no way to dynamically change the value of a val. 52 | val aVal: String 53 | val concreteVal = "foo" 54 | // val fnVal: String => Int 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | scalafmt: 9 | name: Scalafmt 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-java@v4 14 | with: 15 | distribution: 'temurin' 16 | java-version: '11' 17 | - uses: sbt/setup-sbt@v1 18 | - run: sbt scalafmtCheckAll 19 | docs: 20 | name: Website 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 0 26 | - uses: actions/setup-java@v4 27 | with: 28 | distribution: 'temurin' 29 | java-version: '11' 30 | - uses: sbt/setup-sbt@v1 31 | - run: sbt 'docs/mdoc' 32 | # Note that the first Scala LTS 3.3 is unfortunately not compatible 33 | # So we consider 3.2.2 as our baseline until we get a LTS we 34 | # can target. 35 | test-base: 36 | name: Test (3.2.2) 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v3 40 | - uses: actions/setup-java@v4 41 | with: 42 | distribution: 'temurin' 43 | java-version: '11' 44 | - uses: sbt/setup-sbt@v1 45 | - run: sbt test 46 | # We still want to be able to test out the library on newer Scala version. 47 | # For those version, we compile the library using 3.2 and then run the tests 48 | # against the newer version. This effectively emulate what our users are doing. 49 | test-integration: 50 | name: Integration Test (${{ matrix.scalaV }}) 51 | runs-on: ubuntu-latest 52 | strategy: 53 | fail-fast: false 54 | matrix: 55 | scalaV: 56 | - "3.3.3" 57 | - "3.4.2" 58 | - "3.5.0-RC7" 59 | steps: 60 | - uses: actions/checkout@v3 61 | - uses: actions/setup-java@v4 62 | with: 63 | distribution: 'temurin' 64 | java-version: '11' 65 | - uses: sbt/setup-sbt@v1 66 | - run: | 67 | sbt 'core/publishLocal' 68 | sbt '++${{ matrix.scalaV }}! integration/test' 69 | -------------------------------------------------------------------------------- /website/siteConfig.js: -------------------------------------------------------------------------------- 1 | // See https://docusaurus.io/docs/site-config for all the possible 2 | // site configuration options. 3 | 4 | const siteConfig = { 5 | title: 'scala3mock', 6 | tagline: 'Native Scala 3 mocking system', 7 | url: 'https://francois.monniot.eu', 8 | baseUrl: '/scala3mock/', 9 | repoUrl: 'https://github.com/fmonniot/scala3mock', 10 | projectName: 'scala3mock', 11 | githubHost: 'github.com', 12 | // For top-level user or org sites, the organization is still the same. 13 | // e.g., for the https://JoelMarcey.github.io site, it would be set like... 14 | organizationName: 'fmonniot', 15 | 16 | // For no header links in the top nav bar -> headerLinks: [], 17 | headerLinks: [ 18 | {doc: 'getting-started', label: 'Docs'}, 19 | //{href: `/scala3mock/api/0.x/index.html`, label: 'API'}, 20 | {href: 'https://github.com/fmonniot/scala3mock', label: "GitHub", external: true} 21 | ], 22 | 23 | /* path to images for header/footer */ 24 | headerIcon: 'img/logo.svg', 25 | footerIcon: 'img/logo.svg', 26 | favicon: 'img/favicon.png', 27 | 28 | /* Colors for website */ 29 | colors: { 30 | primaryColor: '#d36d6f', 31 | secondaryColor: '#294066', 32 | }, 33 | 34 | // This copyright info is used in /core/Footer.js and blog RSS/Atom feeds. 35 | copyright: `Copyright (c) 2022-2023 François Monniot`, 36 | 37 | highlight: { 38 | // Highlight.js theme to use for syntax highlighting in code blocks. 39 | theme: 'github-gist', 40 | defaultLang: 'plaintext' 41 | }, 42 | 43 | // Add custom scripts here that would be placed in