- Tired of choosing between mocks and stubs?
- Tired of argument matchers and thrown exceptions when expectations not met?
- Or you just don't like 'mocks/stubs' approach entirely?
Then you've come to the right place, meet backstub - stubbing library for Scala 3 inspired by scalamock and compatible with any testing framework out of the box
Add to your build.sbt
libraryDependencies += "io.github.goshacodes" %% "backstub" % "<version_from_badge>"
Library hugely relies on experimental scala 3 features, so consider also adding
Test \ scalacOptions += "-experimental"
For ZIO integration also add:
libraryDependencies += "io.github.goshacodes" %% "backstub-zio" % "<version_from_badge>"
For Cats Effect integration also add:
libraryDependencies += "io.github.goshacodes" %% "backstub-cats-effect" % "<version_from_badge>"
Should be mixed with your test-suite, to provide clean-up API if you create your stubs per suite
package backstub
trait Stubs:
final given stubs: CreatedStubs = CreatedStubs()
final def resetStubs(): Unit = stubs.clearAll()
Using it as simple as:
import backstub.*
class MySpec extends munit.FunSuite with Stubs:
override def afterEach(context: AfterEach) =
resetStubs()
Gives you instance of StubEffect, allowing to integrate ZIO
package backstub
import zio.*
trait ZIOStubs extends Stubs:
// this method is actually in Stubs
final def resetStubsIO[F[+_, +_]: StubEffect]: F[Nothing, Unit] =
summon[StubEffect[F]].unit(resetStubs())
given effect.StubEffect[IO] with
def unit[T](t: => T): UIO[T] = ZIO.succeed(t)
def flatMap[E, EE >: E, T, T2](fa: IO[E, T])(f: T => IO[EE, T2]): IO[EE, T2] = fa.flatMap(f)
Gives you instance of StubEffect.Mono, allowing to integrate Cats Effect
package backstub
import cats.effect.*
trait CatsEffectStubs extends Stubs:
// this method is actually in Stubs
final def resetStubsF[F[+_]: StubEffect.Mono]: F[Unit] =
summon[StubEffect.Mono[F]].unit(resetStubs())
given StubEffect.Mono[IO] = new StubEffect.Mono[IO]:
def unit[T](t: => T): IO[T] = IO(t)
def flatMap[E, EE >: E, T, T2](fa: IO[T])(f: T => IO[T2]): IO[T2] = fa.flatMap(f)
Generates a stub. Without expectations setup just throws NotImplementedError
import backstub.*
trait Foo:
def zeroArgs: String
def oneArg(x: Int): Int
def moreArgs(x: Int, y: String): Option[String]
stub[Foo]
Will generate you:
new Foo:
def zeroArgs: String = ???
def oneArg(x: Int): Int = ???
def moreArgs(x: Int, y: String): Option[String] = ???
Compile time configuration allowing you to setup stub methods results.
Expectation on method can be set only once, so if you want to differentiate calls - use different data.
This also generates a collector for your calls.
import backstub.*
val foo = stub[Foo]:
Expect[Foo]
.method(_.oneArg).returns:
case 1 => 2
case 2 => 3
.method(_.moreArgs).returns(_ => None)
Will generate you:
new Foo:
def zeroArgs: String = ???
val calls$oneArg$1 = new AtomicReference[List[Int]](Nil)
def oneArg(x: Int): Int =
calls$oneArg$1.getAndUpdate(_ :+ x)
x match
case 1 => 2
case 2 => 3
val calls$moreArgs$2 = new AtomicReference[List[(Int, String)]](Nil)
def moreArgs(x: Int, y: String): Option[String] =
calls$moreArgs$2.getAndUpdate(_ :+ (x, y))
None
Also expectations can be provided via an inline given:
import backstub.*
inline given Expect[Foo] = Expect[Foo]
.method(_.oneArg).returns:
case 1 => 2
case 2 => 3
.method(_.moreArgs).returns(_ => None)
val foo = stub[Foo]
The only difference is - you should choose method using methodIO
.
The only difference is - you should choose method using methodF0
(if no arguments) or methodF
.
backstub won't verify anything for you, it only returns the data.
It gives you 2 extension methods for your stubs:
- calls gives you the data
- times gives you the number of times a method was called
import backstub.*
val foo = stub[Foo]
foo.oneArg(1)
foo.oneArg(2)
foo.times(_.oneArg) // 2
foo.calls(_.oneArg) // List(1, 2)
foo.twoArgs(5, "foo")
foo.times(_.twoArgs) // 1
foo.calls(_.twoArgs) // List((5, "foo"))
You can use callsIO
and timesIO
methods
You can use callsF
and timesF
methods
To set expectations for overloaded methods - method type should be specified
import backstub.*
trait Overloaded:
def overloaded(x: Int, y: Boolean): Int
def overloaded(x: String): Boolean
def overloaded: String
val overloaded = stub[Overloaded]:
Expect[Overloaded]:
.method(_.overloaded: String).returnsOnly("foo")
.method(_.overloaded: String => Boolean).returns(_ => true)
.method(_.overloaded: (Int, Boolean) => Int).returns((x, y) => x)
Current implementation has restriction of 1 expectation per method. It is same for generic types (Maybe it will change in the future if I find a solution).
import backstub.*
trait TypeArgs:
def typeArgs[A](x: A): A
val typeArgsStub = stub[TypeArgs]:
Expect[TypeArgs]:
.method(_.typeArgs[Int]).returns(x => x)
typeArgsStub.typeArgs[Int](2)
typeArgsStub.times(_.typeArgs)
typeArgsStub.calls(_.typeArgs)
Model - SessionCheckService.scala
Suite - SessionCheckServiceSpec.scala
scala tests, scala test, testing in scala, scala testing, scalatest, specs2, scalamock, zio mock, mockito scala, backstub, backstub scala