Expectation
An Expectation[R]
is an immutable tree structure that represents expectations on environment R
.
Defining Expectations
ZIO Test has a variety of expectations, such as value
, unit
, failure
, and never
. In this section we are going to learn each of these expectations and their variant, by mocking the UserService
service. So let's assume we have the following service:
import zio._
import zio.mock._
import zio.test._
case class User(id: String, name: String)
trait UserService {
def insert(user: User): IO[String, Unit]
def remove(id: String): IO[String, Unit]
def totalUsers: IO[String, Int]
def recentUsers(n: Int): IO[String, List[User]]
def removeAll: IO[String, Unit]
}
object UserService {
def totalUsers: ZIO[UserService, String, Int] =
ZIO.serviceWithZIO(_.totalUsers)
def insert(user: User): ZIO[UserService, String, Unit] =
ZIO.serviceWithZIO(_.insert(user))
def recentUsers(n: Int): ZIO[UserService, String, List[User]] =
ZIO.serviceWithZIO(_.recentUsers(n))
def remove(id: String): ZIO[UserService, String, Unit] =
ZIO.serviceWithZIO(_.remove(id))
def removeAll: ZIO[UserService, String, Unit] =
ZIO.serviceWithZIO(_.removeAll)
}
We can write the mock version of this class as below:
object MockUserService extends Mock[UserService] {
object Insert extends Effect[User, String, Unit]
object Remove extends Effect[String, String, Unit]
object RecentUsers extends Effect[Int, String, List[User]]
object TotalUsers extends Effect[Unit, String, Int]
object RemoveAll extends Effect[Unit, String, Unit]
val compose: URLayer[mock.Proxy, UserService] =
ZLayer {
for {
proxy <- ZIO.service[mock.Proxy]
} yield new UserService {
override def insert(user: User): IO[String, Unit] = proxy(Insert, user)
override def remove(id: String): IO[String, Unit] = proxy(Remove, id)
override def recentUsers(n: Int): IO[String, List[User]] = proxy(RecentUsers, n)
override def totalUsers: IO[String, Int] = proxy(TotalUsers)
override def removeAll: IO[String, Unit] = proxy(RemoveAll)
}
}
}
To create expectations we use the previously defined capability tags.
- For methods that take input, the first argument will be an assertion on input, and the second the predefined result.
import zio.mock._
import zio.test._
val exp01 = MockUserService.RecentUsers( // capability to build an expectation for
Assertion.equalTo(5), // assertion of the expected input argument
Expectation.value(List(User("1", "Jane Doe"), User("2", "John Doe"))) // result, that will be returned
)
- For methods that take no input, we only define the expected output:
val exp02 = MockUserService.TotalUsers(Expectation.value(42))
- For methods that may return
Unit
, we may skip the predefined result (it will default to successful value) or useunit
helper:
val exp03 = MockUserService.Remove(
Assertion.equalTo("1"),
Expectation.unit
)
- For methods that may return
Unit
and take no input we can skip both:
val exp04 = MockUserService.RemoveAll()
Providing Mocked Environment
Each expectation can be taught of a mocked environment. They can be converted to a ZLayer
implicitly. Therefore, we can compose them together and provide them to the environment of the SUT (System Under Test).
import zio.test._
import zio._
import zio.mock._
import zio.test.{test, _}
test("expecting simple value on call to nextInt") {
val sut = Random.nextInt
val mockEnv = MockRandom.NextInt(Expectation.value(5))
for {
total <- sut.provideLayer(mockEnv)
} yield assertTrue(total == 5)
}
Mocking Unused Collaborators
Often the dependency on a collaborator is only in some branches of the code. To test the correct behaviour of branches without dependencies, we still have to provide it to the environment, but we would like to assert it was never called. With the Mock.empty
method we can obtain a ZLayer
with an empty service (no calls expected):
import zio.mock._
import zio.test._
object MaybeConsoleSpec extends MockSpecDefault {
def spec = suite("processEvent")(
test("expect no call") {
def maybeConsole(invokeConsole: Boolean) =
ZIO.when(invokeConsole)(Console.printLine("foo"))
val sut1 = maybeConsole(false).unit
val mockEnv1 = MockConsole.empty
val sut2 = maybeConsole(true).unit
val mockEnv2 = MockConsole.PrintLine(
Assertion.equalTo("foo"),
Expectation.unit
)
for {
_ <- sut1.provideLayer(mockEnv1)
_ <- sut2.provideLayer(mockEnv2)
} yield assertTrue(true)
}
)
}
Mocking Multiple Collaborators
In some cases we have more than one collaborating service being called. We can create mocks for rich environments and as you enrich the environment by using capability tags from another service, the underlying mocked layer will be updated.
import zio._
import zio.mock._
import zio.test.{test, _}
test("mocking multiple collaborators") {
val sut =
for {
_ <- Console.printLine("What is your name?")
name <- Console.readLine.orDie
num <- Random.nextInt
_ <- Console.printLine(s"$name, your lucky number today is $num!")
} yield ()
val mockEnv: ULayer[Console with Random] = MockConsole.PrintLine(
Assertion.equalTo("What is your name?"),
Expectation.unit
) ++ MockConsole.ReadLine(Expectation.value("Mike")) ++
MockRandom.NextInt(Expectation.value(42)) ++
MockConsole.PrintLine(
Assertion.equalTo("Mike, your lucky number today is 42!"),
Expectation.unit
)
for {
_ <- sut.provideLayer(mockEnv)
} yield assertTrue(true)
}
Expectations
In the most robust example, the result can be either a successful value or a failure. In this section we are going to introduce all these cases, by using the proper expectation from zio.mock.Expectation
companion object:
value
Expecting a simple value:
import zio._
import zio.mock._
import zio.test.{test, _}
test("expecting simple value") {
val sut = UserService.totalUsers
val mockEnv = MockUserService.TotalUsers(Expectation.value(14))
for {
total <- sut.provideLayer(mockEnv)
} yield assertTrue(total == 14)
}
valueF
Expecting a value based on input arguments:
import zio._
import zio.mock._
import zio.test.{test, _}
test("an expectation based on input arguments") {
val sut = UserService.recentUsers(3)
val mockEnv = MockUserService.RecentUsers(
Assertion.isPositive,
Expectation.valueF(n =>
(1 to n).map(id => User(id.toString, "name")).toList
)
)
for {
users <- sut.provideLayer(mockEnv)
} yield assertTrue(users.map(_.id) == List("1", "2", "3"))
}
valueZIO
Expecting a value based on the input arguments and also the result of an effectful operation:
import zio._
import zio.mock._
import zio.test.{test, _}
test("effectful expectation") {
val sut = UserService.recentUsers(3)
val mockEnv = MockUserService.RecentUsers(
Assertion.isPositive,
Expectation.valueZIO(n =>
ZIO.foreach(1 to n) { n =>
Random
.nextUUID
.map(id => User(id.toString, s"name-$n"))
}.map(_.toList)
)
)
for {
users <- sut.provideLayer(mockEnv)
} yield assertTrue(List("name-1", "name-2", "name-3") == users.map(_.name))
}
unit
Expecting simple unit value:
import zio._
import zio.mock._
import zio.test.{test, _}
test("expecting unit") {
val sut = UserService.remove("1")
val mockEnv = MockUserService.Remove(
Assertion.isNonEmptyString,
Expectation.unit
)
for {
res <- sut.provideLayer(mockEnv).exit
} yield assertTrue(
res match {
case Exit.Success(()) => true
case _ => false
}
)
}
failure
Expecting a failure:
import zio._
import zio.mock._
import zio.test.{test, _}
test("failure expectation") {
val sut = UserService.totalUsers
val mockEnv = MockUserService.TotalUsers(Expectation.failure("connection failed"))
for {
total <- sut.provideLayer(mockEnv).exit
} yield assertTrue(
total match {
case Exit.Success(_) =>
false
case Exit.Failure(cause) =>
cause.contains(Cause.fail("connection failed"))
}
)
}
There are also failureF
and failureZIO
variants like what we described for value
expectation.
never
This expectation simulates a never-ending loop:
import zio._
import zio.mock._
import zio.test.{test, _}
test("never ending expectation") {
val sut = UserService.totalUsers
val mockEnv = MockUserService.TotalUsers(
Expectation.never
)
for {
r <- Live.live(
sut.provideLayer(mockEnv).timeout(500.millis)
)
} yield assertTrue(r.isEmpty)
}
Composing Expectations
We can combine our expectation to build complex scenarios using combinators defined in zio.mock.Expectation
.
and
The and
(alias &&
) operator composes two expectations, producing a new expectation to satisfy both in any order:
import zio._
import zio.mock._
import zio.test.{test, _}
test("satisfy both expectations with a logical `and` operator") {
val sut = UserService.recentUsers(5) *> UserService.totalUsers
val mockEnv = MockUserService.TotalUsers(Expectation.value(1)).and(
MockUserService.RecentUsers(
Assertion.isPositive,
Expectation.value(List(User("1", "user"))))
)
for {
total <- sut.provideLayer(mockEnv)
} yield assertTrue(total == 1)
}
or
The or
(alias ||
) operator composes two expectations, producing a new expectation to satisfy only one of them:
import zio._
import zio.mock._
import zio.test.{test, _}
test("satisfy one of expectations with a logical `or` operator") {
val sut = UserService.totalUsers
val mockEnv = MockUserService.TotalUsers(Expectation.value(1)).or(
MockUserService.RecentUsers(
Assertion.isPositive,
Expectation.value(List(User("1", "user"))))
)
for {
total <- sut.provideLayer(mockEnv)
} yield assertTrue(total == 1)
}
andThen
The andThen
(alias ++
) operator composes two expectations, producing a new expectation to satisfy both sequentially:
import zio._
import zio.mock._
import zio.test.{test, _}
test("satisfy sequence of two expectations with `andThen` operator") {
val sut = UserService.recentUsers(5) *> UserService.totalUsers
val mockEnv = MockUserService.RecentUsers(
Assertion.isPositive,
Expectation.value(List(User("1", "user")))
) andThen MockUserService.TotalUsers(Expectation.value(1))
for {
total <- sut.provideLayer(mockEnv)
} yield assertTrue(total == 1)
}
In the example above, changing the SUT to UserService.totalUsers *> UserService.remove(1)
will fail the test.
Exact Repetition
exactly
— Produces a new expectation to satisfy itself exactly the given number of times:
import zio._
import zio.mock._
import zio.test.{test, _}
test("satisfying exact repetition of a method call") {
val sut = ZIO.foreach(List("1", "2", "3", "4"))(id => UserService.remove(id))
val mockEnv = MockUserService.Remove(
Assertion.isNonEmptyString
).exactly(4)
for {
_ <- sut.provideLayer(mockEnv)
} yield assertTrue(true)
}
twice
andthrice
— Aliases forexactly(2)
andexactly(3)
.
Range of Repetitions
Expectation#repeats(range: Range)
— Repeats this expectation within given bounds, producing a new expectation to satisfy itself sequentially given number of times:
import zio._
import zio.mock._
import zio.test.{test, _}
test("expect repeated calls") {
val sut = Random.nextInt *> Random.nextInt
val mockEnv = MockRandom.NextInt(Expectation.value(42)).repeats(2 to 4)
for {
_ <- sut.provideLayer(mockEnv)
} yield assertTrue(true)
}
In the example above, if we repeat nextInt
less than 2 or over 4 times, the test will fail.
Another note on repetitions is that, if we compose expectations with andThen
/++
, once another repetition starts executing, it must be completed in order to satisfy the composite expectation. For example (A ++ B).repeats(1, 2)
will be satisfied by either A->B
(one repetition) or A->B->A->B
(two repetitions), but will fail on A->B->A
(incomplete second repetition):
import zio._
import zio.mock._
import zio.mock.Expectation._
import zio.test.{test, _}
test("if another repetition starts executing, it must be completed") {
val sut = Random.nextInt *> Random.nextBoolean *> Random.nextInt
val mockEnv = (MockRandom.NextInt(value(42)) ++ MockRandom.NextBoolean(value(true)))
.repeats(1 to 2)
for {
_ <- sut.provideLayer(mockEnv)
} yield assertTrue(true)
} @@ TestAspect.failing
- The
atLeast(min: Int)
,atMost(max: Int)
, andoptional
expectations are other variants forrepeats
expectation.
- The
atLeast
operator is a lower-bounded variant ofrepeated
, produces a new expectation to satisfy itself sequentially at least given number of times. - The
atMost
operator is an upper-bounded variant ofrepeated
, produces a new expectation to satisfy itself sequentially at most given number of times. - The
optional
operator is an alias foratMost(1)
, produces a new expectation to satisfy itself at most once.
MockClock
is a built-in mock version of the [Clock
] service.
Here is an example of mocking Clock.nanoTime
capability:
import zio._
import zio.mock._
import zio.mock.Expectation._
import zio.test.{test, _}
test("calling mocked nanoTime should return expected time") {
val app = Clock.nanoTime
val env = MockClock.NanoTime(value(1000L))
val out = app.provideLayer(env)
out.map(r => assertTrue(r == 1000L))
}
MockConsole
is a built-in mock version of the [Console
] service.
Here is an example of mocking Console.readLine
capability:
import zio._
import zio.mock._
import zio.test.{test, _}
test("calling mocked readline should return expected value") {
for {
line <- Console.readLine.provideLayer(
MockConsole.ReadLine(Expectation.value("foo"))
)
} yield assertTrue(line == "foo")
}
MockRandom
is a built-in mock version of the [Random
] service. It mocks all the random service capabilities.
Here's how we can mock the MockRandom.nextIntBounded
capability:
import zio._
import zio.mock._
import zio.test.{test, _}
test("expect call with input satisfying assertion and transforming it into output") {
for {
out <- Random.nextIntBounded(1).provideLayer(
MockRandom.NextIntBounded(
Assertion.equalTo(1),
Expectation.valueF(_ + 41)
)
)
} yield assertTrue(out == 42)
}
MockSystem
is a built-in mock version of the [System
] service. It mocks all the system service capabilities.
Here's how we can mock the MockSystem.property
capability:
import zio._
import zio.mock._
import zio.test.{test, _}
test("calling mocked property should return expected property") {
for {
property <- System.property("java.vm.name").provideLayer(
MockSystem.Property(
Assertion.equalTo("java.vm.name"),
Expectation.value(Some("OpenJDK 64-Bit Server VM"))
)
)
} yield assertTrue(property.get.contains("OpenJDK"))
}