opencode-effect-enforcer 0.2.0
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.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- package/src/write-projection.ts +66 -0
|
@@ -0,0 +1,1331 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-testing
|
|
3
|
+
description: Write comprehensive tests using @effect/vitest for Effect code and vitest for pure functions. Use this skill when implementing tests for Effect-based applications, including services, layers, time-dependent effects, error handling, and property-based testing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Effect Testing Skill
|
|
7
|
+
|
|
8
|
+
This skill provides comprehensive guidance for testing Effect-based applications using `@effect/vitest` and standard `vitest`.
|
|
9
|
+
|
|
10
|
+
## Effect Source Reference
|
|
11
|
+
|
|
12
|
+
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
13
|
+
Browse and read files there directly to look up APIs, types, and implementations.
|
|
14
|
+
|
|
15
|
+
Reference this for:
|
|
16
|
+
|
|
17
|
+
- Testing utilities: `packages/effect/src/Testing.ts`
|
|
18
|
+
- @effect/vitest source: `packages/vitest/`
|
|
19
|
+
- Migration guide: `MIGRATION.md`
|
|
20
|
+
- Effect source: `packages/effect/src/`
|
|
21
|
+
|
|
22
|
+
## Framework Selection
|
|
23
|
+
|
|
24
|
+
**CRITICAL**: Choose the correct testing framework based on the code being tested.
|
|
25
|
+
|
|
26
|
+
### Use @effect/vitest for Effect Code
|
|
27
|
+
|
|
28
|
+
Use `@effect/vitest` when testing:
|
|
29
|
+
|
|
30
|
+
- Functions that return `Effect<A, E, R>`
|
|
31
|
+
- Code that uses services and layers
|
|
32
|
+
- Time-dependent operations with `TestClock`
|
|
33
|
+
- Asynchronous operations coordinated with Effect
|
|
34
|
+
- STM (Software Transactional Memory) operations
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { it, expect } from '@effect/vitest';
|
|
38
|
+
import { Effect } from 'effect';
|
|
39
|
+
|
|
40
|
+
declare const fetchUser: (id: string) => Effect.Effect<{ id: string }, Error>;
|
|
41
|
+
|
|
42
|
+
it.effect('should fetch user', () =>
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const user = yield* fetchUser('123');
|
|
45
|
+
expect(user.id).toBe('123');
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Use Regular vitest for Pure Functions
|
|
51
|
+
|
|
52
|
+
Use standard `vitest` for:
|
|
53
|
+
|
|
54
|
+
- Pure functions with no Effect wrapper
|
|
55
|
+
- Simple data transformations
|
|
56
|
+
- Helper utilities
|
|
57
|
+
- Type constructors (brands, newtypes)
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { describe, expect, it } from 'vitest';
|
|
61
|
+
|
|
62
|
+
declare const Cents: {
|
|
63
|
+
make: (value: bigint) => bigint;
|
|
64
|
+
add: (a: bigint, b: bigint) => bigint;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
describe('Cents', () => {
|
|
68
|
+
it('should add cents correctly', () => {
|
|
69
|
+
const result = Cents.add(Cents.make(100n), Cents.make(50n));
|
|
70
|
+
expect(result).toBe(150n);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Test Variants
|
|
76
|
+
|
|
77
|
+
### it.effect - Default Test Environment
|
|
78
|
+
|
|
79
|
+
Use `it.effect` by default. It scopes the test and installs Effect's `TestClock` and `TestConsole` services. Other services are not automatically test implementations and must be provided explicitly.
|
|
80
|
+
|
|
81
|
+
The callback argument is Vitest's `TestContext` (task metadata, cancellation signal, fixtures), not Effect's service context. Access Effect test services by yielding them, for example with `TestClock.adjust`.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { it, expect } from '@effect/vitest';
|
|
85
|
+
import { Effect } from 'effect';
|
|
86
|
+
|
|
87
|
+
declare const someEffect: Effect.Effect<number>;
|
|
88
|
+
declare const expected: number;
|
|
89
|
+
|
|
90
|
+
it.effect('test name', (context) =>
|
|
91
|
+
Effect.gen(function* () {
|
|
92
|
+
// context is Vitest's TestContext; TestClock is in the Effect context.
|
|
93
|
+
const result = yield* someEffect;
|
|
94
|
+
expect(result).toBe(expected);
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### it.live - Explicit Live Environment
|
|
100
|
+
|
|
101
|
+
Uses real services (real clock, real random, etc.).
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { it } from '@effect/vitest';
|
|
105
|
+
import { Effect, Clock } from 'effect';
|
|
106
|
+
|
|
107
|
+
it.live('test with real time', () =>
|
|
108
|
+
Effect.gen(function* () {
|
|
109
|
+
const now = yield* Clock.currentTimeMillis;
|
|
110
|
+
// Uses actual system time
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Use `it.live` only when real time or live runtime services are behavior under test. It scopes the test without installing `TestClock` or `TestConsole`. Real databases, HTTP clients, and filesystems still require their explicit layers; `it.live` does not provide them.
|
|
116
|
+
|
|
117
|
+
### Resource Management in Tests
|
|
118
|
+
|
|
119
|
+
`it.effect` already handles scoping internally — there is no separate `it.scoped` or `it.scopedLive` variant. Use `Effect.acquireRelease` or `Effect.scoped` directly within `it.effect`:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { it } from '@effect/vitest';
|
|
123
|
+
import { Effect } from 'effect';
|
|
124
|
+
|
|
125
|
+
declare const acquire: Effect.Effect<unknown>;
|
|
126
|
+
declare const release: Effect.Effect<void>;
|
|
127
|
+
|
|
128
|
+
it.effect('test with resources', () =>
|
|
129
|
+
Effect.gen(function* () {
|
|
130
|
+
const resource = yield* Effect.acquireRelease(acquire, () => release);
|
|
131
|
+
// Resource automatically cleaned up when the test's scope closes
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Assertions
|
|
137
|
+
|
|
138
|
+
### Use expect from vitest
|
|
139
|
+
|
|
140
|
+
For all assertions, use the standard `expect` from vitest:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { it, expect } from '@effect/vitest';
|
|
144
|
+
import { Effect } from 'effect';
|
|
145
|
+
|
|
146
|
+
declare const computation: Effect.Effect<number>;
|
|
147
|
+
declare const array: unknown[];
|
|
148
|
+
|
|
149
|
+
it.effect('assertions', () =>
|
|
150
|
+
Effect.gen(function* () {
|
|
151
|
+
const result = yield* computation;
|
|
152
|
+
expect(result).toBe(42);
|
|
153
|
+
expect(result).toBeGreaterThan(0);
|
|
154
|
+
expect(array).toHaveLength(3);
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Effect-Specific Utilities
|
|
160
|
+
|
|
161
|
+
`@effect/vitest` provides additional assertion utilities in `utils`:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { it } from '@effect/vitest';
|
|
165
|
+
import {
|
|
166
|
+
assertEquals, // Uses Effect's Equal.equals
|
|
167
|
+
assertTrue,
|
|
168
|
+
assertFalse,
|
|
169
|
+
assertSome, // For Option.Some
|
|
170
|
+
assertNone, // For Option.None
|
|
171
|
+
assertSuccess, // For Either.Right / Exit.Success
|
|
172
|
+
assertFailure // For Either.Left / Exit.Failure
|
|
173
|
+
} from '@effect/vitest/utils';
|
|
174
|
+
import { Effect, Option, Either } from 'effect';
|
|
175
|
+
|
|
176
|
+
declare const someOptionalEffect: Effect.Effect<Option.Option<number>>;
|
|
177
|
+
declare const someEitherEffect: Effect.Effect<Either.Either<number, Error>>;
|
|
178
|
+
declare const expectedValue: number;
|
|
179
|
+
|
|
180
|
+
it.effect('with effect assertions', () =>
|
|
181
|
+
Effect.gen(function* () {
|
|
182
|
+
const option = yield* someOptionalEffect;
|
|
183
|
+
assertSome(option, expectedValue);
|
|
184
|
+
|
|
185
|
+
const either = yield* someEitherEffect;
|
|
186
|
+
assertSuccess(either, expectedValue);
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Testing with Services and Layers
|
|
192
|
+
|
|
193
|
+
### Providing Services to Tests
|
|
194
|
+
|
|
195
|
+
Use `Effect.provide` to supply test implementations:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { it, expect } from '@effect/vitest';
|
|
199
|
+
import { Effect, Context, Layer } from 'effect';
|
|
200
|
+
|
|
201
|
+
class UserService extends Context.Service<
|
|
202
|
+
UserService,
|
|
203
|
+
{
|
|
204
|
+
getUser: (id: string) => Effect.Effect<{ name: string }>;
|
|
205
|
+
}
|
|
206
|
+
>()('UserService') {}
|
|
207
|
+
|
|
208
|
+
declare const TestUserServiceLayer: Layer.Layer<UserService>;
|
|
209
|
+
|
|
210
|
+
it.effect('should work with dependencies', () =>
|
|
211
|
+
Effect.gen(function* () {
|
|
212
|
+
const userService = yield* UserService;
|
|
213
|
+
const result = yield* userService.getUser('123');
|
|
214
|
+
expect(result.name).toBe('John');
|
|
215
|
+
}).pipe(Effect.provide(TestUserServiceLayer))
|
|
216
|
+
);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// Concise alternative using Layer.mock (v4)
|
|
221
|
+
const TestUserService = Layer.mock(UserService)({
|
|
222
|
+
getUser: (id) => Effect.succeed({ name: 'John' })
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`Layer.mock(Service)({...})` is shorthand for `Layer.succeed(Service, Service.of({...}))` — use whichever reads more clearly in context.
|
|
227
|
+
|
|
228
|
+
### First-Class Controllable Test Services
|
|
229
|
+
|
|
230
|
+
For reusable stateful fakes, define file-local `TestInterface extends Interface`, a separate `TestService` tag, and a `testLayer` that provides one implementation under both tags. Production code depends only on `Service`; tests yield `TestService` to inspect calls and trigger failures or lifecycle transitions. The owning leaf self-exports its canonical identity at the bottom; sibling modules import that identity from the leaf, while folder/package barrels only relay it. This intentional self-reference requires toolchain and runtime support, so preserve an established project convention when it differs.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// notifier.ts
|
|
234
|
+
import { Context, Effect, Layer, Option, Ref } from 'effect';
|
|
235
|
+
import * as Arr from 'effect/Array';
|
|
236
|
+
|
|
237
|
+
export interface Interface {
|
|
238
|
+
readonly send: (message: Message) => Effect.Effect<void, SendError>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export class Service extends Context.Service<Service, Interface>()(
|
|
242
|
+
'@app/Notifier'
|
|
243
|
+
) {}
|
|
244
|
+
|
|
245
|
+
export interface TestInterface extends Interface {
|
|
246
|
+
readonly sentMessages: () => Effect.Effect<ReadonlyArray<Message>>;
|
|
247
|
+
readonly failNextSend: (error: SendError) => Effect.Effect<void>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export class TestService extends Context.Service<TestService, TestInterface>()(
|
|
251
|
+
'@app/Notifier/Test'
|
|
252
|
+
) {}
|
|
253
|
+
|
|
254
|
+
export const testLayer = Layer.effectContext(
|
|
255
|
+
Effect.gen(function* () {
|
|
256
|
+
const sent = yield* Ref.make<ReadonlyArray<Message>>([]);
|
|
257
|
+
const nextFailure = yield* Ref.make<Option.Option<SendError>>(
|
|
258
|
+
Option.none()
|
|
259
|
+
);
|
|
260
|
+
const service = TestService.of({
|
|
261
|
+
send: Effect.fn('Notifier.Test.send')(function* (message) {
|
|
262
|
+
const failure = yield* Ref.getAndSet(nextFailure, Option.none());
|
|
263
|
+
if (Option.isSome(failure)) return yield* Effect.fail(failure.value);
|
|
264
|
+
yield* Ref.update(sent, Arr.append(message));
|
|
265
|
+
}),
|
|
266
|
+
sentMessages: () => Ref.get(sent),
|
|
267
|
+
failNextSend: (error) => Ref.set(nextFailure, Option.some(error))
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return Context.empty().pipe(
|
|
271
|
+
Context.add(Service, service),
|
|
272
|
+
Context.add(TestService, service)
|
|
273
|
+
);
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
export * as Notifier from './notifier.js';
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Use `Layer.succeed` for complete static fakes. Reserve `Layer.mock` for tiny local partial mocks where omitted methods should fail loudly.
|
|
281
|
+
|
|
282
|
+
### Using layer Helper
|
|
283
|
+
|
|
284
|
+
Share a layer across multiple tests with the `layer` function:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { layer, it, expect } from '@effect/vitest';
|
|
288
|
+
import { Effect, Context, Layer } from 'effect';
|
|
289
|
+
|
|
290
|
+
class Database extends Context.Service<
|
|
291
|
+
Database,
|
|
292
|
+
{
|
|
293
|
+
query: (sql: string) => Effect.Effect<Array<unknown>>;
|
|
294
|
+
}
|
|
295
|
+
>()('Database') {
|
|
296
|
+
static Test = Layer.succeed(Database, {
|
|
297
|
+
query: (sql) => Effect.succeed([])
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
layer(Database.Test)((it) => {
|
|
302
|
+
it.effect('test 1', () =>
|
|
303
|
+
Effect.gen(function* () {
|
|
304
|
+
const db = yield* Database;
|
|
305
|
+
const results = yield* db.query('SELECT *');
|
|
306
|
+
expect(results).toEqual([]);
|
|
307
|
+
})
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
it.effect('test 2', () =>
|
|
311
|
+
Effect.gen(function* () {
|
|
312
|
+
const db = yield* Database;
|
|
313
|
+
// Database available in all tests
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// With name for describe block
|
|
319
|
+
layer(Database.Test)('Database tests', (it) => {
|
|
320
|
+
it.effect('query test', () => Effect.succeed(true));
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Nested Layers
|
|
325
|
+
|
|
326
|
+
Compose layers for complex dependencies:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { layer, it } from '@effect/vitest';
|
|
330
|
+
import { Effect, Context, Layer } from 'effect';
|
|
331
|
+
|
|
332
|
+
class Database extends Context.Service<
|
|
333
|
+
Database,
|
|
334
|
+
{
|
|
335
|
+
query: (sql: string) => Effect.Effect<Array<unknown>>;
|
|
336
|
+
}
|
|
337
|
+
>()('Database') {}
|
|
338
|
+
|
|
339
|
+
class UserService extends Context.Service<
|
|
340
|
+
UserService,
|
|
341
|
+
{
|
|
342
|
+
getUser: (id: string) => Effect.Effect<unknown>;
|
|
343
|
+
}
|
|
344
|
+
>()('UserService') {}
|
|
345
|
+
|
|
346
|
+
declare const DatabaseLayer: Layer.Layer<Database>;
|
|
347
|
+
declare const UserServiceLayer: Layer.Layer<UserService, never, Database>;
|
|
348
|
+
|
|
349
|
+
layer(DatabaseLayer)((it) => {
|
|
350
|
+
it.layer(UserServiceLayer)('user tests', (it) => {
|
|
351
|
+
it.effect('has both dependencies', () =>
|
|
352
|
+
Effect.gen(function* () {
|
|
353
|
+
const db = yield* Database;
|
|
354
|
+
const userService = yield* UserService;
|
|
355
|
+
// Both available
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
A nested `it.layer` suite **reuses** the parent suite's memoized layer allocations rather than rebuilding them. As of beta.67, each nested suite also **forks its own memo map**, so layers allocated locally inside one nested suite are isolated from sibling nested suites and are released independently when that suite finishes. The practical effect: shared parent layers (e.g. `DatabaseLayer`) are built once and reused, while sibling-local allocations do not leak across siblings even in concurrent suites.
|
|
363
|
+
|
|
364
|
+
### Excluding Test Services
|
|
365
|
+
|
|
366
|
+
Use live services instead of test services:
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
import { layer, it } from '@effect/vitest';
|
|
370
|
+
import { Effect, Layer } from 'effect';
|
|
371
|
+
|
|
372
|
+
declare const MyServiceLayer: Layer.Layer<never>;
|
|
373
|
+
|
|
374
|
+
layer(MyServiceLayer, { excludeTestServices: true })((it) => {
|
|
375
|
+
it.effect('uses real clock', () =>
|
|
376
|
+
Effect.gen(function* () {
|
|
377
|
+
// Uses actual Clock, not TestClock
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Time-Dependent Testing with TestClock
|
|
384
|
+
|
|
385
|
+
### Basic TestClock Usage
|
|
386
|
+
|
|
387
|
+
`TestClock` allows controlling time without waiting:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { it, expect } from '@effect/vitest';
|
|
391
|
+
import { Effect, Fiber } from 'effect';
|
|
392
|
+
import { TestClock } from 'effect/testing';
|
|
393
|
+
|
|
394
|
+
it.effect('should handle delays', () =>
|
|
395
|
+
Effect.gen(function* () {
|
|
396
|
+
const fiber = yield* Effect.forkChild(
|
|
397
|
+
Effect.sleep('5 seconds').pipe(Effect.as('done'))
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
// Advance time by 5 seconds instantly
|
|
401
|
+
yield* TestClock.adjust('5 seconds');
|
|
402
|
+
|
|
403
|
+
const result = yield* Fiber.join(fiber);
|
|
404
|
+
expect(result).toBe('done');
|
|
405
|
+
})
|
|
406
|
+
);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Testing Recurring Effects
|
|
410
|
+
|
|
411
|
+
Test periodic operations efficiently:
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
import { it, expect } from '@effect/vitest';
|
|
415
|
+
import { Effect, Queue, Option } from 'effect';
|
|
416
|
+
import { TestClock } from 'effect/testing';
|
|
417
|
+
|
|
418
|
+
it.effect('should execute every minute', () =>
|
|
419
|
+
Effect.gen(function* () {
|
|
420
|
+
const queue = yield* Queue.unbounded<number>();
|
|
421
|
+
|
|
422
|
+
// Fork effect that repeats every minute
|
|
423
|
+
yield* Effect.forkChild(
|
|
424
|
+
Queue.offer(queue, 1).pipe(
|
|
425
|
+
Effect.delay('60 seconds'),
|
|
426
|
+
Effect.forever
|
|
427
|
+
)
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// No effect before time passes
|
|
431
|
+
const empty = yield* Queue.poll(queue);
|
|
432
|
+
expect(Option.isNone(empty)).toBe(true);
|
|
433
|
+
|
|
434
|
+
// Advance time
|
|
435
|
+
yield* TestClock.adjust('60 seconds');
|
|
436
|
+
|
|
437
|
+
// Effect executed once
|
|
438
|
+
const value = yield* Queue.take(queue);
|
|
439
|
+
expect(value).toBe(1);
|
|
440
|
+
|
|
441
|
+
// Verify only one execution
|
|
442
|
+
const stillEmpty = yield* Queue.poll(queue);
|
|
443
|
+
expect(Option.isNone(stillEmpty)).toBe(true);
|
|
444
|
+
})
|
|
445
|
+
);
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Testing Clock Methods
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
import { it, expect } from '@effect/vitest';
|
|
452
|
+
import { Effect, Clock } from 'effect';
|
|
453
|
+
import { TestClock } from 'effect/testing';
|
|
454
|
+
|
|
455
|
+
it.effect('should track time correctly', () =>
|
|
456
|
+
Effect.gen(function* () {
|
|
457
|
+
const start = yield* Clock.currentTimeMillis;
|
|
458
|
+
|
|
459
|
+
yield* TestClock.adjust('1 minute');
|
|
460
|
+
|
|
461
|
+
const end = yield* Clock.currentTimeMillis;
|
|
462
|
+
|
|
463
|
+
expect(end - start).toBeGreaterThanOrEqual(60_000);
|
|
464
|
+
})
|
|
465
|
+
);
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
The clock separates Unix wall time (`Clock.currentTimeMillis` / `currentTimeNanos`) from monotonic elapsed time (`Clock.monotonicTimeNanos`). `TestClock.adjust` advances both, while `TestClock.setTime` may move wall time backward without decreasing monotonic time. Duration measurements such as `Effect.timed` therefore remain stable across wall-clock corrections. Nanosecond wall time also remains precise for large finite timestamps, and reads stay total after an infinite adjustment.
|
|
469
|
+
|
|
470
|
+
### TestClock with Deferred
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
import { it, expect } from '@effect/vitest';
|
|
474
|
+
import { Effect, Deferred } from 'effect';
|
|
475
|
+
import { TestClock } from 'effect/testing';
|
|
476
|
+
|
|
477
|
+
it.effect('should handle deferred with delays', () =>
|
|
478
|
+
Effect.gen(function* () {
|
|
479
|
+
const deferred = yield* Deferred.make<number>();
|
|
480
|
+
|
|
481
|
+
yield* Effect.forkChild(
|
|
482
|
+
Effect.gen(function* () {
|
|
483
|
+
yield* Effect.sleep('60 seconds');
|
|
484
|
+
yield* Deferred.succeed(deferred, 42);
|
|
485
|
+
})
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
yield* TestClock.adjust('60 seconds');
|
|
489
|
+
|
|
490
|
+
const result = yield* Deferred.await(deferred);
|
|
491
|
+
expect(result).toBe(42);
|
|
492
|
+
})
|
|
493
|
+
);
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
## Error Testing
|
|
497
|
+
|
|
498
|
+
### Testing Expected Failures
|
|
499
|
+
|
|
500
|
+
Use `Effect.flip` to convert failures to successes:
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
import { it, expect } from '@effect/vitest';
|
|
504
|
+
import { Effect, Schema } from 'effect';
|
|
505
|
+
|
|
506
|
+
class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(
|
|
507
|
+
'UserNotFoundError',
|
|
508
|
+
{
|
|
509
|
+
userId: Schema.String
|
|
510
|
+
}
|
|
511
|
+
) {}
|
|
512
|
+
|
|
513
|
+
declare const failingOperation: () => Effect.Effect<never, UserNotFoundError>;
|
|
514
|
+
|
|
515
|
+
it.effect('should fail with error', () =>
|
|
516
|
+
Effect.gen(function* () {
|
|
517
|
+
const error = yield* Effect.flip(failingOperation());
|
|
518
|
+
expect(error).toBeInstanceOf(UserNotFoundError);
|
|
519
|
+
expect(error.userId).toBe('123');
|
|
520
|
+
})
|
|
521
|
+
);
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Testing with Exit
|
|
525
|
+
|
|
526
|
+
Use `Effect.exit` to capture both success and failure:
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
import { it, expect } from '@effect/vitest';
|
|
530
|
+
import { Effect, Exit } from 'effect';
|
|
531
|
+
|
|
532
|
+
declare const divide: (a: number, b: number) => Effect.Effect<number, string>;
|
|
533
|
+
|
|
534
|
+
it.effect('should handle success', () =>
|
|
535
|
+
Effect.gen(function* () {
|
|
536
|
+
const exit = yield* Effect.exit(divide(4, 2));
|
|
537
|
+
expect(exit).toEqual(Exit.succeed(2));
|
|
538
|
+
})
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
it.effect('should handle failure', () =>
|
|
542
|
+
Effect.gen(function* () {
|
|
543
|
+
const exit = yield* Effect.exit(divide(4, 0));
|
|
544
|
+
expect(exit).toEqual(Exit.fail('Cannot divide by zero'));
|
|
545
|
+
})
|
|
546
|
+
);
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Testing Error Types
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
import { it, expect } from '@effect/vitest';
|
|
553
|
+
import { Effect, Exit, Cause, Schema } from 'effect';
|
|
554
|
+
import * as Option from 'effect/Option';
|
|
555
|
+
|
|
556
|
+
class NotFoundError extends Schema.TaggedError<NotFoundError>()(
|
|
557
|
+
'NotFoundError',
|
|
558
|
+
{
|
|
559
|
+
id: Schema.String
|
|
560
|
+
}
|
|
561
|
+
) {}
|
|
562
|
+
|
|
563
|
+
class UserService extends Context.Service<
|
|
564
|
+
UserService,
|
|
565
|
+
{
|
|
566
|
+
getUser: (id: string) => Effect.Effect<unknown, NotFoundError>;
|
|
567
|
+
}
|
|
568
|
+
>()('UserService') {}
|
|
569
|
+
|
|
570
|
+
declare const userService: {
|
|
571
|
+
getUser: (id: string) => Effect.Effect<unknown, NotFoundError>;
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
it.effect('should fail with specific error', () =>
|
|
575
|
+
Effect.gen(function* () {
|
|
576
|
+
const exit = yield* Effect.exit(userService.getUser('nonexistent'));
|
|
577
|
+
|
|
578
|
+
if (Exit.isFailure(exit)) {
|
|
579
|
+
const cause = exit.cause;
|
|
580
|
+
// v4: use hasFails (not isFailType) and findErrorOption (not failureOrCause)
|
|
581
|
+
expect(Cause.hasFails(cause)).toBe(true);
|
|
582
|
+
const errorOpt = Cause.findErrorOption(cause);
|
|
583
|
+
expect(Option.isSome(errorOpt)).toBe(true);
|
|
584
|
+
if (Option.isSome(errorOpt)) {
|
|
585
|
+
expect(errorOpt.value).toBeInstanceOf(NotFoundError);
|
|
586
|
+
}
|
|
587
|
+
} else {
|
|
588
|
+
throw new Error('Expected failure');
|
|
589
|
+
}
|
|
590
|
+
})
|
|
591
|
+
);
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
## Property-Based Testing
|
|
595
|
+
|
|
596
|
+
### Using it.prop for Pure Properties
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
import { FastCheck } from 'effect/testing';
|
|
600
|
+
import { it } from '@effect/vitest';
|
|
601
|
+
|
|
602
|
+
it.prop(
|
|
603
|
+
'addition is commutative',
|
|
604
|
+
[FastCheck.integer(), FastCheck.integer()],
|
|
605
|
+
([a, b]) => a + b === b + a
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
// With object syntax
|
|
609
|
+
it.prop(
|
|
610
|
+
'multiplication distributes',
|
|
611
|
+
{ a: FastCheck.integer(), b: FastCheck.integer(), c: FastCheck.integer() },
|
|
612
|
+
({ a, b, c }) => a * (b + c) === a * b + a * c
|
|
613
|
+
);
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### Using it.effect.prop for Effect Properties
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
import { it } from '@effect/vitest';
|
|
620
|
+
import { Effect, Context } from 'effect';
|
|
621
|
+
import { FastCheck } from 'effect/testing';
|
|
622
|
+
|
|
623
|
+
class Database extends Context.Service<
|
|
624
|
+
Database,
|
|
625
|
+
{
|
|
626
|
+
set: (key: string, value: number) => Effect.Effect<void>;
|
|
627
|
+
get: (key: string) => Effect.Effect<number>;
|
|
628
|
+
}
|
|
629
|
+
>()('Database') {}
|
|
630
|
+
|
|
631
|
+
it.effect.prop(
|
|
632
|
+
'database operations are idempotent',
|
|
633
|
+
[FastCheck.string(), FastCheck.integer()],
|
|
634
|
+
([key, value]) =>
|
|
635
|
+
Effect.gen(function* () {
|
|
636
|
+
const db = yield* Database;
|
|
637
|
+
|
|
638
|
+
yield* db.set(key, value);
|
|
639
|
+
const result1 = yield* db.get(key);
|
|
640
|
+
|
|
641
|
+
yield* db.set(key, value);
|
|
642
|
+
const result2 = yield* db.get(key);
|
|
643
|
+
|
|
644
|
+
return result1 === result2;
|
|
645
|
+
})
|
|
646
|
+
);
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### With Schema Arbitraries
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
import { it, expect } from '@effect/vitest';
|
|
653
|
+
import { Effect, Schema } from 'effect';
|
|
654
|
+
|
|
655
|
+
const User = Schema.Struct({
|
|
656
|
+
id: Schema.String,
|
|
657
|
+
age: Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 120 }))
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it.effect.prop('user validation works', { user: User }, ({ user }) =>
|
|
661
|
+
Effect.gen(function* () {
|
|
662
|
+
expect(user.age).toBeGreaterThanOrEqual(0);
|
|
663
|
+
expect(user.age).toBeLessThanOrEqual(120);
|
|
664
|
+
return true;
|
|
665
|
+
})
|
|
666
|
+
);
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
`it.prop` and `it.effect.prop` accept schemas directly and derive their arbitraries internally. For manual use, beta.106 consolidated derivation into `Schema.toArbitrary(schema)`, which returns a factory that must receive the fast-check module:
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
import { Schema } from 'effect';
|
|
673
|
+
import { FastCheck } from 'effect/testing';
|
|
674
|
+
|
|
675
|
+
const UserArbitrary = Schema.toArbitrary(User)(FastCheck);
|
|
676
|
+
const samples = FastCheck.sample(UserArbitrary, 10);
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
`Schema.toArbitraryLazy` and arbitrary derivation reports no longer exist.
|
|
680
|
+
|
|
681
|
+
### Configuring FastCheck
|
|
682
|
+
|
|
683
|
+
```typescript
|
|
684
|
+
import { it } from '@effect/vitest';
|
|
685
|
+
import { Effect } from 'effect';
|
|
686
|
+
import { FastCheck } from 'effect/testing';
|
|
687
|
+
|
|
688
|
+
it.effect.prop(
|
|
689
|
+
'property test',
|
|
690
|
+
[FastCheck.integer()],
|
|
691
|
+
([n]) => Effect.succeed(n >= 0 || n < 0),
|
|
692
|
+
{
|
|
693
|
+
timeout: 10000,
|
|
694
|
+
fastCheck: {
|
|
695
|
+
numRuns: 1000,
|
|
696
|
+
seed: 42,
|
|
697
|
+
verbose: true
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
## Test Control
|
|
704
|
+
|
|
705
|
+
### Skipping Tests
|
|
706
|
+
|
|
707
|
+
```typescript
|
|
708
|
+
import { it } from '@effect/vitest';
|
|
709
|
+
import { Effect } from 'effect';
|
|
710
|
+
|
|
711
|
+
declare const condition: boolean;
|
|
712
|
+
|
|
713
|
+
it.effect.skip('not ready yet', () =>
|
|
714
|
+
Effect.gen(function* () {
|
|
715
|
+
// Will not run
|
|
716
|
+
})
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
it.effect.skipIf(condition)('conditional skip', () =>
|
|
720
|
+
Effect.gen(function* () {
|
|
721
|
+
// Only runs if condition is false
|
|
722
|
+
})
|
|
723
|
+
);
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### Running Single Tests
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
import { it } from '@effect/vitest';
|
|
730
|
+
import { Effect } from 'effect';
|
|
731
|
+
|
|
732
|
+
it.effect.only('debug this test', () =>
|
|
733
|
+
Effect.gen(function* () {
|
|
734
|
+
// Only this test runs
|
|
735
|
+
})
|
|
736
|
+
);
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### Running Conditionally
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
import { it } from '@effect/vitest';
|
|
743
|
+
import { Effect } from 'effect';
|
|
744
|
+
|
|
745
|
+
it.effect.runIf(process.env.INTEGRATION_TESTS)('integration test', () =>
|
|
746
|
+
Effect.gen(function* () {
|
|
747
|
+
// Only runs if condition is true
|
|
748
|
+
})
|
|
749
|
+
);
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### Expecting Failures
|
|
753
|
+
|
|
754
|
+
```typescript
|
|
755
|
+
import { it, expect } from '@effect/vitest';
|
|
756
|
+
import { Effect } from 'effect';
|
|
757
|
+
|
|
758
|
+
it.effect.fails('known failing test', () =>
|
|
759
|
+
Effect.gen(function* () {
|
|
760
|
+
// This test is expected to fail
|
|
761
|
+
// Will pass if it fails, fail if it passes
|
|
762
|
+
expect(1).toBe(2);
|
|
763
|
+
})
|
|
764
|
+
);
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
## Testing Flaky Operations
|
|
768
|
+
|
|
769
|
+
Use `it.flakyTest` for operations that may fail intermittently:
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
import { it } from '@effect/vitest';
|
|
773
|
+
import { Effect, Random } from 'effect';
|
|
774
|
+
|
|
775
|
+
it.effect('retrying flaky operation', () =>
|
|
776
|
+
it.flakyTest(
|
|
777
|
+
Effect.gen(function* () {
|
|
778
|
+
const random = yield* Random.nextBoolean;
|
|
779
|
+
if (random) {
|
|
780
|
+
yield* Effect.fail('Random failure');
|
|
781
|
+
}
|
|
782
|
+
}),
|
|
783
|
+
'5 seconds' // Retry timeout
|
|
784
|
+
)
|
|
785
|
+
);
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
## Logging in Tests
|
|
789
|
+
|
|
790
|
+
### Default Behavior (Suppressed)
|
|
791
|
+
|
|
792
|
+
```typescript
|
|
793
|
+
import { it } from '@effect/vitest';
|
|
794
|
+
import { Effect } from 'effect';
|
|
795
|
+
|
|
796
|
+
it.effect('logs are suppressed', () =>
|
|
797
|
+
Effect.gen(function* () {
|
|
798
|
+
yield* Effect.log("This won't appear");
|
|
799
|
+
})
|
|
800
|
+
);
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
### Enabling Logs
|
|
804
|
+
|
|
805
|
+
```typescript
|
|
806
|
+
import { it } from '@effect/vitest';
|
|
807
|
+
import { Effect, Logger } from 'effect';
|
|
808
|
+
|
|
809
|
+
it.effect('logs visible', () =>
|
|
810
|
+
Effect.gen(function* () {
|
|
811
|
+
yield* Effect.log('This will appear');
|
|
812
|
+
}).pipe(Effect.provide(Logger.pretty))
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
// Use it.live only when the live console itself is under test.
|
|
816
|
+
it.live('logs visible', () =>
|
|
817
|
+
Effect.gen(function* () {
|
|
818
|
+
yield* Effect.log('This will appear');
|
|
819
|
+
})
|
|
820
|
+
);
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
## Testing Patterns
|
|
824
|
+
|
|
825
|
+
### Arrange-Act-Assert Pattern
|
|
826
|
+
|
|
827
|
+
```typescript
|
|
828
|
+
import { describe, it, expect } from '@effect/vitest';
|
|
829
|
+
import { Effect, Context, Layer } from 'effect';
|
|
830
|
+
|
|
831
|
+
class UserService extends Context.Service<
|
|
832
|
+
UserService,
|
|
833
|
+
{
|
|
834
|
+
getUser: (id: string) => Effect.Effect<{ id: string; name: string }>;
|
|
835
|
+
}
|
|
836
|
+
>()('UserService') {}
|
|
837
|
+
|
|
838
|
+
declare const TestUserServiceLayer: Layer.Layer<UserService>;
|
|
839
|
+
|
|
840
|
+
describe('UserService', () => {
|
|
841
|
+
describe('getUser', () => {
|
|
842
|
+
it.effect('should return user by id', () =>
|
|
843
|
+
Effect.gen(function* () {
|
|
844
|
+
// Arrange
|
|
845
|
+
const userId = 'user-123';
|
|
846
|
+
const expectedUser = { id: userId, name: 'Alice' };
|
|
847
|
+
|
|
848
|
+
// Act
|
|
849
|
+
const service = yield* UserService;
|
|
850
|
+
const user = yield* service.getUser(userId);
|
|
851
|
+
|
|
852
|
+
// Assert
|
|
853
|
+
expect(user).toEqual(expectedUser);
|
|
854
|
+
}).pipe(Effect.provide(TestUserServiceLayer))
|
|
855
|
+
);
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### Testing STM Operations
|
|
861
|
+
|
|
862
|
+
```typescript
|
|
863
|
+
import { it, expect } from '@effect/vitest';
|
|
864
|
+
import { Effect, STM, TRef } from 'effect';
|
|
865
|
+
|
|
866
|
+
it.effect('should handle concurrent updates', () =>
|
|
867
|
+
Effect.gen(function* () {
|
|
868
|
+
const counter = yield* TRef.make(0);
|
|
869
|
+
|
|
870
|
+
const increment = STM.updateAndGet(counter, (n) => n + 1);
|
|
871
|
+
|
|
872
|
+
yield* STM.commit(increment);
|
|
873
|
+
yield* STM.commit(increment);
|
|
874
|
+
|
|
875
|
+
const final = yield* STM.commit(TRef.get(counter));
|
|
876
|
+
expect(final).toBe(2);
|
|
877
|
+
})
|
|
878
|
+
);
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
### Testing CRDT Operations
|
|
882
|
+
|
|
883
|
+
```typescript
|
|
884
|
+
import { it, expect } from '@effect/vitest';
|
|
885
|
+
import { Effect, STM } from 'effect';
|
|
886
|
+
|
|
887
|
+
declare const GCounter: {
|
|
888
|
+
make: (id: string) => Effect.Effect<unknown>;
|
|
889
|
+
increment: (counter: unknown, value: number) => STM.STM<void>;
|
|
890
|
+
query: (counter: unknown) => STM.STM<unknown>;
|
|
891
|
+
merge: (counter: unknown, state: unknown) => STM.STM<void>;
|
|
892
|
+
value: (counter: unknown) => STM.STM<number>;
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
declare const ReplicaId: (id: string) => string;
|
|
896
|
+
|
|
897
|
+
it.effect('should merge states correctly', () =>
|
|
898
|
+
Effect.gen(function* () {
|
|
899
|
+
const counter1 = yield* GCounter.make(ReplicaId('replica-1'));
|
|
900
|
+
const counter2 = yield* GCounter.make(ReplicaId('replica-2'));
|
|
901
|
+
|
|
902
|
+
yield* STM.commit(GCounter.increment(counter1, 10));
|
|
903
|
+
yield* STM.commit(GCounter.increment(counter2, 20));
|
|
904
|
+
|
|
905
|
+
const state2 = yield* STM.commit(GCounter.query(counter2));
|
|
906
|
+
yield* STM.commit(GCounter.merge(counter1, state2));
|
|
907
|
+
|
|
908
|
+
const result = yield* STM.commit(GCounter.value(counter1));
|
|
909
|
+
expect(result).toBe(30);
|
|
910
|
+
})
|
|
911
|
+
);
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
## HTTP Mock Server Testing
|
|
915
|
+
|
|
916
|
+
For services that speak HTTP protocols (REST, SSE, streaming), **prefer HTTP mock server testing over service-level fakes**. This approach tests the full HTTP integration path — serialization, status codes, retries, SSE framing — and catches bugs that service fakes miss.
|
|
917
|
+
|
|
918
|
+
**When to use HTTP mock servers vs service fakes:**
|
|
919
|
+
|
|
920
|
+
- **HTTP mock server** (preferred): when the service communicates over HTTP/SSE and transport-level correctness matters. The real service layer (`MyService.defaultLayer`) is used, backed by a mock HTTP server at port 0.
|
|
921
|
+
- **Service fake** (`Layer.succeed`): when the service is a pure domain abstraction with no protocol-level concerns.
|
|
922
|
+
|
|
923
|
+
The pattern:
|
|
924
|
+
|
|
925
|
+
1. **Define a `TestServer` service** as a `Context.Service` with semantic helper methods (not raw `push(reply)`). For an LLM server, expose `text(content)`, `tool(call)`, `fail(error)`, `hang`, `hold(promise)`. Each method pushes a typed Step onto a queue.
|
|
926
|
+
2. **Implement with `Layer.effect`** — build an `HttpRouter` that dequeues steps on each request, use `HttpServerResponse.stream` for SSE endpoints, and bind to a random port with `NodeHttpServer.layer(() => Http.createServer(), { port: 0 })`.
|
|
927
|
+
3. **Use `Deferred`-based request counting** for `wait(count)` — the server increments a counter on each request and completes a `Deferred` when the count is reached, letting the test block until the expected number of calls arrive. This eliminates all `setTimeout`/polling from tests.
|
|
928
|
+
4. **Wire the mock URL into test config** via a callback: `config: (url) => ({ baseUrl: url })`.
|
|
929
|
+
|
|
930
|
+
### Typed Step ADT for Mock Responses
|
|
931
|
+
|
|
932
|
+
Define mock response types as a discriminated union so test intent is readable:
|
|
933
|
+
|
|
934
|
+
```typescript
|
|
935
|
+
type Step =
|
|
936
|
+
| { readonly kind: 'text'; readonly content: string }
|
|
937
|
+
| { readonly kind: 'tool'; readonly call: ToolCall }
|
|
938
|
+
| { readonly kind: 'fail'; readonly error: string }
|
|
939
|
+
| { readonly kind: 'hang' } // Keeps connection open indefinitely
|
|
940
|
+
| { readonly kind: 'hold'; readonly wait: Promise<void> }; // Blocks until resolved
|
|
941
|
+
|
|
942
|
+
// Semantic helpers on the TestServer service:
|
|
943
|
+
// yield* server.text("hello") — enqueue a text response
|
|
944
|
+
// yield* server.tool(toolCall) — enqueue a tool call response
|
|
945
|
+
// yield* server.fail("error") — enqueue a mid-stream SSE failure
|
|
946
|
+
// server.hang — enqueue a response that never completes
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
### SSE Response Patterns
|
|
950
|
+
|
|
951
|
+
For SSE endpoints, use `HttpServerResponse.stream` with different `Stream` constructors per step type:
|
|
952
|
+
|
|
953
|
+
```typescript
|
|
954
|
+
import { Effect, Stream } from 'effect';
|
|
955
|
+
import { HttpServerResponse } from 'effect/unstable/http';
|
|
956
|
+
|
|
957
|
+
// Normal SSE response — stream JSON lines, then [DONE]
|
|
958
|
+
const sse = (lines: ReadonlyArray<unknown>) =>
|
|
959
|
+
HttpServerResponse.stream(
|
|
960
|
+
Stream.fromIterable([
|
|
961
|
+
[
|
|
962
|
+
...lines.map((line) => `data: ${JSON.stringify(line)}`),
|
|
963
|
+
'data: [DONE]'
|
|
964
|
+
].join('\n\n') + '\n\n'
|
|
965
|
+
]).pipe(Stream.encodeText),
|
|
966
|
+
{ contentType: 'text/event-stream' }
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
// Hang — connection stays open forever (for testing cancellation)
|
|
970
|
+
const hang = HttpServerResponse.stream(Stream.never, {
|
|
971
|
+
contentType: 'text/event-stream'
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
// Mid-stream failure — partial data then error
|
|
975
|
+
const fail = (error: string) =>
|
|
976
|
+
HttpServerResponse.stream(
|
|
977
|
+
Stream.concat(
|
|
978
|
+
Stream.fromIterable([`data: {"partial": true}\n\n`]),
|
|
979
|
+
Stream.fail(new Error(error))
|
|
980
|
+
).pipe(Stream.encodeText),
|
|
981
|
+
{ contentType: 'text/event-stream' }
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
// Hold — blocks until a promise resolves (for testing timing)
|
|
985
|
+
const hold = (wait: Promise<void>) =>
|
|
986
|
+
HttpServerResponse.stream(
|
|
987
|
+
Stream.fromEffect(Effect.promise(() => wait)).pipe(
|
|
988
|
+
Stream.flatMap(() => Stream.fromIterable(['data: [DONE]\n\n'])),
|
|
989
|
+
Stream.encodeText
|
|
990
|
+
),
|
|
991
|
+
{ contentType: 'text/event-stream' }
|
|
992
|
+
);
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
### Test Fixture Composition
|
|
996
|
+
|
|
997
|
+
```typescript
|
|
998
|
+
// Test fixture composition
|
|
999
|
+
const withMockServer = <A, E, R>(
|
|
1000
|
+
self: (server: TestApiServer) => Effect.Effect<A, E, R>
|
|
1001
|
+
) =>
|
|
1002
|
+
Effect.gen(function* () {
|
|
1003
|
+
const server = yield* TestApiServer;
|
|
1004
|
+
return yield* self(server);
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
// In test — uses the REAL service layer, backed by mock HTTP
|
|
1008
|
+
it.live('calls API correctly', () =>
|
|
1009
|
+
withMockServer((server) =>
|
|
1010
|
+
Effect.gen(function* () {
|
|
1011
|
+
yield* server.text('hello world');
|
|
1012
|
+
const result = yield* MyService.use((svc) => svc.callApi());
|
|
1013
|
+
expect(result).toEqual('hello world');
|
|
1014
|
+
})
|
|
1015
|
+
).pipe(
|
|
1016
|
+
Effect.provide(MyService.defaultLayer), // Real service, not a fake
|
|
1017
|
+
Effect.provide(TestApiServer.layer)
|
|
1018
|
+
)
|
|
1019
|
+
);
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
### Test Fake Factories
|
|
1023
|
+
|
|
1024
|
+
When providing fake service layers in tests, return the test data alongside the layer to avoid duplicating constants between setup and assertions:
|
|
1025
|
+
|
|
1026
|
+
```typescript
|
|
1027
|
+
export function fakeUserRepo(overrides?: { user?: User }) {
|
|
1028
|
+
const user = overrides?.user ?? new User({ id: '1', name: 'Test' });
|
|
1029
|
+
return {
|
|
1030
|
+
user,
|
|
1031
|
+
layer: Layer.succeed(
|
|
1032
|
+
UserRepo,
|
|
1033
|
+
UserRepo.of({
|
|
1034
|
+
findById: Effect.fn('TestUserRepo.findById')(function* (
|
|
1035
|
+
id: string
|
|
1036
|
+
) {
|
|
1037
|
+
if (id === user.id) return user;
|
|
1038
|
+
return yield* Effect.die(
|
|
1039
|
+
new Error(`Unknown test user: ${id}`)
|
|
1040
|
+
);
|
|
1041
|
+
})
|
|
1042
|
+
})
|
|
1043
|
+
)
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Usage in test:
|
|
1048
|
+
const { user, layer } = fakeUserRepo();
|
|
1049
|
+
// assert against `user` values, provide `layer`
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
> Returning test data alongside the layer avoids duplicating constants between test setup and assertions.
|
|
1053
|
+
|
|
1054
|
+
## Lifecycle State-Machine Fakes
|
|
1055
|
+
|
|
1056
|
+
For long-lived services such as connection managers, runners, registries, or background sync loops, prefer explicit controllable test doubles over broad end-to-end flows when transport-level correctness is not the thing under test.
|
|
1057
|
+
|
|
1058
|
+
Model the fake as a small state machine with semantic controls:
|
|
1059
|
+
|
|
1060
|
+
- counters for call counts
|
|
1061
|
+
- explicit transition methods like `disconnect()`, `reloadTools()`, `failNext()`
|
|
1062
|
+
- `Deferred` values for blocking and release points
|
|
1063
|
+
- direct assertions against cache/status transitions after each step
|
|
1064
|
+
|
|
1065
|
+
This keeps race and lifecycle tests fast, deterministic, and reviewable.
|
|
1066
|
+
|
|
1067
|
+
## Instance-Scoped Harness Tests
|
|
1068
|
+
|
|
1069
|
+
When testing tools or services that depend on instance-local context or `InstanceState`, prefer the real layer graph and a real instance/test harness over ad hoc module wrappers.
|
|
1070
|
+
|
|
1071
|
+
```typescript
|
|
1072
|
+
const layer = Layer.mergeAll(
|
|
1073
|
+
MyTool.defaultLayer,
|
|
1074
|
+
Instruction.defaultLayer,
|
|
1075
|
+
OtherDependency.defaultLayer
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
it.live('runs with the real harness', () =>
|
|
1079
|
+
withTestInstance((dir) =>
|
|
1080
|
+
Effect.gen(function* () {
|
|
1081
|
+
const tool = yield* MyTool.Service;
|
|
1082
|
+
yield* tool.run(dir);
|
|
1083
|
+
})
|
|
1084
|
+
).pipe(Effect.provide(layer))
|
|
1085
|
+
);
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
Use fake services when you are isolating pure domain behavior. Use the real harness when correctness depends on instance context, layer composition, or production orchestration.
|
|
1089
|
+
|
|
1090
|
+
## Interrupt Tests Should Prove Cleanup
|
|
1091
|
+
|
|
1092
|
+
If interruption is part of the contract, do not stop at asserting that the fiber was interrupted. Assert the cleanup effect too:
|
|
1093
|
+
|
|
1094
|
+
- busy/idle status reset
|
|
1095
|
+
- pending work marked aborted/cancelled
|
|
1096
|
+
- finalizers or teardown callbacks ran
|
|
1097
|
+
- replacement work can start without a second manual cleanup call
|
|
1098
|
+
|
|
1099
|
+
## Testing with Non-Vitest Runners (bun:test)
|
|
1100
|
+
|
|
1101
|
+
When using `bun:test` or other non-vitest runners, `@effect/vitest`'s `it.effect`, `it.live`, and `layer()` helpers are unavailable. Build a custom test harness that replicates the same semantics:
|
|
1102
|
+
|
|
1103
|
+
```typescript
|
|
1104
|
+
import { Effect, Layer } from 'effect';
|
|
1105
|
+
import { TestClock, TestConsole } from 'effect/testing';
|
|
1106
|
+
import { describe, it } from 'bun:test';
|
|
1107
|
+
|
|
1108
|
+
// Two layer stacks: one with TestClock, one without
|
|
1109
|
+
const testEnv = Layer.mergeAll(TestConsole.layer, TestClock.layer());
|
|
1110
|
+
const liveEnv = TestConsole.layer;
|
|
1111
|
+
|
|
1112
|
+
export const testEffect = <R, E>(layer: Layer.Layer<R, E>) => {
|
|
1113
|
+
const testLayer = Layer.provideMerge(layer, testEnv);
|
|
1114
|
+
const liveLayer = Layer.provideMerge(layer, liveEnv);
|
|
1115
|
+
|
|
1116
|
+
return {
|
|
1117
|
+
effect: (name: string, fn: () => Effect.Effect<void, unknown, R>) =>
|
|
1118
|
+
it(name, () =>
|
|
1119
|
+
Effect.runPromise(fn().pipe(Effect.provide(testLayer)))
|
|
1120
|
+
),
|
|
1121
|
+
|
|
1122
|
+
live: (name: string, fn: () => Effect.Effect<void, unknown, R>) =>
|
|
1123
|
+
it(name, () =>
|
|
1124
|
+
Effect.runPromise(fn().pipe(Effect.provide(liveLayer)))
|
|
1125
|
+
)
|
|
1126
|
+
};
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
// Usage:
|
|
1130
|
+
const deps = Layer.mergeAll(MyService.defaultLayer, OtherService.defaultLayer);
|
|
1131
|
+
const it = testEffect(deps);
|
|
1132
|
+
|
|
1133
|
+
describe('MyService', () => {
|
|
1134
|
+
it.live('handles request', () =>
|
|
1135
|
+
Effect.gen(function* () {
|
|
1136
|
+
const svc = yield* MyService.Service;
|
|
1137
|
+
const result = yield* svc.handle('input');
|
|
1138
|
+
expect(result).toBe('expected');
|
|
1139
|
+
})
|
|
1140
|
+
);
|
|
1141
|
+
});
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
Key differences from `@effect/vitest`:
|
|
1145
|
+
|
|
1146
|
+
- Use `Effect.runPromise` manually — bun:test expects `Promise<void>` from async tests
|
|
1147
|
+
- Layer composition uses `Layer.provideMerge` here because the harness intentionally exposes both application and test services; do not use it blindly
|
|
1148
|
+
- Keep `effect` as the default; use `live` only when real time or live runtime services are under test
|
|
1149
|
+
|
|
1150
|
+
**`TestClock` without an ambient `Scope` (beta.70):** earlier betas required a surrounding `Scope` for `TestClock.adjust` to advance time. As of beta.70, `TestClock.layer()` works when provided directly to a program run with `Effect.runPromise` (no ambient `Scope`) — which is exactly what the harness above relies on. `testEffect(...).effect(...)` merges `TestClock.layer()` into the layer stack and runs with `Effect.runPromise`, and `TestClock.adjust` still drives time correctly.
|
|
1151
|
+
|
|
1152
|
+
## Testing Checklist
|
|
1153
|
+
|
|
1154
|
+
Before completing a testing task, verify:
|
|
1155
|
+
|
|
1156
|
+
- [ ] Correct framework chosen (@effect/vitest vs vitest vs bun:test harness)
|
|
1157
|
+
- [ ] Test variant appropriate — `it.effect` by default; `it.live` only for explicitly live runtime behavior
|
|
1158
|
+
- [ ] Services provided via layers when needed
|
|
1159
|
+
- [ ] HTTP-speaking services tested with HTTP mock server, not service fakes
|
|
1160
|
+
- [ ] TestClock used only for tests that explicitly need time simulation
|
|
1161
|
+
- [ ] Errors tested with Effect.flip or Effect.exit
|
|
1162
|
+
- [ ] Edge cases covered
|
|
1163
|
+
- [ ] Property-based tests for general properties
|
|
1164
|
+
- [ ] Tests are deterministic (no polling/setTimeout — use Deferred-based synchronization)
|
|
1165
|
+
- [ ] Interrupt tests assert resulting cleanup state, not just interruption itself
|
|
1166
|
+
- [ ] Test names describe behavior clearly
|
|
1167
|
+
- [ ] Resources properly scoped and cleaned up
|
|
1168
|
+
- [ ] All tests pass
|
|
1169
|
+
|
|
1170
|
+
## Common Pitfalls
|
|
1171
|
+
|
|
1172
|
+
### Assertion Style
|
|
1173
|
+
|
|
1174
|
+
Effect v4 canonically uses `import { assert } from "@effect/vitest"` with methods like `assert.deepStrictEqual`, `assert.strictEqual`, and `assert.isTrue`. The `expect` API from vitest is still available and works fine. Pick one style and stay consistent within a test file.
|
|
1175
|
+
|
|
1176
|
+
```typescript
|
|
1177
|
+
// ✅ Option A - assert style (canonical v4)
|
|
1178
|
+
import { it, assert } from '@effect/vitest';
|
|
1179
|
+
import { Effect } from 'effect';
|
|
1180
|
+
|
|
1181
|
+
declare const result: unknown;
|
|
1182
|
+
declare const expected: unknown;
|
|
1183
|
+
|
|
1184
|
+
it.effect('test', () =>
|
|
1185
|
+
Effect.gen(function* () {
|
|
1186
|
+
assert.strictEqual(result, expected);
|
|
1187
|
+
assert.deepStrictEqual(result, { id: '123' });
|
|
1188
|
+
assert.isTrue(true);
|
|
1189
|
+
})
|
|
1190
|
+
);
|
|
1191
|
+
|
|
1192
|
+
// ✅ Option B - expect style (still works)
|
|
1193
|
+
import { it, expect } from '@effect/vitest';
|
|
1194
|
+
|
|
1195
|
+
it.effect('test', () =>
|
|
1196
|
+
Effect.gen(function* () {
|
|
1197
|
+
expect(result).toBe(expected);
|
|
1198
|
+
})
|
|
1199
|
+
);
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
### Don't Forget to Fork for TestClock
|
|
1203
|
+
|
|
1204
|
+
```typescript
|
|
1205
|
+
import { it } from '@effect/vitest';
|
|
1206
|
+
import { Effect, Fiber } from 'effect';
|
|
1207
|
+
import { TestClock } from 'effect/testing';
|
|
1208
|
+
|
|
1209
|
+
// ❌ Wrong - will hang waiting for real time
|
|
1210
|
+
it.effect('test', () =>
|
|
1211
|
+
Effect.gen(function* () {
|
|
1212
|
+
yield* Effect.sleep('5 seconds'); // Blocks!
|
|
1213
|
+
yield* TestClock.adjust('5 seconds');
|
|
1214
|
+
})
|
|
1215
|
+
);
|
|
1216
|
+
|
|
1217
|
+
// ✅ Correct - fork the effect
|
|
1218
|
+
it.effect('test', () =>
|
|
1219
|
+
Effect.gen(function* () {
|
|
1220
|
+
const fiber = yield* Effect.forkChild(Effect.sleep('5 seconds'));
|
|
1221
|
+
yield* TestClock.adjust('5 seconds');
|
|
1222
|
+
yield* Fiber.join(fiber);
|
|
1223
|
+
})
|
|
1224
|
+
);
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
### Provide Layers to Effect, Not Test
|
|
1228
|
+
|
|
1229
|
+
```typescript
|
|
1230
|
+
import { it, expect } from '@effect/vitest';
|
|
1231
|
+
import { Effect, Layer } from 'effect';
|
|
1232
|
+
|
|
1233
|
+
declare const someEffect: Effect.Effect<number>;
|
|
1234
|
+
declare const expected: number;
|
|
1235
|
+
declare const layer: Layer.Layer<never>;
|
|
1236
|
+
|
|
1237
|
+
// ❌ Wrong - providing to wrong level
|
|
1238
|
+
it.effect('test', () =>
|
|
1239
|
+
Effect.gen(function* () {
|
|
1240
|
+
const result = yield* someEffect;
|
|
1241
|
+
expect(result).toBe(expected);
|
|
1242
|
+
})
|
|
1243
|
+
); // ❌ Can't provide to test function
|
|
1244
|
+
// .pipe(Effect.provide(layer))
|
|
1245
|
+
|
|
1246
|
+
// ✅ Correct - provide to Effect
|
|
1247
|
+
it.effect(
|
|
1248
|
+
'test',
|
|
1249
|
+
() =>
|
|
1250
|
+
Effect.gen(function* () {
|
|
1251
|
+
const result = yield* someEffect;
|
|
1252
|
+
expect(result).toBe(expected);
|
|
1253
|
+
}).pipe(Effect.provide(layer)) // ✅ Provide to Effect
|
|
1254
|
+
);
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
## Running Tests
|
|
1258
|
+
|
|
1259
|
+
```bash
|
|
1260
|
+
# Run all tests
|
|
1261
|
+
bun run test
|
|
1262
|
+
|
|
1263
|
+
# Run specific file
|
|
1264
|
+
bunx vitest run path/to/file.test.ts
|
|
1265
|
+
|
|
1266
|
+
# Full check (format + lint + typecheck + test)
|
|
1267
|
+
bun run check && bun run test
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
## Example: Complete Test Suite
|
|
1271
|
+
|
|
1272
|
+
```typescript
|
|
1273
|
+
import { describe, expect, it, layer } from '@effect/vitest';
|
|
1274
|
+
import { Effect, Context, Layer, Exit } from 'effect';
|
|
1275
|
+
|
|
1276
|
+
// Service definition
|
|
1277
|
+
class Counter extends Context.Service<
|
|
1278
|
+
Counter,
|
|
1279
|
+
{
|
|
1280
|
+
increment: () => Effect.Effect<void>;
|
|
1281
|
+
value: () => Effect.Effect<number>;
|
|
1282
|
+
}
|
|
1283
|
+
>()('Counter') {
|
|
1284
|
+
static Live = Layer.effect(
|
|
1285
|
+
Counter,
|
|
1286
|
+
Effect.gen(function* () {
|
|
1287
|
+
let count = 0;
|
|
1288
|
+
return {
|
|
1289
|
+
increment: () =>
|
|
1290
|
+
Effect.sync(() => {
|
|
1291
|
+
count++;
|
|
1292
|
+
}),
|
|
1293
|
+
value: () => Effect.succeed(count)
|
|
1294
|
+
};
|
|
1295
|
+
})
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// Tests
|
|
1300
|
+
layer(Counter.Live)('Counter', (it) => {
|
|
1301
|
+
it.effect('should start at 0', () =>
|
|
1302
|
+
Effect.gen(function* () {
|
|
1303
|
+
const counter = yield* Counter;
|
|
1304
|
+
const value = yield* counter.value();
|
|
1305
|
+
expect(value).toBe(0);
|
|
1306
|
+
})
|
|
1307
|
+
);
|
|
1308
|
+
|
|
1309
|
+
it.effect('should increment', () =>
|
|
1310
|
+
Effect.gen(function* () {
|
|
1311
|
+
const counter = yield* Counter;
|
|
1312
|
+
yield* counter.increment();
|
|
1313
|
+
const value = yield* counter.value();
|
|
1314
|
+
expect(value).toBe(1);
|
|
1315
|
+
})
|
|
1316
|
+
);
|
|
1317
|
+
|
|
1318
|
+
it.effect('should handle multiple increments', () =>
|
|
1319
|
+
Effect.gen(function* () {
|
|
1320
|
+
const counter = yield* Counter;
|
|
1321
|
+
yield* counter.increment();
|
|
1322
|
+
yield* counter.increment();
|
|
1323
|
+
yield* counter.increment();
|
|
1324
|
+
const value = yield* counter.value();
|
|
1325
|
+
expect(value).toBe(3);
|
|
1326
|
+
})
|
|
1327
|
+
);
|
|
1328
|
+
});
|
|
1329
|
+
```
|
|
1330
|
+
|
|
1331
|
+
This skill ensures comprehensive, reliable testing of Effect-based applications following best practices.
|