モックでOKとNGを交互に返すようなものを作りたくなったので調査
前回
エラー追加したもの
GlobalValueを使用して、呼び出し回数を数えている
import { Cache, Context, Data, Duration, Effect, GlobalValue, Layer, pipe } from "effect"
type type_employee_id = string
type type_department_id = string
class DatabaseError extends Data.TaggedError("DatabaseError")<{
table_name: string
}> {}
class DomainError extends Data.TaggedError("DomainError")<{
reason: string
}> {}
// ---- test counter ----
const testCounter = GlobalValue.globalValue("testCounter", () => {
let count = 0
return {
get: pipe(Effect.sync(() => count), Effect.tap((count) => Effect.log(`testCounter-get : ${count}`))),
increment: Effect.sync(() => {
count++
})
}
})
// ---- db : employee ----
interface interface_employee {
employee_id: type_employee_id
employee_name: string
department_id: type_department_id
}
class database_employee extends Context.Tag("database_employee")<
database_employee,
{
readonly get_one_employee: (
employee_id: type_employee_id
) => Effect.Effect<interface_employee, DatabaseError>
}
>() {}
const database_employee_mock_ok = Layer.succeed(
database_employee,
{
get_one_employee: (employee_id: type_employee_id) =>
Effect.gen(function*() {
yield* Effect.log("database_employee_mock_ok start")
yield* Effect.sleep(Duration.seconds(5))
const data: interface_employee = {
employee_id,
employee_name: "test employee",
department_id: test_department_id
}
yield* Effect.log("database_employee_mock_ok end")
return data
})
}
)
const database_employee_mock_ng_database_error = Layer.succeed(
database_employee,
{
get_one_employee: (_: type_employee_id) =>
Effect.gen(function*() {
yield* Effect.log("database_employee_mock_ng_database_error start")
return yield* Effect.fail(new DatabaseError({ table_name: "employee" }))
})
}
)
const database_employee_mock_ng_database_error_with_counter = (ng_count: number) =>
Layer.succeed(
database_employee,
{
get_one_employee: (employee_id: type_employee_id) =>
Effect.gen(function*() {
yield* Effect.log("database_employee_mock_ng_database_error_with_counter start")
yield* testCounter.increment
const count = yield* testCounter.get
if (count === ng_count) {
return yield* Effect.fail(new DatabaseError({ table_name: "employee" }))
} else {
const data: interface_employee = {
employee_id,
employee_name: "test employee",
department_id: test_department_id
}
yield* Effect.log("database_employee_mock_ng_database_error_with_counter end")
return data
}
})
}
)
const employee_cache = Cache.make({
capacity: 100,
timeToLive: Duration.seconds(5),
lookup: (employee_id: type_employee_id) =>
Effect.gen(function*() {
yield* Effect.log("employee_cache lookup start")
const DatabaseEmployee = yield* database_employee
const employee = yield* DatabaseEmployee.get_one_employee(employee_id)
return employee
})
})
// ---- db : department ----
interface interface_department {
department_id: type_department_id
department_name: string
}
class database_department extends Context.Tag("database_department")<
database_department,
{
readonly get_one_department: (
department_id: type_department_id
) => Effect.Effect<interface_department, DatabaseError>
}
>() {}
const database_department_mock_ok = Layer.succeed(
database_department,
{
get_one_department: (department_id: type_department_id) =>
Effect.gen(function*() {
yield* Effect.log("get_one_department ok start")
yield* Effect.sleep(Duration.seconds(3))
const data: interface_department = {
department_id,
department_name: "test department"
}
yield* Effect.log("get_one_department end")
return data
})
}
)
const database_department_mock_ng_database_error = Layer.succeed(
database_department,
{
get_one_department: (_: type_department_id) =>
Effect.gen(function*() {
yield* Effect.log("get_one_department ng start")
return yield* Effect.fail(new DatabaseError({ table_name: "department" }))
})
}
)
const department_cache = Cache.make({
capacity: 100,
timeToLive: Duration.seconds(5),
lookup: (department_id: type_department_id) =>
Effect.gen(function*() {
yield* Effect.log("department_cache lookup start")
const DatabaseDepartment = yield* database_department
const department = yield* DatabaseDepartment.get_one_department(department_id)
return department
})
})
// ---- test data ----
const test_employee_id: type_employee_id = "e_00_test"
const test_department_id: type_department_id = "d_00_test"
// ---- domain -----------------
interface employee_data {
employee: interface_employee
department: interface_department
}
class domain_employee extends Context.Tag("domain_employee")<
domain_employee,
{
readonly get_one_employee_data: (
employee_id: type_employee_id
) => Effect.Effect<employee_data, DatabaseError | DomainError>
}
>() {}
const domain_employee_service = Layer.effect(
domain_employee,
Effect.gen(function*() {
yield* Effect.log("domain_employee_service start")
const CacheEmployee = yield* employee_cache
const CacheDepartment = yield* department_cache
return {
get_one_employee_data: (
employee_id: type_employee_id
) =>
Effect.gen(function*() {
yield* Effect.log("get_one_employee_data start")
const isHitEmployee = yield* CacheEmployee.contains(employee_id)
if (isHitEmployee) yield* Effect.log("employee_data from cache")
const employee = yield* CacheEmployee.get(employee_id)
const isHitDepartment = yield* CacheDepartment.contains(employee.department_id)
if (isHitDepartment) yield* Effect.log("department_data from cache")
const department = yield* CacheDepartment.get(employee.department_id)
const result: employee_data = {
employee,
department
}
return result
})
}
})
)
const domain_employee_service_ng = Layer.effect(
domain_employee,
Effect.gen(function*() {
yield* Effect.log("domain_employee_service_ng start")
return {
get_one_employee_data: (
_: type_employee_id
) =>
Effect.gen(function*() {
yield* Effect.log("get_one_employee_data start")
return yield* Effect.fail(new DomainError({ reason: "原因不明のエラーが発生" }))
})
}
})
)
const get_data_from_domain = Effect.gen(function*() {
yield* Effect.log("get_data_from_domain start")
const DomainEmployee = yield* domain_employee
const result_employee = yield* DomainEmployee.get_one_employee_data(test_employee_id)
yield* Effect.log(result_employee)
const result_employee_2 = yield* DomainEmployee.get_one_employee_data(test_employee_id)
yield* Effect.log(result_employee_2)
yield* Effect.sleep(Duration.seconds(5))
const result_employee_3 = yield* DomainEmployee.get_one_employee_data(test_employee_id)
yield* Effect.log(result_employee_3)
}).pipe(
Effect.catchTags({
DatabaseError: (error) => Effect.logError(`DatabaseErrorが発生しました : ${error.table_name}`),
DomainError: (error) => Effect.logError(`DomainErrorが発生しました : ${error.reason}`)
})
)
// ---- layer ----
const domain_service_ok = Effect.provide(get_data_from_domain, domain_employee_service)
const domain_service_ng = Effect.provide(get_data_from_domain, domain_employee_service_ng)
const service_mock_ok = Layer.mergeAll(database_employee_mock_ok, database_department_mock_ok)
const service_mock_ng_employee = Layer.mergeAll(
database_employee_mock_ng_database_error,
database_department_mock_ok
)
const service_mock_ng_employee_with_counter_1 = Layer.mergeAll(
database_employee_mock_ng_database_error_with_counter(1),
database_department_mock_ok
)
const service_mock_ng_employee_with_counter_2 = Layer.mergeAll(
database_employee_mock_ng_database_error_with_counter(2),
database_department_mock_ok
)
const service_mock_ng_employee_with_counter_3 = Layer.mergeAll(
database_employee_mock_ng_database_error_with_counter(3),
database_department_mock_ok
)
const service_mock_ng_department = Layer.mergeAll(
database_employee_mock_ok,
database_department_mock_ng_database_error
)
// ---- program ----
const program_domain_mock_ok = domain_service_ok.pipe(Effect.provide(service_mock_ok))
const program_domain_mock_ng = domain_service_ng.pipe(Effect.provide(service_mock_ok))
const program_domain_mock_ng_employee = domain_service_ok.pipe(Effect.provide(service_mock_ng_employee))
const program_domain_mock_ng_employee_with_counter_1 = domain_service_ok.pipe(
Effect.provide(service_mock_ng_employee_with_counter_1)
)
const program_domain_mock_ng_employee_with_counter_2 = domain_service_ok.pipe(
Effect.provide(service_mock_ng_employee_with_counter_2)
)
const program_domain_mock_ng_employee_with_counter_3 = domain_service_ok.pipe(
Effect.provide(service_mock_ng_employee_with_counter_3)
)
const program_domain_mock_ng_department = domain_service_ok.pipe(Effect.provide(service_mock_ng_department))
// ---- Entry Point ----
Effect.runPromise(program_domain_mock_ng_employee_with_counter_3).catch((reason) => {
console.log(reason)
})
呼び出し回数1でNG
最後を以下にする
Effect.runPromise(program_domain_mock_ng_employee_with_counter_1).catch((reason) => {
console.log(reason)
})
実行結果
timestamp=2025-10-05T13:20:20.396Z level=INFO fiber=#0 message="domain_employee_service start"
timestamp=2025-10-05T13:20:20.404Z level=INFO fiber=#0 message="get_data_from_domain start"
timestamp=2025-10-05T13:20:20.406Z level=INFO fiber=#0 message="get_one_employee_data start"
timestamp=2025-10-05T13:20:20.410Z level=INFO fiber=#0 message="employee_cache lookup start"
timestamp=2025-10-05T13:20:20.411Z level=INFO fiber=#0 message="database_employee_mock_ng_database_error_with_counter start"
timestamp=2025-10-05T13:20:20.413Z level=INFO fiber=#0 message="testCounter-get : 1"
timestamp=2025-10-05T13:20:20.417Z level=ERROR fiber=#0 message="DatabaseErrorが発生しました : employee"
呼び出し回数2でNG
最後を以下にする
Effect.runPromise(program_domain_mock_ng_employee_with_counter_2).catch((reason) => {
console.log(reason)
})
実行結果
timestamp=2025-10-05T13:21:27.535Z level=INFO fiber=#0 message="domain_employee_service start"
timestamp=2025-10-05T13:21:27.543Z level=INFO fiber=#0 message="get_data_from_domain start"
timestamp=2025-10-05T13:21:27.544Z level=INFO fiber=#0 message="get_one_employee_data start"
timestamp=2025-10-05T13:21:27.547Z level=INFO fiber=#0 message="employee_cache lookup start"
timestamp=2025-10-05T13:21:27.548Z level=INFO fiber=#0 message="database_employee_mock_ng_database_error_with_counter start"
timestamp=2025-10-05T13:21:27.550Z level=INFO fiber=#0 message="testCounter-get : 1"
timestamp=2025-10-05T13:21:27.551Z level=INFO fiber=#0 message="database_employee_mock_ng_database_error_with_counter end"
timestamp=2025-10-05T13:21:27.553Z level=INFO fiber=#0 message="department_cache lookup start"
timestamp=2025-10-05T13:21:27.555Z level=INFO fiber=#0 message="get_one_department ok start"
timestamp=2025-10-05T13:21:30.571Z level=INFO fiber=#0 message="get_one_department end"
timestamp=2025-10-05T13:21:30.574Z level=INFO fiber=#0 message="{
\"employee\": {
\"employee_id\": \"e_00_test\",
\"employee_name\": \"test employee\",
\"department_id\": \"d_00_test\"
},
\"department\": {
\"department_id\": \"d_00_test\",
\"department_name\": \"test department\"
}
}"
timestamp=2025-10-05T13:21:30.576Z level=INFO fiber=#0 message="get_one_employee_data start"
timestamp=2025-10-05T13:21:30.577Z level=INFO fiber=#0 message="employee_data from cache"
timestamp=2025-10-05T13:21:30.580Z level=INFO fiber=#0 message="department_data from cache"
timestamp=2025-10-05T13:21:30.582Z level=INFO fiber=#0 message="{
\"employee\": {
\"employee_id\": \"e_00_test\",
\"employee_name\": \"test employee\",
\"department_id\": \"d_00_test\"
},
\"department\": {
\"department_id\": \"d_00_test\",
\"department_name\": \"test department\"
}
}"
timestamp=2025-10-05T13:21:35.591Z level=INFO fiber=#0 message="get_one_employee_data start"
timestamp=2025-10-05T13:21:35.593Z level=INFO fiber=#0 message="employee_data from cache"
timestamp=2025-10-05T13:21:35.597Z level=INFO fiber=#0 message="employee_cache lookup start"
timestamp=2025-10-05T13:21:35.598Z level=INFO fiber=#0 message="database_employee_mock_ng_database_error_with_counter start"
timestamp=2025-10-05T13:21:35.599Z level=INFO fiber=#0 message="testCounter-get : 2"
timestamp=2025-10-05T13:21:35.607Z level=ERROR fiber=#0 message="DatabaseErrorが発生しました : employee"