├── 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