Skip to content

goshacodes/backstub

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GitHub Release

Backstub

  1. Tired of choosing between mocks and stubs?
  2. Tired of argument matchers and thrown exceptions when expectations not met?
  3. 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

Setup

Basic

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"

ZIO

For ZIO integration also add:

libraryDependencies += "io.github.goshacodes" %% "backstub-zio" % "<version_from_badge>"

Cats Effect

For Cats Effect integration also add:

libraryDependencies += "io.github.goshacodes" %% "backstub-cats-effect" % "<version_from_badge>"

API

Basic Stubs

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()
    

ZIO Stubs

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)

Cats Stubs

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)

stub

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] = ???

Expect

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]

ZIO Expect

The only difference is - you should choose method using methodIO.

Cats Effect Expect

The only difference is - you should choose method using methodF0 (if no arguments) or methodF.

Verify

backstub won't verify anything for you, it only returns the data.

It gives you 2 extension methods for your stubs:

  1. calls gives you the data
  2. 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"))

Verify ZIO

You can use callsIO and timesIO methods

Verify Cats Effect

You can use callsF and timesF methods

Overloaded 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)

Generic types support

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)

Example

Model - SessionCheckService.scala

Suite - SessionCheckServiceSpec.scala

Search Optimization

scala tests, scala test, testing in scala, scala testing, scalatest, specs2, scalamock, zio mock, mockito scala, backstub, backstub scala