0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

effect-tsでlayerで実行回数で返却値を変更する

Posted at

モックで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"
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?