@dojocho/effect-ts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/DOJO.md +22 -0
  2. package/dojo.json +50 -0
  3. package/katas/001-hello-effect/SENSEI.md +72 -0
  4. package/katas/001-hello-effect/solution.test.ts +35 -0
  5. package/katas/001-hello-effect/solution.ts +16 -0
  6. package/katas/002-transform-with-map/SENSEI.md +72 -0
  7. package/katas/002-transform-with-map/solution.test.ts +33 -0
  8. package/katas/002-transform-with-map/solution.ts +16 -0
  9. package/katas/003-generator-pipelines/SENSEI.md +72 -0
  10. package/katas/003-generator-pipelines/solution.test.ts +40 -0
  11. package/katas/003-generator-pipelines/solution.ts +29 -0
  12. package/katas/004-flatmap-and-chaining/SENSEI.md +80 -0
  13. package/katas/004-flatmap-and-chaining/solution.test.ts +34 -0
  14. package/katas/004-flatmap-and-chaining/solution.ts +18 -0
  15. package/katas/005-pipe-composition/SENSEI.md +81 -0
  16. package/katas/005-pipe-composition/solution.test.ts +41 -0
  17. package/katas/005-pipe-composition/solution.ts +19 -0
  18. package/katas/006-handle-errors/SENSEI.md +86 -0
  19. package/katas/006-handle-errors/solution.test.ts +53 -0
  20. package/katas/006-handle-errors/solution.ts +30 -0
  21. package/katas/007-tagged-errors/SENSEI.md +79 -0
  22. package/katas/007-tagged-errors/solution.test.ts +82 -0
  23. package/katas/007-tagged-errors/solution.ts +37 -0
  24. package/katas/008-error-patterns/SENSEI.md +89 -0
  25. package/katas/008-error-patterns/solution.test.ts +41 -0
  26. package/katas/008-error-patterns/solution.ts +38 -0
  27. package/katas/009-option-type/SENSEI.md +96 -0
  28. package/katas/009-option-type/solution.test.ts +49 -0
  29. package/katas/009-option-type/solution.ts +26 -0
  30. package/katas/010-either-and-exit/SENSEI.md +86 -0
  31. package/katas/010-either-and-exit/solution.test.ts +33 -0
  32. package/katas/010-either-and-exit/solution.ts +17 -0
  33. package/katas/011-services-and-context/SENSEI.md +82 -0
  34. package/katas/011-services-and-context/solution.test.ts +23 -0
  35. package/katas/011-services-and-context/solution.ts +17 -0
  36. package/katas/012-layers/SENSEI.md +73 -0
  37. package/katas/012-layers/solution.test.ts +23 -0
  38. package/katas/012-layers/solution.ts +26 -0
  39. package/katas/013-testing-effects/SENSEI.md +88 -0
  40. package/katas/013-testing-effects/solution.test.ts +41 -0
  41. package/katas/013-testing-effects/solution.ts +20 -0
  42. package/katas/014-schema-basics/SENSEI.md +81 -0
  43. package/katas/014-schema-basics/solution.test.ts +35 -0
  44. package/katas/014-schema-basics/solution.ts +25 -0
  45. package/katas/015-domain-modeling/SENSEI.md +85 -0
  46. package/katas/015-domain-modeling/solution.test.ts +46 -0
  47. package/katas/015-domain-modeling/solution.ts +42 -0
  48. package/katas/016-retry-and-schedule/SENSEI.md +72 -0
  49. package/katas/016-retry-and-schedule/solution.test.ts +26 -0
  50. package/katas/016-retry-and-schedule/solution.ts +23 -0
  51. package/katas/017-parallel-effects/SENSEI.md +70 -0
  52. package/katas/017-parallel-effects/solution.test.ts +33 -0
  53. package/katas/017-parallel-effects/solution.ts +17 -0
  54. package/katas/018-race-and-timeout/SENSEI.md +75 -0
  55. package/katas/018-race-and-timeout/solution.test.ts +30 -0
  56. package/katas/018-race-and-timeout/solution.ts +27 -0
  57. package/katas/019-ref-and-state/SENSEI.md +72 -0
  58. package/katas/019-ref-and-state/solution.test.ts +29 -0
  59. package/katas/019-ref-and-state/solution.ts +16 -0
  60. package/katas/020-fibers/SENSEI.md +80 -0
  61. package/katas/020-fibers/solution.test.ts +23 -0
  62. package/katas/020-fibers/solution.ts +23 -0
  63. package/katas/021-acquire-release/SENSEI.md +57 -0
  64. package/katas/021-acquire-release/solution.test.ts +23 -0
  65. package/katas/021-acquire-release/solution.ts +22 -0
  66. package/katas/022-scoped-layers/SENSEI.md +52 -0
  67. package/katas/022-scoped-layers/solution.test.ts +35 -0
  68. package/katas/022-scoped-layers/solution.ts +19 -0
  69. package/katas/023-resource-patterns/SENSEI.md +52 -0
  70. package/katas/023-resource-patterns/solution.test.ts +20 -0
  71. package/katas/023-resource-patterns/solution.ts +13 -0
  72. package/katas/024-streams-basics/SENSEI.md +61 -0
  73. package/katas/024-streams-basics/solution.test.ts +30 -0
  74. package/katas/024-streams-basics/solution.ts +16 -0
  75. package/katas/025-stream-operations/SENSEI.md +59 -0
  76. package/katas/025-stream-operations/solution.test.ts +26 -0
  77. package/katas/025-stream-operations/solution.ts +17 -0
  78. package/katas/026-combining-streams/SENSEI.md +54 -0
  79. package/katas/026-combining-streams/solution.test.ts +20 -0
  80. package/katas/026-combining-streams/solution.ts +16 -0
  81. package/katas/027-data-pipelines/SENSEI.md +58 -0
  82. package/katas/027-data-pipelines/solution.test.ts +22 -0
  83. package/katas/027-data-pipelines/solution.ts +16 -0
  84. package/katas/028-logging-and-spans/SENSEI.md +58 -0
  85. package/katas/028-logging-and-spans/solution.test.ts +50 -0
  86. package/katas/028-logging-and-spans/solution.ts +20 -0
  87. package/katas/029-http-client/SENSEI.md +59 -0
  88. package/katas/029-http-client/solution.test.ts +49 -0
  89. package/katas/029-http-client/solution.ts +24 -0
  90. package/katas/030-capstone/SENSEI.md +63 -0
  91. package/katas/030-capstone/solution.test.ts +67 -0
  92. package/katas/030-capstone/solution.ts +55 -0
  93. package/katas/031-config-and-environment/SENSEI.md +77 -0
  94. package/katas/031-config-and-environment/solution.test.ts +38 -0
  95. package/katas/031-config-and-environment/solution.ts +11 -0
  96. package/katas/032-cause-and-defects/SENSEI.md +90 -0
  97. package/katas/032-cause-and-defects/solution.test.ts +50 -0
  98. package/katas/032-cause-and-defects/solution.ts +23 -0
  99. package/katas/033-pattern-matching/SENSEI.md +86 -0
  100. package/katas/033-pattern-matching/solution.test.ts +36 -0
  101. package/katas/033-pattern-matching/solution.ts +28 -0
  102. package/katas/034-deferred-and-coordination/SENSEI.md +85 -0
  103. package/katas/034-deferred-and-coordination/solution.test.ts +25 -0
  104. package/katas/034-deferred-and-coordination/solution.ts +24 -0
  105. package/katas/035-queue-and-backpressure/SENSEI.md +100 -0
  106. package/katas/035-queue-and-backpressure/solution.test.ts +25 -0
  107. package/katas/035-queue-and-backpressure/solution.ts +21 -0
  108. package/katas/036-schema-advanced/SENSEI.md +81 -0
  109. package/katas/036-schema-advanced/solution.test.ts +55 -0
  110. package/katas/036-schema-advanced/solution.ts +19 -0
  111. package/katas/037-cache-and-memoization/SENSEI.md +73 -0
  112. package/katas/037-cache-and-memoization/solution.test.ts +47 -0
  113. package/katas/037-cache-and-memoization/solution.ts +24 -0
  114. package/katas/038-metrics/SENSEI.md +91 -0
  115. package/katas/038-metrics/solution.test.ts +39 -0
  116. package/katas/038-metrics/solution.ts +23 -0
  117. package/katas/039-managed-runtime/SENSEI.md +75 -0
  118. package/katas/039-managed-runtime/solution.test.ts +29 -0
  119. package/katas/039-managed-runtime/solution.ts +19 -0
  120. package/katas/040-request-batching/SENSEI.md +87 -0
  121. package/katas/040-request-batching/solution.test.ts +56 -0
  122. package/katas/040-request-batching/solution.ts +32 -0
  123. package/package.json +22 -0
  124. package/skills/effect-patterns-building-apis/SKILL.md +2393 -0
  125. package/skills/effect-patterns-building-data-pipelines/SKILL.md +1876 -0
  126. package/skills/effect-patterns-concurrency/SKILL.md +2999 -0
  127. package/skills/effect-patterns-concurrency-getting-started/SKILL.md +351 -0
  128. package/skills/effect-patterns-core-concepts/SKILL.md +3199 -0
  129. package/skills/effect-patterns-domain-modeling/SKILL.md +1385 -0
  130. package/skills/effect-patterns-error-handling/SKILL.md +1212 -0
  131. package/skills/effect-patterns-error-handling-resilience/SKILL.md +179 -0
  132. package/skills/effect-patterns-error-management/SKILL.md +1668 -0
  133. package/skills/effect-patterns-getting-started/SKILL.md +237 -0
  134. package/skills/effect-patterns-making-http-requests/SKILL.md +1756 -0
  135. package/skills/effect-patterns-observability/SKILL.md +1586 -0
  136. package/skills/effect-patterns-platform/SKILL.md +1195 -0
  137. package/skills/effect-patterns-platform-getting-started/SKILL.md +179 -0
  138. package/skills/effect-patterns-project-setup--execution/SKILL.md +233 -0
  139. package/skills/effect-patterns-resource-management/SKILL.md +827 -0
  140. package/skills/effect-patterns-scheduling/SKILL.md +451 -0
  141. package/skills/effect-patterns-scheduling-periodic-tasks/SKILL.md +763 -0
  142. package/skills/effect-patterns-streams/SKILL.md +2052 -0
  143. package/skills/effect-patterns-streams-getting-started/SKILL.md +421 -0
  144. package/skills/effect-patterns-streams-sinks/SKILL.md +1181 -0
  145. package/skills/effect-patterns-testing/SKILL.md +1632 -0
  146. package/skills/effect-patterns-tooling-and-debugging/SKILL.md +1125 -0
  147. package/skills/effect-patterns-value-handling/SKILL.md +676 -0
  148. package/tsconfig.json +20 -0
  149. package/vitest.config.ts +3 -0
@@ -0,0 +1,1632 @@
1
+ ---
2
+ name: effect-patterns-testing
3
+ description: Effect-TS patterns for Testing. Use when working with testing in Effect-TS applications.
4
+ ---
5
+ # Effect-TS Patterns: Testing
6
+ This skill provides 10 curated Effect-TS patterns for testing.
7
+ Use this skill when working on tasks related to:
8
+ - testing
9
+ - Best practices in Effect-TS applications
10
+ - Real-world patterns and solutions
11
+
12
+ ---
13
+
14
+ ## 🟢 Beginner Patterns
15
+
16
+ ### Your First Effect Test
17
+
18
+ **Rule:** Use Effect.runPromise in tests to run and assert on Effect results.
19
+
20
+ **Good Example:**
21
+
22
+ ```typescript
23
+ import { describe, it, expect } from "vitest"
24
+ import { Effect } from "effect"
25
+
26
+ // ============================================
27
+ // Code to test
28
+ // ============================================
29
+
30
+ const add = (a: number, b: number): Effect.Effect<number> =>
31
+ Effect.succeed(a + b)
32
+
33
+ const divide = (a: number, b: number): Effect.Effect<number, Error> =>
34
+ b === 0
35
+ ? Effect.fail(new Error("Cannot divide by zero"))
36
+ : Effect.succeed(a / b)
37
+
38
+ const fetchUser = (id: string): Effect.Effect<{ id: string; name: string }> =>
39
+ Effect.succeed({ id, name: `User ${id}` })
40
+
41
+ // ============================================
42
+ // Tests
43
+ // ============================================
44
+
45
+ describe("Basic Effect Tests", () => {
46
+ it("should add two numbers", async () => {
47
+ const result = await Effect.runPromise(add(2, 3))
48
+ expect(result).toBe(5)
49
+ })
50
+
51
+ it("should divide numbers", async () => {
52
+ const result = await Effect.runPromise(divide(10, 2))
53
+ expect(result).toBe(5)
54
+ })
55
+
56
+ it("should fail on divide by zero", async () => {
57
+ await expect(Effect.runPromise(divide(10, 0))).rejects.toThrow(
58
+ "Cannot divide by zero"
59
+ )
60
+ })
61
+
62
+ it("should fetch a user", async () => {
63
+ const user = await Effect.runPromise(fetchUser("123"))
64
+
65
+ expect(user).toEqual({
66
+ id: "123",
67
+ name: "User 123",
68
+ })
69
+ })
70
+ })
71
+
72
+ // ============================================
73
+ // Testing Effect.gen programs
74
+ // ============================================
75
+
76
+ const calculateDiscount = (price: number, quantity: number) =>
77
+ Effect.gen(function* () {
78
+ if (price <= 0) {
79
+ return yield* Effect.fail(new Error("Invalid price"))
80
+ }
81
+
82
+ const subtotal = price * quantity
83
+ const discount = quantity >= 10 ? 0.1 : 0
84
+ const total = subtotal * (1 - discount)
85
+
86
+ return { subtotal, discount, total }
87
+ })
88
+
89
+ describe("Effect.gen Tests", () => {
90
+ it("should calculate without discount", async () => {
91
+ const result = await Effect.runPromise(calculateDiscount(10, 5))
92
+
93
+ expect(result.subtotal).toBe(50)
94
+ expect(result.discount).toBe(0)
95
+ expect(result.total).toBe(50)
96
+ })
97
+
98
+ it("should apply bulk discount", async () => {
99
+ const result = await Effect.runPromise(calculateDiscount(10, 10))
100
+
101
+ expect(result.subtotal).toBe(100)
102
+ expect(result.discount).toBe(0.1)
103
+ expect(result.total).toBe(90)
104
+ })
105
+
106
+ it("should fail for invalid price", async () => {
107
+ await expect(
108
+ Effect.runPromise(calculateDiscount(-5, 10))
109
+ ).rejects.toThrow("Invalid price")
110
+ })
111
+ })
112
+ ```
113
+
114
+ **Rationale:**
115
+
116
+ Test Effect programs by running them with `Effect.runPromise` and using standard test assertions on the results.
117
+
118
+ ---
119
+
120
+
121
+ Testing Effect code is straightforward:
122
+
123
+ 1. **Effects are values** - Build them in tests like any other value
124
+ 2. **Run to get results** - Use `Effect.runPromise` to execute
125
+ 3. **Assert normally** - Standard assertions work on the results
126
+
127
+ ---
128
+
129
+ ---
130
+
131
+ ### Test Effects with Services
132
+
133
+ **Rule:** Provide test implementations of services to make Effect programs testable.
134
+
135
+ **Good Example:**
136
+
137
+ ```typescript
138
+ import { describe, it, expect } from "vitest"
139
+ import { Effect, Context } from "effect"
140
+
141
+ // ============================================
142
+ // 1. Define a service
143
+ // ============================================
144
+
145
+ class UserRepository extends Context.Tag("UserRepository")<
146
+ UserRepository,
147
+ {
148
+ readonly findById: (id: string) => Effect.Effect<User | null>
149
+ readonly save: (user: User) => Effect.Effect<void>
150
+ }
151
+ >() {}
152
+
153
+ interface User {
154
+ id: string
155
+ name: string
156
+ email: string
157
+ }
158
+
159
+ // ============================================
160
+ // 2. Code that uses the service
161
+ // ============================================
162
+
163
+ const getUser = (id: string) =>
164
+ Effect.gen(function* () {
165
+ const repo = yield* UserRepository
166
+ const user = yield* repo.findById(id)
167
+
168
+ if (!user) {
169
+ return yield* Effect.fail(new Error(`User ${id} not found`))
170
+ }
171
+
172
+ return user
173
+ })
174
+
175
+ const createUser = (name: string, email: string) =>
176
+ Effect.gen(function* () {
177
+ const repo = yield* UserRepository
178
+
179
+ const user: User = {
180
+ id: crypto.randomUUID(),
181
+ name,
182
+ email,
183
+ }
184
+
185
+ yield* repo.save(user)
186
+ return user
187
+ })
188
+
189
+ // ============================================
190
+ // 3. Create a test implementation
191
+ // ============================================
192
+
193
+ const makeTestUserRepository = (initialUsers: User[] = []) => {
194
+ const users = new Map(initialUsers.map(u => [u.id, u]))
195
+
196
+ return UserRepository.of({
197
+ findById: (id) => Effect.succeed(users.get(id) ?? null),
198
+ save: (user) => Effect.sync(() => { users.set(user.id, user) }),
199
+ })
200
+ }
201
+
202
+ // ============================================
203
+ // 4. Write tests
204
+ // ============================================
205
+
206
+ describe("User Service Tests", () => {
207
+ it("should find an existing user", async () => {
208
+ const testUser: User = {
209
+ id: "123",
210
+ name: "Alice",
211
+ email: "alice@example.com",
212
+ }
213
+
214
+ const testRepo = makeTestUserRepository([testUser])
215
+
216
+ const result = await Effect.runPromise(
217
+ getUser("123").pipe(
218
+ Effect.provideService(UserRepository, testRepo)
219
+ )
220
+ )
221
+
222
+ expect(result).toEqual(testUser)
223
+ })
224
+
225
+ it("should fail when user not found", async () => {
226
+ const testRepo = makeTestUserRepository([])
227
+
228
+ await expect(
229
+ Effect.runPromise(
230
+ getUser("999").pipe(
231
+ Effect.provideService(UserRepository, testRepo)
232
+ )
233
+ )
234
+ ).rejects.toThrow("User 999 not found")
235
+ })
236
+
237
+ it("should create and save a user", async () => {
238
+ const savedUsers: User[] = []
239
+
240
+ const trackingRepo = UserRepository.of({
241
+ findById: () => Effect.succeed(null),
242
+ save: (user) => Effect.sync(() => { savedUsers.push(user) }),
243
+ })
244
+
245
+ const result = await Effect.runPromise(
246
+ createUser("Bob", "bob@example.com").pipe(
247
+ Effect.provideService(UserRepository, trackingRepo)
248
+ )
249
+ )
250
+
251
+ expect(result.name).toBe("Bob")
252
+ expect(result.email).toBe("bob@example.com")
253
+ expect(savedUsers).toHaveLength(1)
254
+ expect(savedUsers[0].name).toBe("Bob")
255
+ })
256
+ })
257
+ ```
258
+
259
+ **Rationale:**
260
+
261
+ When testing Effects that require services, provide test implementations using `Effect.provideService` or test layers.
262
+
263
+ ---
264
+
265
+
266
+ Effect's service pattern makes testing easy:
267
+
268
+ 1. **Declare dependencies** - Effects specify what they need
269
+ 2. **Inject test doubles** - Provide fake implementations for tests
270
+ 3. **No mocking libraries** - Just provide different service implementations
271
+ 4. **Type-safe** - Compiler ensures you provide all dependencies
272
+
273
+ ---
274
+
275
+ ---
276
+
277
+
278
+ ## 🟡 Intermediate Patterns
279
+
280
+ ### Accessing the Current Time with Clock
281
+
282
+ **Rule:** Use the Clock service to get the current time, enabling deterministic testing with TestClock.
283
+
284
+ **Good Example:**
285
+
286
+ This example shows a function that checks if a token is expired. Its logic depends on `Clock`, making it fully testable.
287
+
288
+ ```typescript
289
+ import { Effect, Clock, Duration } from "effect";
290
+
291
+ interface Token {
292
+ readonly value: string;
293
+ readonly expiresAt: number; // UTC milliseconds
294
+ }
295
+
296
+ // This function is pure and testable because it depends on Clock
297
+ const isTokenExpired = (
298
+ token: Token
299
+ ): Effect.Effect<boolean, never, Clock.Clock> =>
300
+ Clock.currentTimeMillis.pipe(
301
+ Effect.map((now) => now > token.expiresAt),
302
+ Effect.tap((expired) =>
303
+ Clock.currentTimeMillis.pipe(
304
+ Effect.flatMap((currentTime) =>
305
+ Effect.log(
306
+ `Token expired? ${expired} (current time: ${new Date(currentTime).toISOString()})`
307
+ )
308
+ )
309
+ )
310
+ )
311
+ );
312
+
313
+ // Create a test clock service that advances time
314
+ const makeTestClock = (timeMs: number): Clock.Clock => ({
315
+ currentTimeMillis: Effect.succeed(timeMs),
316
+ currentTimeNanos: Effect.succeed(BigInt(timeMs * 1_000_000)),
317
+ sleep: (duration: Duration.Duration) => Effect.succeed(void 0),
318
+ unsafeCurrentTimeMillis: () => timeMs,
319
+ unsafeCurrentTimeNanos: () => BigInt(timeMs * 1_000_000),
320
+ [Clock.ClockTypeId]: Clock.ClockTypeId,
321
+ });
322
+
323
+ // Create a token that expires in 1 second
324
+ const token = { value: "abc", expiresAt: Date.now() + 1000 };
325
+
326
+ // Check token expiry with different clocks
327
+ const program = Effect.gen(function* () {
328
+ // Check with current time
329
+ yield* Effect.log("Checking with current time...");
330
+ yield* isTokenExpired(token);
331
+
332
+ // Check with past time
333
+ yield* Effect.log("\nChecking with past time (1 minute ago)...");
334
+ const pastClock = makeTestClock(Date.now() - 60_000);
335
+ yield* isTokenExpired(token).pipe(
336
+ Effect.provideService(Clock.Clock, pastClock)
337
+ );
338
+
339
+ // Check with future time
340
+ yield* Effect.log("\nChecking with future time (1 hour ahead)...");
341
+ const futureClock = makeTestClock(Date.now() + 3600_000);
342
+ yield* isTokenExpired(token).pipe(
343
+ Effect.provideService(Clock.Clock, futureClock)
344
+ );
345
+ });
346
+
347
+ // Run the program with default clock
348
+ Effect.runPromise(
349
+ program.pipe(Effect.provideService(Clock.Clock, makeTestClock(Date.now())))
350
+ );
351
+ ```
352
+
353
+ ---
354
+
355
+ **Anti-Pattern:**
356
+
357
+ Directly calling `Date.now()` inside your business logic. This creates an impure function that cannot be tested reliably without manipulating the system clock, which is a bad practice.
358
+
359
+ ```typescript
360
+ import { Effect } from "effect";
361
+
362
+ interface Token {
363
+ readonly expiresAt: number;
364
+ }
365
+
366
+ // ❌ WRONG: This function's behavior changes every millisecond.
367
+ const isTokenExpiredUnsafely = (token: Token): Effect.Effect<boolean> =>
368
+ Effect.sync(() => Date.now() > token.expiresAt);
369
+
370
+ // Testing this function would require complex mocking of global APIs
371
+ // or would be non-deterministic.
372
+ ```
373
+
374
+ **Rationale:**
375
+
376
+ Whenever you need to get the current time within an `Effect`, do not call `Date.now()` directly. Instead, depend on the `Clock` service and use one of its methods, such as `Clock.currentTimeMillis`.
377
+
378
+ ---
379
+
380
+
381
+ Directly calling `Date.now()` makes your code impure and tightly coupled to the system clock. This makes testing difficult and unreliable, as the output of your function will change every time it's run.
382
+
383
+ The `Clock` service is Effect's solution to this problem. It's an abstraction for "the current time."
384
+
385
+ - In **production**, the default `Live` `Clock` implementation uses the real system time.
386
+ - In **tests**, you can provide the `TestClock` layer. This gives you a virtual clock that you can manually control, allowing you to set the time to a specific value or advance it by a specific duration.
387
+
388
+ This makes any time-dependent logic pure, deterministic, and easy to test with perfect precision.
389
+
390
+ ---
391
+
392
+ ---
393
+
394
+ ### Write Tests That Adapt to Application Code
395
+
396
+ **Rule:** Write tests that adapt to application code.
397
+
398
+ **Good Example:**
399
+
400
+ ```typescript
401
+ import { Effect } from "effect";
402
+
403
+ // Define our types
404
+ interface User {
405
+ id: number;
406
+ name: string;
407
+ }
408
+
409
+ class NotFoundError extends Error {
410
+ readonly _tag = "NotFoundError";
411
+ constructor(readonly id: number) {
412
+ super(`User ${id} not found`);
413
+ }
414
+ }
415
+
416
+ // Define database service interface
417
+ interface DatabaseServiceApi {
418
+ getUserById: (id: number) => Effect.Effect<User, NotFoundError>;
419
+ }
420
+
421
+ // Implement the service with mock data
422
+ class DatabaseService extends Effect.Service<DatabaseService>()(
423
+ "DatabaseService",
424
+ {
425
+ sync: () => ({
426
+ getUserById: (id: number) => {
427
+ // Simulate database lookup
428
+ if (id === 404) {
429
+ return Effect.fail(new NotFoundError(id));
430
+ }
431
+ return Effect.succeed({ id, name: `User ${id}` });
432
+ },
433
+ }),
434
+ }
435
+ ) {}
436
+
437
+ // Test service implementation for testing
438
+ class TestDatabaseService extends Effect.Service<TestDatabaseService>()(
439
+ "TestDatabaseService",
440
+ {
441
+ sync: () => ({
442
+ getUserById: (id: number) => {
443
+ // Test data with predictable responses
444
+ const testUsers = [
445
+ { id: 1, name: "Test User 1" },
446
+ { id: 2, name: "Test User 2" },
447
+ { id: 123, name: "User 123" },
448
+ ];
449
+
450
+ const user = testUsers.find((u) => u.id === id);
451
+ if (user) {
452
+ return Effect.succeed(user);
453
+ }
454
+ return Effect.fail(new NotFoundError(id));
455
+ },
456
+ }),
457
+ }
458
+ ) {}
459
+
460
+ // Business logic that uses the database service
461
+ const getUserWithFallback = (id: number) =>
462
+ Effect.gen(function* () {
463
+ const db = yield* DatabaseService;
464
+ return yield* Effect.gen(function* () {
465
+ const user = yield* db.getUserById(id);
466
+ return user;
467
+ }).pipe(
468
+ Effect.catchAll((error) =>
469
+ Effect.gen(function* () {
470
+ if (error instanceof NotFoundError) {
471
+ yield* Effect.logInfo(`User ${id} not found, using fallback`);
472
+ return { id, name: `Fallback User ${id}` };
473
+ }
474
+ return yield* Effect.fail(error);
475
+ })
476
+ )
477
+ );
478
+ });
479
+
480
+ // Create a program that demonstrates the service
481
+ const program = Effect.gen(function* () {
482
+ yield* Effect.logInfo(
483
+ "=== Writing Tests that Adapt to Application Code Demo ==="
484
+ );
485
+
486
+ const db = yield* DatabaseService;
487
+
488
+ // Example 1: Successful user lookup
489
+ yield* Effect.logInfo("\n1. Looking up existing user 123...");
490
+ const user = yield* Effect.gen(function* () {
491
+ try {
492
+ return yield* db.getUserById(123);
493
+ } catch (error) {
494
+ yield* Effect.logError(
495
+ `Failed to get user: ${error instanceof Error ? error.message : "Unknown error"}`
496
+ );
497
+ return { id: -1, name: "Error" };
498
+ }
499
+ });
500
+ yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);
501
+
502
+ // Example 2: Handle non-existent user with proper error handling
503
+ yield* Effect.logInfo("\n2. Looking up non-existent user 404...");
504
+ const notFoundUser = yield* Effect.gen(function* () {
505
+ try {
506
+ return yield* db.getUserById(404);
507
+ } catch (error) {
508
+ if (error instanceof NotFoundError) {
509
+ yield* Effect.logInfo(
510
+ `✅ Properly handled NotFoundError: ${error.message}`
511
+ );
512
+ return { id: 404, name: "Not Found" };
513
+ }
514
+ yield* Effect.logError(
515
+ `Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`
516
+ );
517
+ return { id: -1, name: "Error" };
518
+ }
519
+ });
520
+ yield* Effect.logInfo(`Result: ${JSON.stringify(notFoundUser)}`);
521
+
522
+ // Example 3: Business logic with fallback
523
+ yield* Effect.logInfo("\n3. Business logic with fallback for missing user:");
524
+ const userWithFallback = yield* getUserWithFallback(999);
525
+ yield* Effect.logInfo(
526
+ `User with fallback: ${JSON.stringify(userWithFallback)}`
527
+ );
528
+
529
+ // Example 4: Testing with different service implementation
530
+ yield* Effect.logInfo("\n4. Testing with test service implementation:");
531
+ yield* Effect.provide(
532
+ Effect.gen(function* () {
533
+ const testDb = yield* TestDatabaseService;
534
+
535
+ // Test existing user
536
+ const testUser1 = yield* Effect.gen(function* () {
537
+ try {
538
+ return yield* testDb.getUserById(1);
539
+ } catch (error) {
540
+ yield* Effect.logError(
541
+ `Test failed: ${error instanceof Error ? error.message : "Unknown error"}`
542
+ );
543
+ return { id: -1, name: "Test Error" };
544
+ }
545
+ });
546
+ yield* Effect.logInfo(`Test user 1: ${JSON.stringify(testUser1)}`);
547
+
548
+ // Test non-existing user
549
+ const testUser404 = yield* Effect.gen(function* () {
550
+ try {
551
+ return yield* testDb.getUserById(404);
552
+ } catch (error) {
553
+ yield* Effect.logInfo(
554
+ `✅ Test service properly threw NotFoundError: ${error instanceof Error ? error.message : "Unknown error"}`
555
+ );
556
+ return { id: 404, name: "Test Not Found" };
557
+ }
558
+ });
559
+ yield* Effect.logInfo(`Test result: ${JSON.stringify(testUser404)}`);
560
+ }),
561
+ TestDatabaseService.Default
562
+ );
563
+
564
+ yield* Effect.logInfo(
565
+ "\n✅ Tests that adapt to application code demonstration completed!"
566
+ );
567
+ yield* Effect.logInfo(
568
+ "The same business logic works with different service implementations!"
569
+ );
570
+ });
571
+
572
+ // Run the program with the default database service
573
+ Effect.runPromise(
574
+ Effect.provide(program, DatabaseService.Default) as Effect.Effect<
575
+ void,
576
+ never,
577
+ never
578
+ >
579
+ );
580
+ ```
581
+
582
+ **Explanation:**
583
+ Tests should reflect the real interface and behavior of your code, not force changes to it.
584
+
585
+ **Anti-Pattern:**
586
+
587
+ Any action where the test dictates a change to the application code. Do not modify a service file to add a method just because a test needs it. If a test fails, fix the test.
588
+
589
+ **Rationale:**
590
+
591
+ Tests are secondary artifacts that serve to validate the application. The application's code and interfaces are the source of truth. When a test fails, fix the test's logic or setup, not the production code.
592
+
593
+
594
+ Treating application code as immutable during testing prevents the introduction of bugs and false test confidence. The goal of a test is to verify real-world behavior; changing that behavior to suit the test invalidates its purpose.
595
+
596
+ ---
597
+
598
+ ### Use the Auto-Generated .Default Layer in Tests
599
+
600
+ **Rule:** Use the auto-generated .Default layer in tests.
601
+
602
+ **Good Example:**
603
+
604
+ ```typescript
605
+ import { Effect } from "effect";
606
+
607
+ // Define MyService using Effect.Service pattern
608
+ class MyService extends Effect.Service<MyService>()("MyService", {
609
+ sync: () => ({
610
+ doSomething: () =>
611
+ Effect.succeed("done").pipe(
612
+ Effect.tap(() => Effect.log("MyService did something!"))
613
+ ),
614
+ }),
615
+ }) {}
616
+
617
+ // Create a program that uses MyService
618
+ const program = Effect.gen(function* () {
619
+ yield* Effect.log("Getting MyService...");
620
+ const service = yield* MyService;
621
+
622
+ yield* Effect.log("Calling doSomething()...");
623
+ const result = yield* service.doSomething();
624
+
625
+ yield* Effect.log(`Result: ${result}`);
626
+ });
627
+
628
+ // Run the program with default service implementation
629
+ Effect.runPromise(Effect.provide(program, MyService.Default));
630
+ ```
631
+
632
+ **Explanation:**
633
+ This approach ensures your tests are idiomatic, maintainable, and take full advantage of Effect's dependency injection system.
634
+
635
+ **Anti-Pattern:**
636
+
637
+ Do not create manual layers for your service in tests (`Layer.succeed(...)`) or try to provide the service class directly. This bypasses the intended dependency injection mechanism.
638
+
639
+ **Rationale:**
640
+
641
+ In your tests, provide service dependencies using the static `.Default` property that `Effect.Service` automatically attaches to your service class.
642
+
643
+
644
+ The `.Default` layer is the canonical way to provide a service in a test environment. It's automatically created, correctly scoped, and handles resolving any transitive dependencies, making tests cleaner and more robust.
645
+
646
+ ---
647
+
648
+ ### Mocking Dependencies in Tests
649
+
650
+ **Rule:** Provide mock service implementations via a test-specific Layer to isolate the unit under test.
651
+
652
+ **Good Example:**
653
+
654
+ We want to test a `Notifier` service that uses an `EmailClient` to send emails. In our test, we provide a mock `EmailClient` that doesn't actually send emails but just returns a success value.
655
+
656
+ ```typescript
657
+ import { Effect, Layer } from "effect";
658
+
659
+ // --- The Services ---
660
+ interface EmailClientService {
661
+ send: (address: string, body: string) => Effect.Effect<void>;
662
+ }
663
+
664
+ class EmailClient extends Effect.Service<EmailClientService>()("EmailClient", {
665
+ sync: () => ({
666
+ send: (address: string, body: string) =>
667
+ Effect.sync(() => Effect.log(`Sending email to ${address}: ${body}`)),
668
+ }),
669
+ }) {}
670
+
671
+ interface NotifierService {
672
+ notifyUser: (userId: number, message: string) => Effect.Effect<void>;
673
+ }
674
+
675
+ class Notifier extends Effect.Service<NotifierService>()("Notifier", {
676
+ effect: Effect.gen(function* () {
677
+ const emailClient = yield* EmailClient;
678
+ return {
679
+ notifyUser: (userId: number, message: string) =>
680
+ emailClient.send(`user-${userId}@example.com`, message),
681
+ };
682
+ }),
683
+ dependencies: [EmailClient.Default],
684
+ }) {}
685
+
686
+ // Create a program that uses the Notifier service
687
+ const program = Effect.gen(function* () {
688
+ yield* Effect.log("Using default EmailClient implementation...");
689
+ const notifier = yield* Notifier;
690
+ yield* notifier.notifyUser(123, "Your invoice is ready.");
691
+
692
+ // Create mock EmailClient that logs differently
693
+ yield* Effect.log("\nUsing mock EmailClient implementation...");
694
+ const mockEmailClient = Layer.succeed(EmailClient, {
695
+ send: (address: string, body: string) =>
696
+ // Directly return the Effect.log without nesting it in Effect.sync
697
+ Effect.log(`MOCK: Would send to ${address} with body: ${body}`),
698
+ } as EmailClientService);
699
+
700
+ // Run the same notification with mock client
701
+ yield* Effect.gen(function* () {
702
+ const notifier = yield* Notifier;
703
+ yield* notifier.notifyUser(123, "Your invoice is ready.");
704
+ }).pipe(Effect.provide(mockEmailClient));
705
+ });
706
+
707
+ // Run the program
708
+ Effect.runPromise(Effect.provide(program, Notifier.Default));
709
+ ```
710
+
711
+ ---
712
+
713
+ **Anti-Pattern:**
714
+
715
+ Testing your business logic using the "live" implementation of its dependencies. This creates an integration test, not a unit test. It will be slow, unreliable, and may have real-world side effects (like actually sending an email).
716
+
717
+ ```typescript
718
+ import { Effect } from "effect";
719
+ import { NotifierLive } from "./somewhere";
720
+ import { EmailClientLive } from "./somewhere"; // The REAL email client
721
+
722
+ // ❌ WRONG: This test will try to send a real email.
723
+ it("sends a real email", () =>
724
+ Effect.gen(function* () {
725
+ const notifier = yield* Notifier;
726
+ yield* notifier.notifyUser(123, "This is a test email!");
727
+ }).pipe(
728
+ Effect.provide(NotifierLive),
729
+ Effect.provide(EmailClientLive), // Using the live layer makes this an integration test
730
+ Effect.runPromise
731
+ ));
732
+ ```
733
+
734
+ **Rationale:**
735
+
736
+ To test a piece of code in isolation, identify its service dependencies and provide mock implementations for them using a test-specific `Layer`. The most common way to create a mock layer is with `Layer.succeed(ServiceTag, mockImplementation)`.
737
+
738
+ ---
739
+
740
+
741
+ The primary goal of a unit test is to verify the logic of a single unit of code, independent of its external dependencies. Effect's dependency injection system is designed to make this easy and type-safe.
742
+
743
+ By providing a mock `Layer` in your test, you replace a real dependency (like an `HttpClient` that makes network calls) with a fake one that returns predictable data. This provides several key benefits:
744
+
745
+ - **Determinism:** Your tests always produce the same result, free from the flakiness of network or database connections.
746
+ - **Speed:** Tests run instantly without waiting for slow I/O operations.
747
+ - **Type Safety:** The TypeScript compiler ensures your mock implementation perfectly matches the real service's interface, preventing your tests from becoming outdated.
748
+ - **Explicitness:** The test setup clearly documents all the dependencies required for the code to run.
749
+
750
+ ---
751
+
752
+ ---
753
+
754
+
755
+ ## 🟠 Advanced Patterns
756
+
757
+ ### Organize Layers into Composable Modules
758
+
759
+ **Rule:** Organize services into modular Layers that are composed hierarchically to manage complexity in large applications.
760
+
761
+ **Good Example:**
762
+
763
+ This example shows a `BaseLayer` with a `Logger`, a `UserModule` that uses the `Logger`, and a final `AppLayer` that wires them together.
764
+
765
+ ### 1. The Base Infrastructure Layer
766
+
767
+ ```typescript
768
+ // src/core/Logger.ts
769
+ import { Effect } from "effect";
770
+
771
+ export class Logger extends Effect.Service<Logger>()("App/Core/Logger", {
772
+ sync: () => ({
773
+ log: (msg: string) => Effect.log(`[LOG] ${msg}`),
774
+ }),
775
+ }) {}
776
+
777
+ // src/features/User/UserRepository.ts
778
+ export class UserRepository extends Effect.Service<UserRepository>()(
779
+ "App/User/UserRepository",
780
+ {
781
+ // Define implementation that uses Logger
782
+ effect: Effect.gen(function* () {
783
+ const logger = yield* Logger;
784
+ return {
785
+ findById: (id: number) =>
786
+ Effect.gen(function* () {
787
+ yield* logger.log(`Finding user ${id}`);
788
+ return { id, name: `User ${id}` };
789
+ }),
790
+ };
791
+ }),
792
+ // Declare Logger dependency
793
+ dependencies: [Logger.Default],
794
+ }
795
+ ) {}
796
+
797
+ // Example usage
798
+ const program = Effect.gen(function* () {
799
+ const repo = yield* UserRepository;
800
+ const user = yield* repo.findById(1);
801
+ return user;
802
+ });
803
+
804
+ // Run with default implementations
805
+ Effect.runPromise(Effect.provide(program, UserRepository.Default));
806
+
807
+ const programWithLogging = Effect.gen(function* () {
808
+ const result = yield* program;
809
+ yield* Effect.log(`Program result: ${JSON.stringify(result)}`);
810
+ return result;
811
+ });
812
+
813
+ Effect.runPromise(Effect.provide(programWithLogging, UserRepository.Default));
814
+ ```
815
+
816
+ ### 2. The Feature Module Layer
817
+
818
+ ```typescript
819
+ // src/core/Logger.ts
820
+ import { Effect } from "effect";
821
+
822
+ export class Logger extends Effect.Service<Logger>()("App/Core/Logger", {
823
+ sync: () => ({
824
+ log: (msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`)),
825
+ }),
826
+ }) {}
827
+
828
+ // src/features/User/UserRepository.ts
829
+ export class UserRepository extends Effect.Service<UserRepository>()(
830
+ "App/User/UserRepository",
831
+ {
832
+ // Define implementation that uses Logger
833
+ effect: Effect.gen(function* () {
834
+ const logger = yield* Logger;
835
+ return {
836
+ findById: (id: number) =>
837
+ Effect.gen(function* () {
838
+ yield* logger.log(`Finding user ${id}`);
839
+ return { id, name: `User ${id}` };
840
+ }),
841
+ };
842
+ }),
843
+ // Declare Logger dependency
844
+ dependencies: [Logger.Default],
845
+ }
846
+ ) {}
847
+
848
+ // Example usage
849
+ const program = Effect.gen(function* () {
850
+ const repo = yield* UserRepository;
851
+ const user = yield* repo.findById(1);
852
+ return user;
853
+ });
854
+
855
+ // Run with default implementations
856
+ Effect.runPromise(Effect.provide(program, UserRepository.Default)).then(
857
+ console.log
858
+ );
859
+ ```
860
+
861
+ ### 3. The Final Application Composition
862
+
863
+ ```typescript
864
+ // src/layers.ts
865
+ import { Layer } from "effect";
866
+ import { BaseLayer } from "./core";
867
+ import { UserModuleLive } from "./features/User";
868
+ // import { ProductModuleLive } from "./features/Product";
869
+
870
+ const AllModules = Layer.mergeAll(UserModuleLive /*, ProductModuleLive */);
871
+
872
+ // Provide the BaseLayer to all modules at once, creating a self-contained AppLayer.
873
+ export const AppLayer = Layer.provide(AllModules, BaseLayer);
874
+ ```
875
+
876
+ ---
877
+
878
+ **Anti-Pattern:**
879
+
880
+ A flat composition strategy for a large application. While simple at first, it quickly becomes difficult to manage.
881
+
882
+ ```typescript
883
+ // ❌ This file becomes huge and hard to navigate in a large project.
884
+ const AppLayer = Layer.mergeAll(
885
+ LoggerLive,
886
+ ConfigLive,
887
+ DatabaseLive,
888
+ TracerLive,
889
+ UserServiceLive,
890
+ UserRepositoryLive,
891
+ ProductServiceLive,
892
+ ProductRepositoryLive,
893
+ BillingServiceLive
894
+ // ...and 50 other services
895
+ );
896
+ ```
897
+
898
+ **Rationale:**
899
+
900
+ For large applications, avoid a single, flat list of services. Instead, structure your application by creating hierarchical layers:
901
+
902
+ 1. **`BaseLayer`**: Provides application-wide infrastructure (Logger, Config, Database).
903
+ 2. **`FeatureModule` Layers**: Provide the services for a specific business domain (e.g., `UserModule`, `ProductModule`). These depend on the `BaseLayer`.
904
+ 3. **`AppLayer`**: The top-level layer that composes the feature modules by providing them with the `BaseLayer`.
905
+
906
+ ---
907
+
908
+
909
+ As an application grows, a flat composition strategy where all services are merged into one giant layer becomes unwieldy and hard to reason about. The Composable Modules pattern solves this by introducing structure.
910
+
911
+ This approach creates a clean, scalable, and highly testable architecture where complexity is contained within each module. The top-level composition becomes a clear, high-level diagram of your application's architecture, and feature modules can be tested in isolation by providing them with a mocked `BaseLayer`.
912
+
913
+ ---
914
+
915
+ ---
916
+
917
+ ### Test Streaming Effects
918
+
919
+ **Rule:** Use Stream.runCollect and assertions to verify stream behavior.
920
+
921
+ **Good Example:**
922
+
923
+ ```typescript
924
+ import { describe, it, expect } from "vitest"
925
+ import { Effect, Stream, Chunk, Ref } from "effect"
926
+
927
+ describe("Stream Testing", () => {
928
+ // ============================================
929
+ // 1. Test basic stream operations
930
+ // ============================================
931
+
932
+ it("should transform stream elements", async () => {
933
+ const result = await Effect.runPromise(
934
+ Stream.fromIterable([1, 2, 3, 4, 5]).pipe(
935
+ Stream.map((n) => n * 2),
936
+ Stream.runCollect
937
+ )
938
+ )
939
+
940
+ expect(Chunk.toReadonlyArray(result)).toEqual([2, 4, 6, 8, 10])
941
+ })
942
+
943
+ it("should filter stream elements", async () => {
944
+ const result = await Effect.runPromise(
945
+ Stream.fromIterable([1, 2, 3, 4, 5, 6]).pipe(
946
+ Stream.filter((n) => n % 2 === 0),
947
+ Stream.runCollect
948
+ )
949
+ )
950
+
951
+ expect(Chunk.toReadonlyArray(result)).toEqual([2, 4, 6])
952
+ })
953
+
954
+ // ============================================
955
+ // 2. Test stream aggregation
956
+ // ============================================
957
+
958
+ it("should fold stream to single value", async () => {
959
+ const result = await Effect.runPromise(
960
+ Stream.fromIterable([1, 2, 3, 4, 5]).pipe(
961
+ Stream.runFold(0, (acc, n) => acc + n)
962
+ )
963
+ )
964
+
965
+ expect(result).toBe(15)
966
+ })
967
+
968
+ it("should count stream elements", async () => {
969
+ const count = await Effect.runPromise(
970
+ Stream.fromIterable(["a", "b", "c", "d"]).pipe(
971
+ Stream.runCount
972
+ )
973
+ )
974
+
975
+ expect(count).toBe(4)
976
+ })
977
+
978
+ // ============================================
979
+ // 3. Test error handling in streams
980
+ // ============================================
981
+
982
+ it("should catch errors in stream", async () => {
983
+ const result = await Effect.runPromise(
984
+ Stream.fromIterable([1, 2, 3]).pipe(
985
+ Stream.mapEffect((n) =>
986
+ n === 2
987
+ ? Effect.fail(new Error("Failed on 2"))
988
+ : Effect.succeed(n * 10)
989
+ ),
990
+ Stream.catchAll((error) =>
991
+ Stream.succeed(-1) // Replace error with sentinel
992
+ ),
993
+ Stream.runCollect
994
+ )
995
+ )
996
+
997
+ expect(Chunk.toReadonlyArray(result)).toEqual([10, -1])
998
+ })
999
+
1000
+ it("should handle errors and continue with orElse", async () => {
1001
+ const failingStream = Stream.fail(new Error("Primary failed"))
1002
+ const fallbackStream = Stream.fromIterable([1, 2, 3])
1003
+
1004
+ const result = await Effect.runPromise(
1005
+ failingStream.pipe(
1006
+ Stream.orElse(() => fallbackStream),
1007
+ Stream.runCollect
1008
+ )
1009
+ )
1010
+
1011
+ expect(Chunk.toReadonlyArray(result)).toEqual([1, 2, 3])
1012
+ })
1013
+
1014
+ // ============================================
1015
+ // 4. Test stream chunking
1016
+ // ============================================
1017
+
1018
+ it("should chunk stream elements", async () => {
1019
+ const result = await Effect.runPromise(
1020
+ Stream.fromIterable([1, 2, 3, 4, 5]).pipe(
1021
+ Stream.grouped(2),
1022
+ Stream.runCollect
1023
+ )
1024
+ )
1025
+
1026
+ const chunks = Chunk.toReadonlyArray(result).map(Chunk.toReadonlyArray)
1027
+ expect(chunks).toEqual([[1, 2], [3, 4], [5]])
1028
+ })
1029
+
1030
+ // ============================================
1031
+ // 5. Test stream with effects
1032
+ // ============================================
1033
+
1034
+ it("should run effects for each element", async () => {
1035
+ const processed: number[] = []
1036
+
1037
+ await Effect.runPromise(
1038
+ Stream.fromIterable([1, 2, 3]).pipe(
1039
+ Stream.tap((n) =>
1040
+ Effect.sync(() => {
1041
+ processed.push(n)
1042
+ })
1043
+ ),
1044
+ Stream.runDrain
1045
+ )
1046
+ )
1047
+
1048
+ expect(processed).toEqual([1, 2, 3])
1049
+ })
1050
+
1051
+ // ============================================
1052
+ // 6. Test stream resource management
1053
+ // ============================================
1054
+
1055
+ it("should release resources on completion", async () => {
1056
+ const acquired: string[] = []
1057
+ const released: string[] = []
1058
+
1059
+ const managedStream = Stream.acquireRelease(
1060
+ Effect.gen(function* () {
1061
+ acquired.push("resource")
1062
+ return "resource"
1063
+ }),
1064
+ () =>
1065
+ Effect.sync(() => {
1066
+ released.push("resource")
1067
+ })
1068
+ ).pipe(
1069
+ Stream.flatMap(() => Stream.fromIterable([1, 2, 3]))
1070
+ )
1071
+
1072
+ await Effect.runPromise(Stream.runDrain(managedStream))
1073
+
1074
+ expect(acquired).toEqual(["resource"])
1075
+ expect(released).toEqual(["resource"])
1076
+ })
1077
+
1078
+ it("should release resources on error", async () => {
1079
+ const released: string[] = []
1080
+
1081
+ const managedStream = Stream.acquireRelease(
1082
+ Effect.succeed("resource"),
1083
+ () => Effect.sync(() => { released.push("released") })
1084
+ ).pipe(
1085
+ Stream.flatMap(() =>
1086
+ Stream.fromEffect(Effect.fail(new Error("Oops")))
1087
+ )
1088
+ )
1089
+
1090
+ await Effect.runPromise(
1091
+ Stream.runDrain(managedStream).pipe(
1092
+ Effect.catchAll(() => Effect.void)
1093
+ )
1094
+ )
1095
+
1096
+ expect(released).toEqual(["released"])
1097
+ })
1098
+
1099
+ // ============================================
1100
+ // 7. Test stream timing with take/drop
1101
+ // ============================================
1102
+
1103
+ it("should take first N elements", async () => {
1104
+ const result = await Effect.runPromise(
1105
+ Stream.fromIterable([1, 2, 3, 4, 5]).pipe(
1106
+ Stream.take(3),
1107
+ Stream.runCollect
1108
+ )
1109
+ )
1110
+
1111
+ expect(Chunk.toReadonlyArray(result)).toEqual([1, 2, 3])
1112
+ })
1113
+
1114
+ it("should drop first N elements", async () => {
1115
+ const result = await Effect.runPromise(
1116
+ Stream.fromIterable([1, 2, 3, 4, 5]).pipe(
1117
+ Stream.drop(2),
1118
+ Stream.runCollect
1119
+ )
1120
+ )
1121
+
1122
+ expect(Chunk.toReadonlyArray(result)).toEqual([3, 4, 5])
1123
+ })
1124
+
1125
+ // ============================================
1126
+ // 8. Test stream merging
1127
+ // ============================================
1128
+
1129
+ it("should merge streams", async () => {
1130
+ const stream1 = Stream.fromIterable([1, 3, 5])
1131
+ const stream2 = Stream.fromIterable([2, 4, 6])
1132
+
1133
+ const result = await Effect.runPromise(
1134
+ Stream.merge(stream1, stream2).pipe(
1135
+ Stream.runCollect
1136
+ )
1137
+ )
1138
+
1139
+ const array = Chunk.toReadonlyArray(result)
1140
+ expect(array).toHaveLength(6)
1141
+ expect(array).toContain(1)
1142
+ expect(array).toContain(6)
1143
+ })
1144
+ })
1145
+ ```
1146
+
1147
+ **Rationale:**
1148
+
1149
+ Test streams by collecting results and verifying transformations, error handling, and resource management.
1150
+
1151
+ ---
1152
+
1153
+
1154
+ Stream tests verify:
1155
+
1156
+ 1. **Transformations** - map, filter, flatMap work correctly
1157
+ 2. **Error handling** - Failures are caught and handled
1158
+ 3. **Resource safety** - Resources are released
1159
+ 4. **Backpressure** - Data flow is controlled
1160
+
1161
+ ---
1162
+
1163
+ ---
1164
+
1165
+ ### Test Concurrent Code
1166
+
1167
+ **Rule:** Use TestClock and controlled concurrency to make concurrent tests deterministic.
1168
+
1169
+ **Good Example:**
1170
+
1171
+ ```typescript
1172
+ import { describe, it, expect } from "vitest"
1173
+ import { Effect, Fiber, Ref, TestClock, Duration, Deferred } from "effect"
1174
+
1175
+ describe("Concurrent Code Testing", () => {
1176
+ // ============================================
1177
+ // 1. Test parallel execution
1178
+ // ============================================
1179
+
1180
+ it("should run effects in parallel", async () => {
1181
+ const executionOrder: string[] = []
1182
+
1183
+ const task1 = Effect.gen(function* () {
1184
+ yield* Effect.sleep("100 millis")
1185
+ executionOrder.push("task1")
1186
+ return 1
1187
+ })
1188
+
1189
+ const task2 = Effect.gen(function* () {
1190
+ yield* Effect.sleep("50 millis")
1191
+ executionOrder.push("task2")
1192
+ return 2
1193
+ })
1194
+
1195
+ const program = Effect.all([task1, task2], { concurrency: 2 })
1196
+
1197
+ // Use TestClock to control time
1198
+ const result = await Effect.runPromise(
1199
+ Effect.gen(function* () {
1200
+ const fiber = yield* Effect.fork(program)
1201
+
1202
+ // Advance time to trigger both tasks
1203
+ yield* TestClock.adjust("100 millis")
1204
+
1205
+ return yield* Fiber.join(fiber)
1206
+ }).pipe(Effect.provide(TestClock.live))
1207
+ )
1208
+
1209
+ expect(result).toEqual([1, 2])
1210
+ // With real time, task2 would complete first
1211
+ expect(executionOrder).toContain("task1")
1212
+ expect(executionOrder).toContain("task2")
1213
+ })
1214
+
1215
+ // ============================================
1216
+ // 2. Test race conditions
1217
+ // ============================================
1218
+
1219
+ it("should handle race condition correctly", async () => {
1220
+ const counter = await Effect.runPromise(
1221
+ Effect.gen(function* () {
1222
+ const ref = yield* Ref.make(0)
1223
+
1224
+ // Simulate concurrent increments
1225
+ const increment = Ref.update(ref, (n) => n + 1)
1226
+
1227
+ // Run 100 concurrent increments
1228
+ yield* Effect.all(
1229
+ Array.from({ length: 100 }, () => increment),
1230
+ { concurrency: "unbounded" }
1231
+ )
1232
+
1233
+ return yield* Ref.get(ref)
1234
+ })
1235
+ )
1236
+
1237
+ // Ref is atomic, so all increments should be counted
1238
+ expect(counter).toBe(100)
1239
+ })
1240
+
1241
+ // ============================================
1242
+ // 3. Test with controlled fiber execution
1243
+ // ============================================
1244
+
1245
+ it("should test fiber lifecycle", async () => {
1246
+ const events: string[] = []
1247
+
1248
+ const program = Effect.gen(function* () {
1249
+ const fiber = yield* Effect.fork(
1250
+ Effect.gen(function* () {
1251
+ events.push("started")
1252
+ yield* Effect.sleep("1 second")
1253
+ events.push("completed")
1254
+ return "result"
1255
+ })
1256
+ )
1257
+
1258
+ events.push("forked")
1259
+
1260
+ // Interrupt the fiber
1261
+ yield* Fiber.interrupt(fiber)
1262
+ events.push("interrupted")
1263
+
1264
+ const exit = yield* Fiber.await(fiber)
1265
+ return exit
1266
+ })
1267
+
1268
+ await Effect.runPromise(program)
1269
+
1270
+ expect(events).toEqual(["forked", "started", "interrupted"])
1271
+ expect(events).not.toContain("completed")
1272
+ })
1273
+
1274
+ // ============================================
1275
+ // 4. Test timeout behavior
1276
+ // ============================================
1277
+
1278
+ it("should timeout slow operations", async () => {
1279
+ const slowOperation = Effect.gen(function* () {
1280
+ yield* Effect.sleep("10 seconds")
1281
+ return "completed"
1282
+ })
1283
+
1284
+ const result = await Effect.runPromise(
1285
+ Effect.gen(function* () {
1286
+ const fiber = yield* Effect.fork(
1287
+ slowOperation.pipe(Effect.timeout("1 second"))
1288
+ )
1289
+
1290
+ // Advance past the timeout
1291
+ yield* TestClock.adjust("2 seconds")
1292
+
1293
+ return yield* Fiber.join(fiber)
1294
+ }).pipe(Effect.provide(TestClock.live))
1295
+ )
1296
+
1297
+ // Result is Option.None due to timeout
1298
+ expect(result._tag).toBe("None")
1299
+ })
1300
+
1301
+ // ============================================
1302
+ // 5. Test with Deferred for synchronization
1303
+ // ============================================
1304
+
1305
+ it("should synchronize fibers correctly", async () => {
1306
+ const result = await Effect.runPromise(
1307
+ Effect.gen(function* () {
1308
+ const deferred = yield* Deferred.make<string>()
1309
+ const results: string[] = []
1310
+
1311
+ // Consumer waits for producer
1312
+ const consumer = Effect.fork(
1313
+ Effect.gen(function* () {
1314
+ const value = yield* Deferred.await(deferred)
1315
+ results.push(`consumed: ${value}`)
1316
+ })
1317
+ )
1318
+
1319
+ // Producer completes the deferred
1320
+ const producer = Effect.gen(function* () {
1321
+ results.push("producing")
1322
+ yield* Deferred.succeed(deferred, "data")
1323
+ results.push("produced")
1324
+ })
1325
+
1326
+ yield* consumer
1327
+ yield* producer
1328
+
1329
+ // Wait for consumer to process
1330
+ yield* Effect.sleep("10 millis")
1331
+
1332
+ return results
1333
+ })
1334
+ )
1335
+
1336
+ expect(result).toContain("producing")
1337
+ expect(result).toContain("produced")
1338
+ expect(result).toContain("consumed: data")
1339
+ })
1340
+
1341
+ // ============================================
1342
+ // 6. Test for absence of deadlocks
1343
+ // ============================================
1344
+
1345
+ it("should not deadlock with proper resource ordering", async () => {
1346
+ const result = await Effect.runPromise(
1347
+ Effect.gen(function* () {
1348
+ const ref1 = yield* Ref.make(0)
1349
+ const ref2 = yield* Ref.make(0)
1350
+
1351
+ // Two fibers accessing refs in same order (no deadlock)
1352
+ const fiber1 = yield* Effect.fork(
1353
+ Effect.gen(function* () {
1354
+ yield* Ref.update(ref1, (n) => n + 1)
1355
+ yield* Ref.update(ref2, (n) => n + 1)
1356
+ })
1357
+ )
1358
+
1359
+ const fiber2 = yield* Effect.fork(
1360
+ Effect.gen(function* () {
1361
+ yield* Ref.update(ref1, (n) => n + 1)
1362
+ yield* Ref.update(ref2, (n) => n + 1)
1363
+ })
1364
+ )
1365
+
1366
+ yield* Fiber.join(fiber1)
1367
+ yield* Fiber.join(fiber2)
1368
+
1369
+ return [yield* Ref.get(ref1), yield* Ref.get(ref2)]
1370
+ }).pipe(Effect.timeout("1 second"))
1371
+ )
1372
+
1373
+ expect(result._tag).toBe("Some")
1374
+ expect(result.value).toEqual([2, 2])
1375
+ })
1376
+ })
1377
+ ```
1378
+
1379
+ **Rationale:**
1380
+
1381
+ Use Effect's TestClock and fiber control to make concurrent tests deterministic and repeatable.
1382
+
1383
+ ---
1384
+
1385
+
1386
+ Concurrent code is hard to test:
1387
+
1388
+ 1. **Non-determinism** - Different runs, different results
1389
+ 2. **Race conditions** - Timing-dependent bugs
1390
+ 3. **Deadlocks** - Hard to reproduce
1391
+ 4. **Flaky tests** - Pass sometimes, fail others
1392
+
1393
+ Effect's test utilities provide control over timing and concurrency.
1394
+
1395
+ ---
1396
+
1397
+ ---
1398
+
1399
+ ### Property-Based Testing with Effect
1400
+
1401
+ **Rule:** Use property-based testing to find edge cases your example-based tests miss.
1402
+
1403
+ **Good Example:**
1404
+
1405
+ ```typescript
1406
+ import { describe, it, expect } from "vitest"
1407
+ import { Effect, Option, Either, Schema } from "effect"
1408
+ import * as fc from "fast-check"
1409
+
1410
+ describe("Property-Based Testing with Effect", () => {
1411
+ // ============================================
1412
+ // 1. Test pure function properties
1413
+ // ============================================
1414
+
1415
+ it("should satisfy array reverse properties", () => {
1416
+ fc.assert(
1417
+ fc.property(fc.array(fc.integer()), (arr) => {
1418
+ // Reversing twice returns original
1419
+ const reversed = arr.slice().reverse()
1420
+ const doubleReversed = reversed.slice().reverse()
1421
+
1422
+ return JSON.stringify(arr) === JSON.stringify(doubleReversed)
1423
+ })
1424
+ )
1425
+ })
1426
+
1427
+ it("should satisfy sort idempotence", () => {
1428
+ fc.assert(
1429
+ fc.property(fc.array(fc.integer()), (arr) => {
1430
+ const sorted = arr.slice().sort((a, b) => a - b)
1431
+ const sortedTwice = sorted.slice().sort((a, b) => a - b)
1432
+
1433
+ return JSON.stringify(sorted) === JSON.stringify(sortedTwice)
1434
+ })
1435
+ )
1436
+ })
1437
+
1438
+ // ============================================
1439
+ // 2. Test Effect operations
1440
+ // ============================================
1441
+
1442
+ it("should map then flatMap equals flatMap with mapping", async () => {
1443
+ await fc.assert(
1444
+ fc.asyncProperty(fc.integer(), async (n) => {
1445
+ const f = (x: number) => x * 2
1446
+ const g = (x: number) => Effect.succeed(x + 1)
1447
+
1448
+ // map then flatMap
1449
+ const result1 = await Effect.runPromise(
1450
+ Effect.succeed(n).pipe(
1451
+ Effect.map(f),
1452
+ Effect.flatMap(g)
1453
+ )
1454
+ )
1455
+
1456
+ // flatMap with mapping inside
1457
+ const result2 = await Effect.runPromise(
1458
+ Effect.succeed(n).pipe(
1459
+ Effect.flatMap((x) => g(f(x)))
1460
+ )
1461
+ )
1462
+
1463
+ return result1 === result2
1464
+ })
1465
+ )
1466
+ })
1467
+
1468
+ // ============================================
1469
+ // 3. Test Option properties
1470
+ // ============================================
1471
+
1472
+ it("should satisfy Option map identity", () => {
1473
+ fc.assert(
1474
+ fc.property(fc.option(fc.integer(), { nil: undefined }), (maybeN) => {
1475
+ const option = maybeN === undefined ? Option.none() : Option.some(maybeN)
1476
+
1477
+ // Mapping identity function returns same Option
1478
+ const mapped = Option.map(option, (x) => x)
1479
+
1480
+ return Option.getOrElse(option, () => -1) ===
1481
+ Option.getOrElse(mapped, () => -1)
1482
+ })
1483
+ )
1484
+ })
1485
+
1486
+ // ============================================
1487
+ // 4. Test Schema encode/decode roundtrip
1488
+ // ============================================
1489
+
1490
+ it("should roundtrip through Schema", async () => {
1491
+ const UserSchema = Schema.Struct({
1492
+ name: Schema.String,
1493
+ age: Schema.Number.pipe(Schema.int(), Schema.positive()),
1494
+ })
1495
+
1496
+ const userArbitrary = fc.record({
1497
+ name: fc.string({ minLength: 1 }),
1498
+ age: fc.integer({ min: 1, max: 120 }),
1499
+ })
1500
+
1501
+ await fc.assert(
1502
+ fc.asyncProperty(userArbitrary, async (user) => {
1503
+ const encode = Schema.encode(UserSchema)
1504
+ const decode = Schema.decode(UserSchema)
1505
+
1506
+ // Encode then decode should return equivalent value
1507
+ const encoded = await Effect.runPromise(encode(user))
1508
+ const decoded = await Effect.runPromise(decode(encoded))
1509
+
1510
+ return decoded.name === user.name && decoded.age === user.age
1511
+ })
1512
+ )
1513
+ })
1514
+
1515
+ // ============================================
1516
+ // 5. Test error handling properties
1517
+ // ============================================
1518
+
1519
+ it("should recover from any error", async () => {
1520
+ await fc.assert(
1521
+ fc.asyncProperty(
1522
+ fc.string(),
1523
+ fc.string(),
1524
+ async (errorMsg, fallback) => {
1525
+ const failing = Effect.fail(new Error(errorMsg))
1526
+
1527
+ const result = await Effect.runPromise(
1528
+ failing.pipe(
1529
+ Effect.catchAll(() => Effect.succeed(fallback))
1530
+ )
1531
+ )
1532
+
1533
+ return result === fallback
1534
+ }
1535
+ )
1536
+ )
1537
+ })
1538
+
1539
+ // ============================================
1540
+ // 6. Custom generators for domain types
1541
+ // ============================================
1542
+
1543
+ interface Email {
1544
+ readonly _tag: "Email"
1545
+ readonly value: string
1546
+ }
1547
+
1548
+ const emailArbitrary = fc.emailAddress().map((value): Email => ({
1549
+ _tag: "Email",
1550
+ value,
1551
+ }))
1552
+
1553
+ interface UserId {
1554
+ readonly _tag: "UserId"
1555
+ readonly value: string
1556
+ }
1557
+
1558
+ const userIdArbitrary = fc.uuid().map((value): UserId => ({
1559
+ _tag: "UserId",
1560
+ value,
1561
+ }))
1562
+
1563
+ it("should handle domain types correctly", () => {
1564
+ fc.assert(
1565
+ fc.property(emailArbitrary, userIdArbitrary, (email, userId) => {
1566
+ // Test your domain functions with generated domain types
1567
+ return email.value.includes("@") && userId.value.length > 0
1568
+ })
1569
+ )
1570
+ })
1571
+
1572
+ // ============================================
1573
+ // 7. Test algebraic properties
1574
+ // ============================================
1575
+
1576
+ it("should satisfy monoid properties for string concat", () => {
1577
+ const empty = ""
1578
+ const concat = (a: string, b: string) => a + b
1579
+
1580
+ fc.assert(
1581
+ fc.property(fc.string(), fc.string(), fc.string(), (a, b, c) => {
1582
+ // Identity: empty + a = a = a + empty
1583
+ const leftIdentity = concat(empty, a) === a
1584
+ const rightIdentity = concat(a, empty) === a
1585
+
1586
+ // Associativity: (a + b) + c = a + (b + c)
1587
+ const associative = concat(concat(a, b), c) === concat(a, concat(b, c))
1588
+
1589
+ return leftIdentity && rightIdentity && associative
1590
+ })
1591
+ )
1592
+ })
1593
+
1594
+ // ============================================
1595
+ // 8. Test with constraints
1596
+ // ============================================
1597
+
1598
+ it("should handle positive numbers", () => {
1599
+ fc.assert(
1600
+ fc.property(
1601
+ fc.integer({ min: 1, max: 1000000 }),
1602
+ fc.integer({ min: 1, max: 1000000 }),
1603
+ (a, b) => {
1604
+ // Division of positives is positive
1605
+ const result = a / b
1606
+ return result > 0
1607
+ }
1608
+ )
1609
+ )
1610
+ })
1611
+ })
1612
+ ```
1613
+
1614
+ **Rationale:**
1615
+
1616
+ Use property-based testing with fast-check to test invariants and find edge cases automatically.
1617
+
1618
+ ---
1619
+
1620
+
1621
+ Property-based testing finds bugs that example tests miss:
1622
+
1623
+ 1. **Edge cases** - Empty arrays, negative numbers, unicode
1624
+ 2. **Invariants** - Properties that should always hold
1625
+ 3. **Shrinking** - Minimal failing examples
1626
+ 4. **Coverage** - Many inputs from one test
1627
+
1628
+ ---
1629
+
1630
+ ---
1631
+
1632
+