Write Tests
Use this skill to produce reliable tests that satisfy this repository's quality gates and coverage requirements.
When To Use
- •Adding or changing behavior in
src/** - •Fixing bugs that need regression coverage
- •Refactoring code paths with branch-heavy logic
- •Closing line/function/branch coverage gaps
Setup Prerequisite
Do not install or configure test tooling in this skill.
If the repository is missing Vitest and @effect/vitest setup, invoke the setup-tests skill first.
Minimal preflight checks:
- •
package.jsonincludestest,test:watch, andtest:coveragescripts. - •
vitest.config.tsexists and sets coverage thresholds. - •
devDependenciesincludevitest,@effect/vitest, and@vitest/coverage-v8.
Workflow
- •Read the changed module and list all behavior branches, not just the happy path.
- •Write or update co-located tests in
src/**/*.test.ts. - •For Effect modules, import test APIs from
@effect/vitestand default toit.effect(...). - •Add failure-path assertions with
Effect.exit(...)so error outcomes are explicit. - •Use
TestClockfor all time-sensitive behavior. - •Mock API, database, and filesystem boundaries; never make real network calls.
- •Keep test names behavior-focused (
returns X when Y,fails with Z when A). - •Run
bun run testfirst, thenbun run test:coverageto close branch gaps. - •Finish by rerunning all mandatory gates (
typecheck,lint,test,test:coverage).
Commonly Difficult Patterns
Mocking Effect Services With Test Layers
Prefer Layer-based mocks for Effect services over global/module monkey patching.
- •Keep production code depending on
Context.Tagservices. - •Build a test
Layer.succeed(...)orLayer.effect(...)implementation. - •Provide the test layer only to the Effect under test.
- •Assert both returned values and interaction behavior (calls, arguments, retries).
import { describe, expect, it } from "@effect/vitest";
import { Context, Effect, Layer, Ref, Schema } from "effect";
type User = { readonly id: string; readonly email: string };
class UserRepoError extends Schema.TaggedError<UserRepoError>("UserRepoError")({
message: Schema.String,
}) {}
type UserRepoShape = {
readonly findById: (id: string) => Effect.Effect<User, UserRepoError>;
};
class UserRepo extends Context.Tag("UserRepo")<UserRepo, UserRepoShape>() {}
const getUserEmail = (id: string) =>
Effect.gen(function* () {
const repo = yield* UserRepo;
const user = yield* repo.findById(id);
return user.email;
});
describe("getUserEmail", () => {
it.effect("uses a test layer and tracks interactions", () =>
Effect.gen(function* () {
const calls = yield* Ref.make<Array<string>>([]);
const UserRepoTest = Layer.succeed(
UserRepo,
{
findById: (id: string) =>
Ref.update(calls, (existing) => [...existing, id]).pipe(
Effect.as({ id, email: "test@example.com" }),
),
} as const,
);
const email = yield* getUserEmail("u-1").pipe(Effect.provide(UserRepoTest));
expect(email).toBe("test@example.com");
const recorded = yield* Ref.get(calls);
expect(recorded).toStrictEqual(["u-1"]);
}),
);
});
Testing Failure Paths With Layer Overrides
Create a failing test layer to validate typed error handling.
const UserRepoFailing = Layer.succeed(
UserRepo,
{
findById: (id: string) =>
Effect.fail(
new UserRepoError({
message: `missing user ${id}`,
}),
),
} as const,
);
const exit = yield* Effect.exit(getUserEmail("u-404").pipe(Effect.provide(UserRepoFailing)));
expect(exit._tag).toBe("Failure");
When To Use vi.mock(...) Instead
Use vi.mock(...) for non-Effect module boundaries where dependency injection is not available.
For Effect services, prefer layer substitution because it preserves typed dependency graphs and avoids global mock leakage.
Patterns To Follow
- •Use
it.scoped(...)orit.scopedLive(...)when resource lifecycle requiresScope. - •Keep tests deterministic and isolated from wall-clock timing.
- •Use
it.live(...)only when live services are intentionally required. - •Never commit
it.effect.onlyand avoid long-livedskiptests. - •Do not exclude business-logic files from coverage.
Anti-Patterns
- •Asserting only success paths while leaving failure branches untested.
- •Using
setTimeout/real clock delays instead ofTestClock. - •Calling third-party services in unit tests.
- •Hiding type failures with
anyor assertion chains.
Example: Success And Failure
import { describe, expect, it } from "@effect/vitest";
import { Effect, Exit, TestClock } from "effect";
describe("divide", () => {
it.effect("returns quotient for valid input", () =>
Effect.gen(function* () {
const result = yield* divide(4, 2);
expect(result).toBe(2);
}),
);
it.effect("fails on zero divisor", () =>
Effect.gen(function* () {
const exit = yield* Effect.exit(divide(4, 0));
expect(exit).toStrictEqual(Exit.fail("Cannot divide by zero"));
}),
);
it.effect("uses deterministic time", () =>
Effect.gen(function* () {
yield* TestClock.adjust("1 second");
const result = yield* timeAwareComputation;
expect(result).toBe("done");
}),
);
});
Verification Checklist
- •New logic has tests for success, failure, and edge branches.
- •Tests are deterministic and isolated from external services.
- •
bun run testpasses. - •
bun run test:coveragemeets repository thresholds. - •No committed
only, long-livedskip, or flaky behavior without explicit rationale.
References
- •
setup-testsskill in.agents/skills/setup-tests/SKILL.md - •https://vitest.dev/guide/
- •https://vitest.dev/guide/coverage
- •https://effect-ts.github.io/effect/docs/vitest
- •https://effect-ts.github.io/effect/effect/Layer.ts.html
- •https://effect-ts.github.io/effect/effect/Context.ts.html
- •https://www.npmjs.com/package/@effect/vitest