Skip to content

Commit

Permalink
Added test mocking
Browse files Browse the repository at this point in the history
  • Loading branch information
Kalin-Rudnicki committed Mar 20, 2024
1 parent 433b079 commit ba621ce
Show file tree
Hide file tree
Showing 10 changed files with 745 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- uses: olafurpg/setup-scala@v11
with:
java-version: openjdk@1.11
- run: sbt ci-release
- run: sbt harness-modules/ci-release
env:
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
PGP_SECRET: ${{ secrets.PGP_SECRET }}
Expand Down
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ lazy val `harness-zio-test` =
name := "harness-zio-test",
publishSettings,
miscSettings,
testSettings,
)
.dependsOn(
`harness-zio` % testAndCompile,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package harness.zio.test.mock

import java.util.UUID
import zio.*

abstract class Mock[Z](implicit rTag: Tag[Z]) { myMock =>

private[mock] final val mockId: UUID = UUID.randomUUID

override final def hashCode: Int = mockId.hashCode

override final def equals(that: Any): Boolean =
that.asInstanceOf[Matchable] match {
case that: Mock[?] => this.mockId == that.mockId
case _ => false
}

final def name: String =
myMock.getClass.getSimpleName.stripSuffix("$")

abstract class Capability[I, R, E, A] { myCapability =>

private[mock] final val capabilityId: UUID = UUID.randomUUID

override final def hashCode: Int = capabilityId.hashCode

override final def equals(that: Any): Boolean =
that.asInstanceOf[Matchable] match {
case that: Capability[?, ?, ?, ?] => this.capabilityId == that.capabilityId
case _ => false
}

final def name: String =
s"${myMock.name}<${myCapability.getClass.getSimpleName.stripSuffix("$")}>"

// =====| |=====

private[mock] def getMock: Mock[Z] = myMock

// =====| |=====

/**
* Use these when creating the mocked layer.
* It allows you to specify how the capability should be implemented.
* These differ from seeded expectations,
* in that implementations don't care what order they are called in,
* or if they are called at all.
*/
object implement {

def zioI(f: I => ZIO[R, E, A]): Mocked[Z] =
Mocked.single(myCapability, f)

def zio(f: => ZIO[R, E, A]): Mocked[Z] =
zioI(_ => f)

def successI(f: I => A): Mocked[Z] =
zioI(i => ZIO.succeed(f(i)))

def success(f: => A): Mocked[Z] =
zioI(_ => ZIO.succeed(f))

def failureI(f: I => E): Mocked[Z] =
zioI(i => ZIO.fail(f(i)))

def failure(f: => E): Mocked[Z] =
zioI(_ => ZIO.fail(f))

}

/**
* Use these to seed expected calls to the capability.
* It is very important to note that ordering is enforced.
* If you seed:
* - `MyMock.Capability1.seed.success(1)`
* - `MyMock.Capability2.seed.success("test")`
* and then call `capability2()` before calling `capability1()`,
* ZIO will die with an error letting you know that a call to `capability2()` is expected next.
*/
object seed {

// append

def zioI(f: I => ZIO[R, E, A]): URIO[Proxy & Z, Unit] =
ZIO.serviceWithZIO[Proxy](_.appendSeed(myCapability, f))

def zio(f: => ZIO[R, E, A]): URIO[Proxy & Z, Unit] =
zioI(_ => f)

def successI(f: I => A): URIO[Proxy & Z, Unit] =
zioI(i => ZIO.succeed(f(i)))

def success(f: => A): URIO[Proxy & Z, Unit] =
zioI(_ => ZIO.succeed(f))

def failureI(f: I => E): URIO[Proxy & Z, Unit] =
zioI(i => ZIO.fail(f(i)))

def failure(f: => E): URIO[Proxy & Z, Unit] =
zioI(_ => ZIO.fail(f))

object prepend {

def zioI(f: I => ZIO[R, E, A]): URIO[Proxy & Z, Unit] =
ZIO.serviceWithZIO[Proxy](_.prependSeed(myCapability, f))

def zio(f: => ZIO[R, E, A]): URIO[Proxy & Z, Unit] =
zioI(_ => f)

def successI(f: I => A): URIO[Proxy & Z, Unit] =
zioI(i => ZIO.succeed(f(i)))

def success(f: => A): URIO[Proxy & Z, Unit] =
zioI(_ => ZIO.succeed(f))

def failureI(f: I => E): URIO[Proxy & Z, Unit] =
zioI(i => ZIO.fail(f(i)))

def failure(f: => E): URIO[Proxy & Z, Unit] =
zioI(_ => ZIO.fail(f))

}

object append {

def zioI(f: I => ZIO[R, E, A]): URIO[Proxy & Z, Unit] =
ZIO.serviceWithZIO[Proxy](_.appendSeed(myCapability, f))

def zio(f: => ZIO[R, E, A]): URIO[Proxy & Z, Unit] =
zioI(_ => f)

def successI(f: I => A): URIO[Proxy & Z, Unit] =
zioI(i => ZIO.succeed(f(i)))

def success(f: => A): URIO[Proxy & Z, Unit] =
zioI(_ => ZIO.succeed(f))

def failureI(f: I => E): URIO[Proxy & Z, Unit] =
zioI(i => ZIO.fail(f(i)))

def failure(f: => E): URIO[Proxy & Z, Unit] =
zioI(_ => ZIO.fail(f))

}

}

}

abstract class Effect[I, E, A] extends Capability[I, Any, E, A]

// =====| |=====

def empty: Mocked[Z] = Mocked.empty[Z](myMock)

protected def buildInternal(proxy: Proxy): Z

private[mock] final def build(proxy: Proxy): ZEnvironment[Z] = ZEnvironment(buildInternal(proxy))

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package harness.zio.test.mock

import cats.syntax.list.*
import harness.zio.test.mock.Types.*
import harness.zio.test.mock.error.MockError
import zio.*

sealed trait Mocked[Z] { self =>

final def ++[Z2](that: Mocked[Z2]): Mocked[Z & Z2] =
(self.asInstanceOf[Mocked[Z & Z2]], that.asInstanceOf[Mocked[Z & Z2]]) match {
case (self: Mocked.Building[Z & Z2], that: Mocked.Building[Z & Z2]) =>
(self.impls.keySet & that.impls.keySet).toList.toNel match {
case None =>
new Mocked.Building[Z & Z2](
impls = self.impls ++ that.impls,
mocks = self.mocks | that.mocks,
)
case Some(conflictingCapabilities) =>
Mocked.FailedDuringBuild(MockError.OverlappingCapabilityImplementations(conflictingCapabilities))
}
case (self: Mocked.FailedDuringBuild[Z & Z2], _) => self
case (_: Mocked.Building[Z & Z2], that: Mocked.FailedDuringBuild[Z & Z2]) => that
}

final def toLayer(implicit envTag: EnvironmentTag[Z]): ULayer[Proxy & Z] =
self match {
case self: Mocked.Building[Z] =>
Proxy.make(self.impls.asInstanceOf[Map[ErasedCapability, ErasedEffectImpl]]) >+>
ZLayer.fromZIOEnvironment {
ZIO.serviceWith[Proxy] { proxy =>
val zEnvs: List[ZEnvironment[?]] =
self.mocks.toList.map { _.build(proxy) }
val zEnv: ZEnvironment[Z] =
zEnvs.foldLeft(ZEnvironment.empty) { _ ++ _ }.asInstanceOf[ZEnvironment[Z]]

zEnv
}
}
case Mocked.FailedDuringBuild(error) =>
ZLayer.die(error)
}

}
object Mocked {

// =====| Builders |=====

private final class Building[Z](
private[Mocked] val impls: Map[ErasedCapabilityZ[? >: Z], ErasedEffectImpl],
private[Mocked] val mocks: Set[Mock[? >: Z]],
) extends Mocked[Z]

private final case class FailedDuringBuild[Z](
error: Throwable,
) extends Mocked[Z]

// =====| |=====

private[mock] def single[Z, I, R, E, A](capability: Mock[Z]#Capability[I, R, E, A], effect: I => ZIO[R, E, A]): Mocked[Z] =
new Mocked.Building[Z](
Map(capability -> effect),
Set(capability.getMock),
)

private[mock] def empty[Z](mock: Mock[Z]): Mocked[Z] =
new Mocked.Building[Z](
Map.empty,
Set(mock),
)

}

0 comments on commit ba621ce

Please sign in to comment.