object test { import scalaz.zio._ type UserID = String case class UserProfile(name: String) // The database module: trait Database { val database: Database.Service } object Database { // The database module contains the database service: trait Service { def lookup(id: UserID): Task[UserProfile] def update(id: UserID, profile: UserProfile): Task[Unit] } } // The logger module: trait Logger { def logger: Logger.Service } object Logger { // The logger module contains the logger service: trait Service { def info(id: String): Task[Unit] } } // A concurrent-safe test database service, which uses a `Ref` to keep track // of changes to the test database state: class DatabaseTestService(ref: Ref[DatabaseTestService.State]) extends Database.Service { def lookup(id: UserID): Task[UserProfile] = ref.modify(_.lookup(id)).flatMap(option => Task(option.get)) def update(id: UserID, profile: UserProfile): Task[Unit] = ref.update(_.update(id, profile)).unit } object DatabaseTestService { // The database state, which keeps track of the data as well as a log of // database operations performed against the database: final case class State(map: Map[UserID, UserProfile], ops: List[String]) { def log(op: String): State = copy(ops = op :: ops) def lookup(id: UserID): (Option[UserProfile], State) = (map.get(id), log(s"Lookup(${id})")) def update(id: UserID, profile: UserProfile): State = copy(map = map + (id -> profile)).log(s"Update(${id}, ${profile})") } } // A concurrent-safe test logger service, which uses a `Ref` to keep track // of log output: class LoggerTestService(ref: Ref[Vector[String]]) extends Logger.Service { def info(line: String): Task[Unit] = ref.update(_ :+ line).unit } // A helper function to run a test scenario, and extract out test data. // This function can be used many times across many unit tests. def testScenario[E, A]( state: DatabaseTestService.State )(eff: ZIO[Database with Logger, E, A]): UIO[(Either[E, A], DatabaseTestService.State, Vector[String])] = for { databaseRef <- Ref.make(state) loggerRef <- Ref.make(Vector.empty[String]) // Construct a new environment for the effect being tested: env = new Database with Logger { val database = new DatabaseTestService(databaseRef) val logger = new LoggerTestService(loggerRef) } either <- eff.provide(env).either dbState <- databaseRef.get loggerState <- loggerRef.get } yield (either, dbState, loggerState) // An example program that uses database and logger modules: val lookedUpProfile: ZIO[Database with Logger, Throwable, UserProfile] = ZIO.accessM[Logger with Database] { modules => import modules.database import modules.logger for { profile <- database.lookup("abc") _ <- logger.info(profile.name) } yield profile } // Running a test scenario and unsafely executing it to see what happens: val v = testScenario(DatabaseTestService.State(Map("abc" -> UserProfile("testName")), Nil))(lookedUpProfile) val runtime = new DefaultRuntime {} runtime.unsafeRun(v) }