01-test-services
Effect-TS 4 testing with layers — mock services, test Refs, Layer.succeed, and test dependency injection
- License 许可
- MIT
- 最近更新
- 2026-05-31
- Stars
- ★ 0
- Forks
- 0
01 前言 Front Matter
- name
- 01-test-services
- license
- MIT
- description
- Effect-TS 4 testing with layers — mock services, test Refs, Layer.succeed, and test dependency injection
- compatibility
- opencode
03 SKILL.md 正文
Testing Services with Layers
Core pattern: mock at the Layer level
Provide a Layer.succeed or Layer.effect that replaces the real implementation.
Never reach past the layer boundary in tests.
import { Context, Effect, Layer, Ref } from "effect";
// Mock a service with a Ref for state inspection
const mockDb = Layer.effect(
Database,
Effect.gen(function* () {
const store = yield* Ref.make<Array<Row>>([]);
return Database.of({
query: Effect.fnUntraced(function* (_sql) {
return yield* Ref.get(store);
}),
insert: Effect.fnUntraced(function* (row) {
yield* Ref.update(store, (rows) => [...rows, row]);
}),
});
}),
);
Test Ref service — expose internal state to tests
export class TodoRepoTestRef extends Context.Service<TodoRepoTestRef, Ref.Ref<Array<Todo>>>()("app/TodoRepoTestRef") {
static readonly layer = Layer.effect(TodoRepoTestRef, Ref.make([]));
}
// layerTest: expose TestRef via provideMerge so tests can inspect
class TodoRepo extends Context.Service<
TodoRepo,
{
create(title: string): Effect.Effect<Todo>;
readonly list: Effect.Effect<ReadonlyArray<Todo>>;
}
>()("app/TodoRepo") {
static readonly layerTest = Layer.effect(
TodoRepo,
Effect.gen(function* () {
const store = yield* TodoRepoTestRef;
return TodoRepo.of({
create: Effect.fnUntraced(function* (title: string) {
const todos = yield* Ref.get(store);
const todo = { id: todos.length + 1, title };
yield* Ref.set(store, [...todos, todo]);
return todo;
}),
list: Ref.get(store),
});
}),
).pipe(Layer.provideMerge(TodoRepoTestRef.layer));
}
// In test: access raw store
const todos = yield * Ref.get(yield * TodoRepoTestRef);
Mocking LanguageModel (AI SDK)
The AI SDK uses Context.Service exactly like any other service:
import { LanguageModel } from "effect/unstable/ai";
const mockLmLayer = (responses: ReadonlyArray<string>) =>
Layer.effect(
LanguageModel.LanguageModel,
Effect.gen(function* () {
const idx = yield* Ref.make(0);
return {
generateText: Effect.fnUntraced(function* (_options) {
const i = yield* Ref.getAndUpdate(idx, (n) => n + 1);
return {
text: responses[Math.min(i, responses.length - 1)] ?? "",
usage: { inputTokens: 0, outputTokens: 0 },
toolCalls: [],
finishReason: "stop" as const,
};
}),
generateObject: () => Effect.die(new Error("not mocked")),
streamText: () => Effect.die(new Error("not mocked")),
} as unknown as LanguageModel.Service;
}),
);
Per-test provide — isolated
test("processes item", async () => {
await Effect.runPromise(
Effect.gen(function* () {
const svc = yield* MyService;
const result = yield* svc.doThing("input");
expect(result).toBe("expected");
}).pipe(Effect.provide(MyService.layerTest)),
);
});
Shared layer across describe block — use layer() from @effect/vitest
import { layer } from "@effect/vitest";
layer(TodoRepo.layerTest)("TodoRepo tests", (it) => {
it.effect("creates a todo", () =>
Effect.gen(function* () {
const repo = yield* TodoRepo;
yield* repo.create("Write tests");
const all = yield* repo.list;
expect(all.length).toBeGreaterThanOrEqual(1);
}),
);
});
Counting calls with Ref
test("calls LLM twice for maxDepth=0", async () => {
await Effect.runPromise(
Effect.gen(function* () {
const callCount = yield* Ref.make(0);
const layer = mockLmLayerCounting(callCount);
yield* myEffect.pipe(Effect.provide(layer));
const total = yield* Ref.get(callCount);
expect(total).toBeGreaterThanOrEqual(2);
}),
);
});
04 同仓库更多 Skills
tmux-workflow
Development workflow with tmux — run dev servers, Storybook, and background processes in persistent sessions with live logs, while keeping OpenCode free for chat
04-anti-patterns
React anti-patterns to avoid — manual JSX repetition, useEffect misuse, reference instability, prop drilling, conditional hooks, and common performance traps
03-state-management
React state management patterns — lifting state, colocation, URL state, server state with TanStack Query, local state with useState/useReducer
02-component-composition
React component composition patterns — render props, compound components, list rendering with map, slot patterns, and layout primitives
01-fp-style
React functional programming style — pure components, hooks conventions, Effect Atom integration, and Tailwind patterns
react-animation
React animation with react/motion (Framer Motion) — declarative animations, gestures, layout animations, and performance best practices