Skip to content

Instantly share code, notes, and snippets.

@jdegoes
Last active January 6, 2023 14:08
  • Star 25 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save jdegoes/dd66656382247dc5b7228fb0f2cb97c8 to your computer and use it in GitHub Desktop.
Simple example of testing with ZIO environment
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)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment