Install
$ agentstack add skill-andrueandersoncs-claude-skill-effect-ts-testing ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Testing in Effect
Overview
Effect testing uses @effect/vitest as the standard test runner integration. This package provides Effect-aware test functions that handle Effect execution, scoped resources, layer composition, and TestClock injection automatically.
The two pillars of Effect testing that enable 100% test coverage:
- Service-Oriented Architecture — Every external/effectful dependency (API calls, databases, file systems, third-party services, clocks, random number generators) MUST be wrapped in an Effect Service using
Context.Tag. Tests provide test implementations via Layers, giving you complete control over all I/O and side effects.
- Schema-Driven Property Testing — Since every data type has a Schema, every data type can generate test data via
Arbitrary. This makes property-based testing the primary approach for verifying domain logic across thousands of automatically generated inputs.
Together, these two pillars mean: services eliminate external dependencies from tests, and Arbitrary eliminates hand-crafted test data. The result is fast, deterministic, comprehensive tests with 100% coverage.
Core testing tools:
- @effect/vitest - Effect-native test runner (
it.effect,it.scoped,it.live,it.layer,it.prop) - Effect Services + Test Layers - Replace ALL external dependencies with test doubles via
Context.Tagandit.layer - Schema.Arbitrary - Generate test data from any Schema (primary approach — never hand-craft test data)
- Property Testing - Test invariants with generated data via
it.propor fast-check - TestClock - Control time in tests (automatically provided by
it.effect)
The Service-Oriented Testing Pattern (CRITICAL)
This is the most important testing pattern in Effect. Every external or effectful operation MUST be wrapped in a Service so that tests can provide a test implementation. This is how you achieve 100% test coverage without hitting real APIs, databases, or file systems.
The Rule
> If it makes a network call, reads from disk, talks to a database, calls a third-party API, generates random values, or performs any I/O — it MUST be behind an Effect Service.
Why Services Are Required for Testing
Without services, your code is untestable because it directly depends on external systems:
// ❌ UNTESTABLE: Direct API call baked into business logic
const getUser = (id: string) =>
Effect.tryPromise({
try: () => fetch(`/api/users/${id}`).then((r) => r.json()),
catch: (error) => new NetworkError({ cause: error }),
});
// ❌ UNTESTABLE: Direct database access
const saveOrder = (order: Order) =>
Effect.tryPromise({
try: () => db.query("INSERT INTO orders ...", order),
catch: (error) => new DatabaseError({ cause: error }),
});
With services, your business logic is pure and fully testable:
// ✅ TESTABLE: Service abstraction for API calls
class UserApi extends Context.Tag("UserApi") Effect.Effect;
readonly saveUser: (user: User) => Effect.Effect;
}
>() {}
// ✅ TESTABLE: Service abstraction for database
class OrderRepository extends Context.Tag("OrderRepository") Effect.Effect;
readonly findById: (id: string) => Effect.Effect;
}
>() {}
// ✅ Business logic is pure — depends only on service interfaces
const processOrder = (orderId: string) =>
Effect.gen(function* () {
const userApi = yield* UserApi;
const orderRepo = yield* OrderRepository;
const order = yield* orderRepo.findById(orderId);
const user = yield* userApi.getUser(order.userId);
// ... pure business logic using service abstractions
});
What MUST Be a Service
Every one of these MUST be wrapped in a Context.Tag service:
| External Dependency | Service Example | | ------------------------ | -------------------------------------------------- | | REST/GraphQL API calls | UserApi, PaymentGateway, NotificationService | | Database operations | UserRepository, OrderRepository | | File system access | FileStorage, ConfigReader | | Third-party SDKs | StripeClient, SendGridClient, AwsS3Client | | Email/SMS sending | EmailService, SmsService | | Message queues | EventPublisher, QueueConsumer | | Caching systems | CacheService, RedisClient | | Authentication providers | AuthProvider, TokenService | | External clock/time | Use Effect's built-in Clock service | | Random values | Use Effect's built-in Random service |
Complete Service + Test Layer Pattern
import { Context, Effect, Layer, Schema, Arbitrary } from "effect";
import { it, expect, layer } from "@effect/vitest";
import * as fc from "fast-check";
// 1. Define schemas for domain types
const TransactionId = Schema.String.pipe(
Schema.pattern(/^txn_[a-zA-Z0-9]{16}$/),
Schema.annotations({
arbitrary: () => (fc) => fc.stringMatching(/^txn_[a-zA-Z0-9]{16}$/),
}),
);
const PaymentStatus = Schema.Literal("succeeded", "pending", "failed");
class PaymentResult extends Schema.Class("PaymentResult")({
transactionId: TransactionId,
amount: Schema.Number.pipe(Schema.positive()),
currency: Schema.Literal("usd", "eur", "gbp"),
status: PaymentStatus,
}) {}
// 2. Define the service interface
class PaymentGateway extends Context.Tag("PaymentGateway") Effect.Effect;
readonly refund: (transactionId: string) => Effect.Effect;
}
>() {}
// 3. Live implementation (used in production)
const PaymentGatewayLive = Layer.succeed(PaymentGateway, {
charge: (amount, currency) =>
Effect.tryPromise({
try: () => stripe.charges.create({ amount, currency }),
catch: (error) => new PaymentError({ cause: error }),
}),
refund: (transactionId) =>
Effect.tryPromise({
try: () => stripe.refunds.create({ charge: transactionId }),
catch: (error) => new RefundError({ cause: error }),
}),
});
// 4. Test implementation using Arbitrary — generates varied test data
const PaymentGatewayTest = Layer.effect(
PaymentGateway,
Effect.sync(() => ({
charge: (amount, currency) =>
Effect.succeed(
new PaymentResult({
transactionId: fc.sample(Arbitrary.make(TransactionId)(fc), 1)[0],
amount,
currency: currency as "usd" | "eur" | "gbp",
status: fc.sample(Arbitrary.make(PaymentStatus)(fc), 1)[0],
}),
),
refund: (_transactionId) => Effect.void,
})),
);
// 5. Property test with the test layer — 100% coverage, zero external calls
layer(PaymentGatewayTest)("PaymentService", (it) => {
it.effect.prop("should process payment for any valid amount", [Schema.Number.pipe(Schema.positive())], ([amount]) =>
Effect.gen(function* () {
const gateway = yield* PaymentGateway;
const result = yield* gateway.charge(amount, "usd");
expect(result.amount).toBe(amount);
expect(["succeeded", "pending", "failed"]).toContain(result.status);
}),
);
it.effect.prop("should handle refund for any transaction", [TransactionId], ([txnId]) =>
Effect.gen(function* () {
const gateway = yield* PaymentGateway;
yield* gateway.refund(txnId);
// No error = success
}),
);
});
Stateful Test Layers (for Repository Testing)
For services that need to maintain state across operations within a test, use Layer.effect with Ref:
import { Effect, Layer, Ref, Option } from "effect";
const OrderRepositoryTest = Layer.effect(
OrderRepository,
Effect.gen(function* () {
const store = yield* Ref.make>(new Map());
return {
save: (order: Order) => Ref.update(store, (m) => new Map(m).set(order.id, order)),
findById: (id: string) =>
Effect.gen(function* () {
const orders = yield* Ref.get(store);
return yield* Option.match(Option.fromNullable(orders.get(id)), {
onNone: () => Effect.fail(new OrderNotFound({ orderId: id })),
onSome: Effect.succeed,
}).pipe(Effect.flatten);
}),
findAll: () => Ref.get(store).pipe(Effect.map((m) => Array.from(m.values()))),
};
}),
);
Composing Multiple Test Layers
Real tests often need multiple services. Compose test layers with Layer.merge and use Arbitrary for all test data:
import { Schema, Arbitrary, Effect, Layer } from "effect";
import { it, expect, layer } from "@effect/vitest";
import * as fc from "fast-check";
// Define schemas for all domain types
const UserId = Schema.String.pipe(Schema.minLength(1));
const OrderId = Schema.String.pipe(Schema.minLength(1));
class OrderItem extends Schema.Class("OrderItem")({
productId: Schema.String,
price: Schema.Number.pipe(Schema.positive()),
quantity: Schema.Number.pipe(Schema.int(), Schema.positive()),
}) {}
class Order extends Schema.Class("Order")({
id: OrderId,
userId: UserId,
items: Schema.NonEmptyArray(OrderItem),
total: Schema.Number.pipe(Schema.positive()),
}) {}
// Compose all test layers
const TestEnv = Layer.merge(UserApiTest, Layer.merge(OrderRepositoryTest, PaymentGatewayTest));
layer(TestEnv)("Order Processing", (it) => {
it.effect.prop(
"should process complete order flow for any user and order",
[Arbitrary.make(UserId), Arbitrary.make(Order)],
([userId, order]) =>
Effect.gen(function* () {
const userApi = yield* UserApi;
const orderRepo = yield* OrderRepository;
const gateway = yield* PaymentGateway;
// Full integration test with ALL services mocked + generated data
const user = yield* userApi.getUser(userId);
yield* orderRepo.save(order);
const savedOrder = yield* orderRepo.findById(order.id);
const payment = yield* gateway.charge(savedOrder.total, "usd");
expect(payment.amount).toBe(savedOrder.total);
}),
);
});
Anti-Pattern: Hard-Coded Service Mocks in Property Tests
This is what NOT to do. The following pattern defeats the purpose of property-based testing because it uses hard-coded values and Effect.fail("Not implemented") instead of using Arbitrary to generate varied test data:
// ❌ WRONG: Hard-coded values and "Not implemented" errors
const defaultTestLayer = Layer.mergeAll(
Layer.succeed(ImporterService, {
import: (identifier: string) =>
Effect.fail(
new HalImportError({
message: `No importer configured for: ${identifier}`,
identifier,
}),
),
}),
Layer.succeed(UserWalletService, {
// ❌ Hard-coded address - every property test gets the same value
getAddress: () => Effect.succeed("0xTestWallet1234567890123456789012345678"),
}),
Layer.succeed(BlockchainClientService, {
// ❌ Hard-coded responses - no variation across test runs
call: () => Effect.succeed("0x"),
getLogs: () => Effect.succeed([]),
// ❌ "Not implemented" - these won't be tested at all
getTransactionTrace: () => Effect.fail(new Error("Not implemented")),
}),
Layer.succeed(ContractMetadataService, {
getFunctionAbi: () => Effect.fail(new Error("Not implemented")),
getEventAbi: () => Effect.fail(new Error("Not implemented")),
}),
Layer.succeed(TransactionService, {
sendTransaction: () => Effect.fail(new Error("Not implemented")),
}),
Layer.succeed(SwapService, {
executeSwap: () => Effect.fail(new Error("Not implemented")),
}),
Layer.succeed(CodeExecutionService, {
execute: () => Effect.fail(new CodeExecutionNotImplementedError()),
}),
);
Why this is wrong:
- Hard-coded values - Property tests run the same inputs every time, missing edge cases
Effect.fail(new Error("Not implemented"))- These code paths are never exercised- No Arbitrary - The whole point of property testing is generating varied data
The correct approach: Use Arbitrary inside Layer.effect to generate different values for each property test run:
import { Arbitrary, Schema, Effect, Layer, Ref } from "effect";
import * as fc from "fast-check";
// Define schemas for your service return types
const WalletAddress = Schema.String.pipe(
Schema.pattern(/^0x[a-fA-F0-9]{40}$/),
Schema.annotations({
arbitrary: () => (fc) => fc.hexaString({ minLength: 40, maxLength: 40 }).map((hex) => `0x${hex}`),
}),
);
const HexData = Schema.String.pipe(
Schema.pattern(/^0x[a-fA-F0-9]*$/),
Schema.annotations({
arbitrary: () => (fc) => fc.hexaString({ minLength: 0, maxLength: 64 }).map((hex) => `0x${hex}`),
}),
);
// ✅ CORRECT: Use Arbitrary to generate test data in the layer
const PropertyTestLayer = Layer.effect(
UserWalletService,
Effect.sync(() => {
// Generate a random address for THIS test run
const address = fc.sample(Arbitrary.make(WalletAddress)(fc), 1)[0];
return {
getAddress: () => Effect.succeed(address),
};
}),
);
// ✅ CORRECT: For stateful services, combine Ref with Arbitrary
const BlockchainClientTestLayer = Layer.effect(
BlockchainClientService,
Effect.gen(function* () {
// Pre-generate test data for this test run
const callResultArb = Arbitrary.make(HexData)(fc);
const logsArb = Arbitrary.make(Schema.Array(EventLog))(fc);
return {
call: () => Effect.succeed(fc.sample(callResultArb, 1)[0]),
getLogs: () => Effect.succeed(fc.sample(logsArb, 1)[0]),
// ✅ Generate valid trace data instead of failing
getTransactionTrace: () => Effect.succeed(fc.sample(Arbitrary.make(TransactionTrace)(fc), 1)[0]),
};
}),
);
// ✅ CORRECT: Full test layer with Arbitrary-generated data
const FullPropertyTestLayer = Layer.mergeAll(
PropertyTestLayer,
BlockchainClientTestLayer,
// For services you actually want to test failure cases,
// use Arbitrary to generate the ERROR data too:
Layer.effect(
ImporterService,
Effect.sync(() => ({
import: (identifier: string) =>
// ✅ Either succeed with generated data OR fail with generated error
fc.sample(fc.boolean(), 1)[0]
? Effect.succeed(fc.sample(Arbitrary.make(ImportResult)(fc), 1)[0])
: Effect.fail(fc.sample(Arbitrary.make(HalImportError)(fc), 1)[0]),
})),
),
);
Key principles:
- Every service method should return Arbitrary-generated data - Not hard-coded strings
- Generate errors with Arbitrary too - Error schemas should produce varied error cases
- Use
Layer.effect+Effect.sync- So each test run gets fresh generated values - If a method "shouldn't be called" - Either generate valid data anyway, or use a schema-based error
Combining Services with Property Testing
The ultimate testing pattern: service test layers + Schema Arbitrary. This lets you test business logic across thousands of generated inputs with all external dependencies controlled:
import { it, expect, layer } from "@effect/vitest";
import { Schema, Arbitrary, Effect } from "effect";
layer(TestEnv)("Order Processing Properties", (it) => {
it.effect.prop("should calculate correct total for any valid order", [Arbitrary.make(Order)], ([order]) =>
Effect.gen(function* () {
const orderRepo = yield* OrderRepository;
yield* orderRepo.save(order);
const saved = yield* orderRepo.findById(order.id);
expect(saved.total).toBe(order.items.reduce((sum, i) => sum + i.price, 0));
}),
);
it.effect.prop("should never charge negative amounts", [Arbitrary.make(Order)], ([order]) =>
Effect.gen(function* () {
const gateway = yield* PaymentGateway;
const result = yield* gateway.charge(order.total, "usd");
expect(result.amount).toBeGreaterThanOrEqual(0);
}),
);
});
Setup
Install @effect/vitest alongside vitest (v1.6
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: andrueandersoncs
- Source: andrueandersoncs/claude-skill-effect-ts
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.