Mock
A Mock[R]
represents a mockable environment R
. It's a base abstract class for every service we want to mock.
Creating a Mock Service​
In order to create a mock object, we should define an object which implements the Mock
abstract class in the test sources. To implement the Mock
need to define capability tags and the compose layer:
Encoding Service Capabilities​
Capabilities are service functionalities that are accessible from the client-side. For example, in the following service the send
method is a service capability:
import zio._
trait UserService {
def register(username: String, age: Int, email: String): Task[Unit]
}
A capability tag encodes all information needed to mock the target capability. It is just a value that extends the zio.mock.Capability[R, I, E, A]
type constructor, where:
R
is the type of environment the method belongs toI
is the type of methods input argumentsE
is the type of error it can fail withA
is the type of return value it can produce
The Capability
type is not publicly available, instead we have to extend Mock
dependent types Effect
, Method
, Sink
or Stream
.
We can have 4 types of capabilities inside a service:
Effect
— describes an effectful ZIO operationMethod
— describes an ordinary scala functionSink
— describes an effectful ZIO SinkStream
— describes an effectful ZIO Stream
Let's say we have the following service:
import zio._
import zio.mock._
import zio.stream._
trait ExampleService {
def exampleEffect(i: Int): Task[String]
def exampleMethod(i: Int): String
def exampleSink(a: Int): Sink[Throwable, Int, Nothing, List[Int]]
def exampleStream(a: Int): Stream[Throwable, String]
}
Therefore, the mock service should have the following capability tags:
import zio.mock._
object MockExampleService extends Mock[ExampleService] {
object ExampleEffect extends Effect[Int, Throwable, String]
object ExampleMethod extends Method[Int, Throwable, String]
object ExampleSink extends Sink[Any, Throwable, Int, Nothing, List[Int]]
object ExampleStream extends Stream[Int, Throwable, String]
override val compose: URLayer[Proxy, ExampleService] = ???
}
In this example, all ExampleEffect
, ExampleMethod
, ExampleSink
, and ExampleStream
are capability tags. Each of these capability tags encodes all information needed to mock the target capability.
For example, the ExampleEffect
capability tag encodes the type of environments, arguments (inputs), the error channel, and also the success channel of the exampleEffect(i: Int)
method.
We encode service capabilities according to the following scheme:
Encoding Zero Argument Capability​
For zero arguments the type is Unit
import zio._
trait ZeroParamService {
def zeroParams: Task[Int]
}
So the capability tag of zeroParams
should be:
import zio.mock._
object MockZeroParamService extends Mock[ZeroParamService] {
object ZeroParams extends Effect[Unit, Throwable, Int]
override val compose = ???
}
Encoding Multiple Arguments Capability​
For one or more arguments, regardless of how many parameter lists, the type is a TupleN
where N
is the size of arguments list
Note:
We're using tuples to represent multiple argument methods, which follows with a limit to max 22 arguments, as is Scala itself limited.
If the capability has more than one argument, we should encode the argument types in the Tuple
data type. For example, if we have the following service:
import zio._
trait ManyParamsService {
def manyParams(a: Int, b: String, c: Long): Task[Int]
def manyParamLists(a: Int, b: String)(c: Long): Task[Int]
}
We should encode that with the following capability tag:
import zio.mock._
trait MockExampleService extends Mock[ManyParamsService] {
object ManyParams extends Method[(Int, String, Long), Throwable, String]
object ManyParamLists extends Method[(Int, String, Long), Throwable, String]
override val compose = ???
}
Encoding Overloaded Capabilities​
For overloaded methods, we nest a list of numbered objects, each representing subsequent overloads:
// Main sources
import zio._
import zio.stream.{ ZSink, ZStream }
trait OverloadedService {
def overloaded(arg1: Int) : UIO[String]
def overloaded(arg1: Long) : UIO[String]
}
We encode both overloaded capabilities by using numbered objects inside a nested object:
// Test sources
import zio.mock._
object MockOervloadedService extends Mock[OverloadedService] {
object Overloaded {
object _0 extends Effect[Int, Nothing, String]
object _1 extends Effect[Long, Nothing, String]
}
val compose: URLayer[Proxy, OverloadedService] = ???
}
Encoding Polymorphic Capabilities​
Mocking polymorphic methods is also supported, but the interface must require zio.Tag
implicit evidence for each type parameter:
// main sources
import zio._
trait PolyService {
def polyInput[I: Tag](input: I): Task[String]
def polyError[E: Tag](input: Int): IO[E, String]
def polyOutput[A: Tag](input: Int): Task[A]
def polyAll[I: Tag, E: Tag, A: Tag](input: I): IO[E, A]
}
In the test sources we construct partially applied capability tags by extending Method.Poly
family. The unknown types must be provided at call site. To produce a final monomorphic Method
tag we must use the of
combinator and pass the missing types:
// test sources
import zio.mock._
object MockPolyService extends Mock[PolyService] {
object PolyInput extends Poly.Effect.Input[Throwable, String]
object PolyError extends Poly.Effect.Error[Int, String]
object PolyOutput extends Poly.Effect.Output[Int, Throwable]
object PolyAll extends Poly.Effect.InputErrorOutput
// We will learn about the compose layer in the next section
val compose: URLayer[Proxy, PolyService] =
ZLayer {
for {
proxy <- ZIO.service[Proxy]
} yield new PolyService {
def polyInput[I: Tag](input: I) = proxy(PolyInput.of[I], input)
def polyError[E: Tag](input: Int) = proxy(PolyError.of[E], input)
def polyOutput[A: Tag](input: Int) = proxy(PolyOutput.of[A], input)
def polyAll[I: Tag, E: Tag, A: Tag](input: I) = proxy(PolyAll.of[I, E, A], input)
}
}
}
Similarly, we use the same of
combinator to refer to concrete monomorphic call in our test suite when building expectations:
import zio.test._
import MockPolyService._
val exp06 = PolyInput.of[String](
Assertion.equalTo("foo"),
Expectation.value("bar")
)
val exp07 = PolyInput.of[Int](
Assertion.equalTo(42),
Expectation.failure(new Exception)
)
val exp08 = PolyInput.of[Long](
Assertion.equalTo(42L),
Expectation.value("baz")
)
val exp09 = PolyAll.of[Int, Throwable, String](
Assertion.equalTo(42),
Expectation.value("foo")
)
val exp10 = PolyAll.of[Int, Throwable, String](
Assertion.equalTo(42),
Expectation.failure(new Exception)
)
Defining a Layer for the Mocked Service​
Finally, we need to define a compose layer that can create our environment from a Proxy
. A Proxy
holds the mock state and serves predefined responses to calls.
So again, assume we have the following service:
import zio._
import zio.mock._
trait ExampleService {
def exampleEffect(i: Int): Task[String]
def exampleMethod(i: Int): String
def exampleSink(a: Int): stream.Sink[Throwable, Int, Nothing, List[Int]]
def exampleStream(a: Int): stream.Stream[Throwable, String]
}
In this step, we need to provide a layer in which used to construct the mocked object. To do that, we should obtain the Proxy
data type from the environment and then implement the service interface by wrapping all capability tags with proxy:
object MockExampleService extends Mock[ExampleService] {
object ExampleEffect extends Effect[Int, Throwable, String]
object ExampleMethod extends Method[Int, Throwable, String]
object ExampleSink extends Sink[Any, Throwable, Int, Nothing, List[Int]]
object ExampleStream extends Stream[Int, Throwable, String]
override val compose: URLayer[Proxy, ExampleService] =
ZLayer {
ZIO.serviceWithZIO[Proxy] { proxy =>
withRuntime[Proxy, ExampleService] { runtime =>
ZIO.succeed {
new ExampleService {
override def exampleEffect(i: Int): Task[String] =
proxy(ExampleEffect, i)
override def exampleMethod(i: Int): String =
Unsafe.unsafe { implicit unsafe =>
runtime.unsafe.run(proxy(ExampleMethod, i)).getOrThrow()
}
override def exampleSink(a: Int): stream.Sink[Throwable, Int, Nothing, List[Int]] =
Unsafe.unsafe { implicit unsafe =>
runtime.unsafe.run(proxy(ExampleSink, a)).getOrThrow()
}
override def exampleStream(a: Int): stream.Stream[Throwable, String] =
Unsafe.unsafe { implicit unsafe =>
runtime.unsafe.run(proxy(ExampleStream, a)).getOrThrow()
}
}
}
}
}
}
}
Note: The
withRuntime
helper is defined inMock
. It accesses the Runtime viaZIO.runtime
and if you're on JS platform, it will replace the executor to an unyielding one.
A reference to this layer is passed to capability tags, so it can be used to automatically build environment for composed expectations on multiple services.
The Complete Example​
trait AccountEvent
// main sources
import zio._
import zio.mock._
trait AccountObserver {
def processEvent(event: AccountEvent): UIO[Unit]
def runCommand(): UIO[Unit]
}
object AccountObserver {
def processEvent(event: AccountEvent) =
ZIO.serviceWithZIO[AccountObserver](_.processEvent(event))
def runCommand() =
ZIO.serviceWithZIO[AccountObserver](_.runCommand())
}
case class AccountObserverLive(console: Console) extends AccountObserver {
def processEvent(event: AccountEvent): UIO[Unit] =
for {
_ <- console.printLine(s"Got $event").orDie
line <- console.readLine.orDie
_ <- console.printLine(s"You entered: $line").orDie
} yield ()
def runCommand(): UIO[Unit] =
console.printLine("Done!").orDie
}
object AccountObserverLive {
val layer =
ZLayer {
for {
console <- ZIO.service[Console]
} yield AccountObserverLive(console)
}
}
// test sources
object AccountObserverMock extends Mock[AccountObserver] {
object ProcessEvent extends Effect[AccountEvent, Nothing, Unit]
object RunCommand extends Effect[Unit, Nothing, Unit]
val compose: URLayer[Proxy, AccountObserver] =
ZLayer {
for {
proxy <- ZIO.service[Proxy]
} yield new AccountObserver {
def processEvent(event: AccountEvent) = proxy(ProcessEvent, event)
def runCommand(): UIO[Unit] = proxy(RunCommand)
}
}
}
More examples​
We can find more examples in the examples
and test-tests
subproject:
An Expectation[R]
is an immutable tree structure that represents expectations on environment R
.