Skip to content

Instantly share code, notes, and snippets.

@truizlop
Created July 5, 2019 13:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save truizlop/bb859567ea8318dd81650ea736ce9d84 to your computer and use it in GitHub Desktop.
Save truizlop/bb859567ea8318dd81650ea736ce9d84 to your computer and use it in GitHub Desktop.
Testing effectful services with BowEffects (not released yet)
import Bow
import BowEffects
typealias UserID = String
struct UserProfile: Equatable {
let name: String
}
protocol Database {
var database: DatabaseService { get }
}
protocol DatabaseService {
func lookup(id: UserID) -> Task<UserProfile>
func update(id: UserID, profile: UserProfile) -> Task<()>
}
protocol Logger {
var logger: LoggerService { get }
}
protocol LoggerService {
func info(_ id: String) -> Task<()>
}
struct DatabaseTestService: DatabaseService {
let ref: Ref<IOPartial<Error>, State>
func lookup(id: UserID) -> Task<UserProfile> {
return ref.modify { db in
db.lookup(id: id)
}.flatMap { x in Task.invoke { x! } }^
}
func update(id: UserID, profile: UserProfile) -> Task<()> {
return ref.update { db in
db.update(id: id, profile: profile)
}^
}
struct State: Equatable {
let map: [UserID: UserProfile]
let ops: [String]
func log(_ op: String) -> State {
return State(map: map, ops: [op] + self.ops)
}
func lookup(id: UserID) -> (State, UserProfile?) {
return (log("Lookup(\(id))"), map[id])
}
func update(id: UserID, profile: UserProfile) -> State {
var newMap = map
newMap[id] = profile
return State(map: newMap, ops: ops).log("Update(\(id), \(profile))")
}
}
}
struct LoggerTestService: LoggerService {
let ref: Ref<IOPartial<Error>, [String]>
func info(_ line: String) -> Task<()> {
return ref.update { logs in logs + [line] }^
}
}
func lookedUpProfile<F: Database & Logger>(_ environment: F) -> Task<UserProfile> {
return Task<UserProfile>.binding(
{ environment.database.lookup(id: "abc") },
{ profile in environment.logger.info(profile.name) },
{ profile, _ in yield(profile) })^
}
struct TestEnvironment: Database, Logger {
let database: DatabaseService
let logger: LoggerService
}
func testScenario(_ state: DatabaseTestService.State, _ eff: (TestEnvironment) -> Task<UserProfile>) -> Task<(UserProfile, DatabaseTestService.State, [String])> {
let databaseRef = Ref<ForTask, DatabaseTestService.State>.unsafe(state)
let loggerRef = Ref<ForTask, [String]>.unsafe([])
let environment = TestEnvironment(database: DatabaseTestService(ref: databaseRef),
logger: LoggerTestService(ref: loggerRef))
return ForTask.binding(
{ eff(environment) },
{ _ in databaseRef.get() },
{ _, _ in loggerRef.get() },
{ profile, state, logs in yield((profile, state, logs)) })^
}
class Run {
static func main() {
let program = testScenario(DatabaseTestService.State(map: ["abc": UserProfile(name: "testName")], ops: []), lookedUpProfile)
program.unsafeRunAsync { either in
either.fold(
{ error in print(error) },
{ value in
print("User Profile: \(value.0)")
print("State: \(value.1)")
print("Logs: \(value.2)")
})
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment