Complete mocking system for creating controlled test doubles with expectation-based verification.
Base class for creating service mocks with expectation management.
/**
* Base class for service mocks
* @tparam M - Service type (usually Has[Service])
*/
abstract class Mock[-M <: Has[_]] {
/** Creates expectation for specific capability */
def expects[I, A](capability: Capability[M, I, A]): Expectation[I]
/** Composes this mock with another mock */
def compose[M1 <: Has[_]](that: Mock[M1]): Mock[M with M1]
}Expectation types for defining mock behavior and verification.
/**
* Represents an expectation for mock method calls
* @tparam I - Input parameter type
*/
sealed trait Expectation[-I] {
/** Combines with another expectation (both must be satisfied) */
def and[I1 <: I](that: Expectation[I1]): Expectation[I1]
/** Alternative expectation (either can be satisfied) */
def or[I1 <: I](that: Expectation[I1]): Expectation[I1]
/** Repeats expectation for specified range of calls */
def repeats(range: Range): Expectation[I]
/** Specifies return value for matching calls */
def returns[A](value: A): Expectation[I]
/** Specifies effectful return value */
def returnsM[R, E, A](effect: ZIO[R, E, A]): Expectation[I]
/** Specifies that calls should throw error */
def throws[E](error: E): Expectation[I]
/** Specifies that calls should die with throwable */
def dies(throwable: Throwable): Expectation[I]
}
object Expectation {
/** Expectation that matches specific value */
def value[A](assertion: Assertion[A]): Expectation[A]
/** Expectation that matches any value */
val unit: Expectation[Unit]
/** Expectation that never matches */
val never: Expectation[Any]
}Capability represents a mockable method or operation of a service.
/**
* Represents a capability (method) that can be mocked
* @tparam R - Service environment type
* @tparam I - Input parameter type
* @tparam A - Return type
*/
case class Capability[R <: Has[_], I, A](name: String) {
/** Creates expectation for this capability */
def apply(assertion: Assertion[I]): Expectation[I]
}
object Capability {
/** Creates capability with given name */
def of[M <: Has[_], I, A](name: String): Capability[M, I, A]
}Result types for specifying mock return values and behaviors.
/**
* Result of a mock method call
*/
sealed trait Result
object Result {
/** Successful result with value */
def succeed[A](value: A): Result
/** Failed result with error */
def fail[E](error: E): Result
/** Death result with throwable */
def die(throwable: Throwable): Result
/** Empty/unit result */
val unit: Result
}System for creating dynamic proxy instances from mocks.
/**
* Proxy factory for creating service instances from mocks
*/
object Proxy {
/** Creates service proxy from mock */
def make[R <: Has[_]](mock: Mock[R]): ULayer[R]
}Pre-built mocks for standard ZIO services.
/**
* Mock for Clock service
*/
object MockClock extends Mock[Clock] {
/** currentTime capability */
val CurrentTime: Capability[Clock, Any, OffsetDateTime]
/** currentDateTime capability */
val CurrentDateTime: Capability[Clock, Any, OffsetDateTime]
/** nanoTime capability */
val NanoTime: Capability[Clock, Any, Long]
/** sleep capability */
val Sleep: Capability[Clock, Duration, Unit]
}
/**
* Mock for Console service
*/
object MockConsole extends Mock[Console] {
/** putStr capability */
val PutStr: Capability[Console, String, Unit]
/** putStrLn capability */
val PutStrLn: Capability[Console, String, Unit]
/** putStrErr capability */
val PutStrErr: Capability[Console, String, Unit]
/** getStrLn capability */
val GetStrLn: Capability[Console, Any, String]
}
/**
* Mock for Random service
*/
object MockRandom extends Mock[Random] {
/** nextBoolean capability */
val NextBoolean: Capability[Random, Any, Boolean]
/** nextBytes capability */
val NextBytes: Capability[Random, Int, Chunk[Byte]]
/** nextDouble capability */
val NextDouble: Capability[Random, Any, Double]
/** nextFloat capability */
val NextFloat: Capability[Random, Any, Float]
/** nextInt capability */
val NextInt: Capability[Random, Any, Int]
/** nextIntBounded capability */
val NextIntBounded: Capability[Random, Int, Int]
/** nextLong capability */
val NextLong: Capability[Random, Any, Long]
}
/**
* Mock for System service
*/
object MockSystem extends Mock[System] {
/** env capability */
val Env: Capability[System, String, Option[String]]
/** property capability */
val Property: Capability[System, String, Option[String]]
/** lineSeparator capability */
val LineSeparator: Capability[System, Any, String]
}import zio.test._
import zio.test.mock._
// Define service interface
trait UserService {
def getUser(id: String): Task[User]
def createUser(user: User): Task[String]
}
// Create mock object
object MockUserService extends Mock[Has[UserService]] {
object GetUser extends Effect[String, Throwable, User]
object CreateUser extends Effect[User, Throwable, String]
val compose: URLayer[Has[Proxy], Has[UserService]] =
ZLayer.fromService { proxy =>
new UserService {
def getUser(id: String) = proxy(GetUser, id)
def createUser(user: User) = proxy(CreateUser, user)
}
}
}
// Use in tests
test("user service mock") {
val mockLayer = MockUserService.GetUser(
Expectation.value(equalTo("123")).returns(User("123", "John"))
)
val program = for {
userService <- ZIO.service[UserService]
user <- userService.getUser("123")
} yield user
assertM(program)(hasField("name", _.name, equalTo("John")))
.provideLayer(mockLayer)
}test("complex mock expectations") {
val mockLayer =
MockUserService.GetUser(
Expectation.value(equalTo("user1")).returns(User("user1", "Alice"))
) &&
MockUserService.GetUser(
Expectation.value(equalTo("user2")).returns(User("user2", "Bob"))
) &&
MockUserService.CreateUser(
Expectation.value(hasField("name", _.name, startsWith("Test")))
.returns("new-id-123")
.repeats(1 to 3)
)
val program = for {
service <- ZIO.service[UserService]
user1 <- service.getUser("user1")
user2 <- service.getUser("user2")
id1 <- service.createUser(User("", "Test User 1"))
id2 <- service.createUser(User("", "Test User 2"))
} yield (user1, user2, id1, id2)
assertM(program) {
case (u1, u2, id1, id2) =>
assert(u1.name)(equalTo("Alice")) &&
assert(u2.name)(equalTo("Bob")) &&
assert(id1)(equalTo("new-id-123")) &&
assert(id2)(equalTo("new-id-123"))
}.provideLayer(mockLayer)
}test("built-in service mocks") {
val clockMock = MockClock.CurrentTime(
Expectation.unit.returns(OffsetDateTime.now())
) ++ MockClock.Sleep(
Expectation.value(equalTo(1.second)).returns(())
)
val consoleMock = MockConsole.PutStrLn(
Expectation.value(equalTo("Hello")).returns(())
) ++ MockConsole.GetStrLn(
Expectation.unit.returns("input")
)
val program = for {
_ <- clock.currentTime
_ <- clock.sleep(1.second)
_ <- putStrLn("Hello")
input <- getStrLn
} yield input
assertM(program)(equalTo("input"))
.provideLayer(clockMock ++ consoleMock)
}test("mock failures and errors") {
val mockLayer = MockUserService.GetUser(
Expectation.value(equalTo("missing")).throws(new RuntimeException("User not found"))
) ++ MockUserService.CreateUser(
Expectation.value(hasField("name", _.name, isEmpty))
.fails(ValidationError("Name cannot be empty"))
)
val program1 = ZIO.service[UserService].flatMap(_.getUser("missing"))
val program2 = ZIO.service[UserService].flatMap(_.createUser(User("", "")))
assertM(program1.run)(dies(hasMessage(containsString("User not found")))) &&
assertM(program2.run)(fails(isSubtype[ValidationError]))
.provideLayer(mockLayer)
}test("call verification") {
val mockLayer = MockUserService.GetUser(
Expectation.value(anything).returns(User("", "")).repeats(2 to 4)
)
val program = for {
service <- ZIO.service[UserService]
_ <- service.getUser("1")
_ <- service.getUser("2")
_ <- service.getUser("3")
} yield ()
assertM(program)(isUnit).provideLayer(mockLayer)
}