/** * Provides helpers for testing Schema behavior. * * These utilities assert how schemas construct values, decode input, encode * output, generate arbitrary values, and round-trip between encoded and decoded * forms. The `Asserts` class groups the common checks for one schema, while * `Decoding` and `Encoding` can be used directly when a test only needs one * direction. * * Methods with an `Effect` suffix return lazy assertions that use the calling * fiber's services. The Promise-returning methods can be used with any test * runner. * * @stability unstable * @since 4.0.0 */ import * as assert from "node:assert" import * as Arbitrary from "../Arbitrary.ts" import * as Cause from "../Cause.ts" import type * as Context from "../Context.ts" import * as Effect from "../Effect.ts" import * as Exit from "../Exit.ts" import { pipe } from "../Function.ts" import * as InternalSchemaCause from "../internal/schema/cause.ts" import * as Result from "../Result.ts" import * as Schema from "../Schema.ts" import * as SchemaAST from "../SchemaAST.ts" import * as SchemaIssue from "../SchemaIssue.ts" import * as SchemaParser from "../SchemaParser.ts" function assertPropertyPassed(result: Arbitrary.CheckResult): void { const failure = Arbitrary.formatCheckFailure(result) if (failure !== undefined) assert.fail(failure) } const schemaResult = Effect.fnUntraced(function*( effect: Effect.Effect ): Effect.fn.Return, never, R> { const exit = yield* Effect.exit(effect) if (Exit.isSuccess(exit)) { return Result.succeed(exit.value) } const issue = InternalSchemaCause.getSchemaIssue(exit.cause) if (issue !== undefined) { return Result.fail(SchemaIssue.defaultFormatter(issue)) } return yield* Effect.failCause(Cause.fromReasons(exit.cause.reasons.map((reason) => Cause.isFailReason(reason) ? Cause.makeDieReason(reason.error).annotate(Cause.reasonAnnotations(reason)) : reason ))) }) const assertResult = Effect.fnUntraced(function*( parse: () => Effect.Effect, expected: Result.Result ) { const result = yield* schemaResult(parse()) assert.deepStrictEqual(result, expected) }) /** * Provides schema test assertions for decoding, encoding, make, arbitrary generation, and round-trip verification. * * **When to use** * * Use when writing schema unit tests for decoding, encoding, construction, property-based round-trip, or generation behavior. * * **Example** (Decoding and encoding a struct) * * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const schema = Schema.Struct({ name: Schema.String }) * const asserts = new TestSchema.Asserts(schema) * * // decoding * await asserts.decoding().succeed({ name: "Alice" }) // => undefined * * // encoding * await asserts.encoding().succeed({ name: "Alice" }) // => undefined * ``` * * @see {@link Decoding} * @see {@link Encoding} * @stability unstable * @category testing * @since 4.0.0 */ export class Asserts { /** * Static helpers for comparing schema AST structures. * * **When to use** * * Use to assert that two schema field or tuple element definitions produce * the same AST structure. * * **Details** * * `ast.fields.equals(a, b)` compares struct field ASTs via `assert.deepStrictEqual`. `ast.elements.equals(a, b)` compares tuple element ASTs via `assert.deepStrictEqual`. * * **Example** (Comparing struct fields) * * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const fieldsA = { name: Schema.String } * const fieldsB = { name: Schema.String } * TestSchema.Asserts.ast.fields.equals(fieldsA, fieldsB) // => undefined * ``` * * @stability unstable */ static ast = { /** * Groups schema field comparisons. * * @stability unstable */ fields: { /** * Asserts that schema fields have equal AST structures. * * @stability unstable */ equals: (a: Schema.Struct.Fields, b: Schema.Struct.Fields) => { assert.deepStrictEqual( Object.fromEntries(Reflect.ownKeys(a).map((key) => [key, SchemaAST.getAST(a[key])])), Object.fromEntries(Reflect.ownKeys(b).map((key) => [key, SchemaAST.getAST(b[key])])) ) } }, /** * Groups tuple element comparisons. * * @stability unstable */ elements: { /** * Asserts that tuple elements have equal AST structures. * * @stability unstable */ equals: (a: Schema.Tuple.Elements, b: Schema.Tuple.Elements) => { assert.deepStrictEqual(a.map(SchemaAST.getAST), b.map(SchemaAST.getAST)) } } } as const /** * Schema used by these assertions. * * @stability unstable */ readonly schema: S /** * Creates assertions for a schema. * * @stability unstable */ constructor(schema: S) { this.schema = schema } /** * Returns Promise and Effect assertions for the schema's `make` operation. * * **When to use** * * Use to assert how `Schema.make` accepts, transforms, or rejects * construction input for this schema. * * **Details** * * `succeed(input)` asserts make returns the input unchanged. `succeed(input, expected)` asserts make returns `expected`. `fail(input, message)` asserts make fails with `message`. * `succeedEffect` and `failEffect` return lazy effects with the same assertions. * Assertion mismatches are defects in the returned effects. * * **Example** (Testing make) * * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const schema = Schema.String * const asserts = new TestSchema.Asserts(schema) * await asserts.make().succeed("hello") // => undefined * ``` * * @see {@link decoding} for assertions against decoded input * @see {@link encoding} for assertions against encoded output * @stability unstable */ make(options?: Schema.MakeOptions) { const makeEffect = SchemaParser.makeEffect(this.schema) function succeedEffect(input: S["Type"]): Effect.Effect function succeedEffect(input: S["~type.make.in"], expected: S["Type"]): Effect.Effect function succeedEffect(input: S["~type.make.in"], expected?: S["Type"]): Effect.Effect { return assertResult( () => makeEffect(input, options), Result.succeed(arguments.length === 1 ? input : expected) ) } async function succeed(input: S["Type"]): Promise async function succeed(input: S["~type.make.in"], expected: S["Type"]): Promise async function succeed(input: S["~type.make.in"], expected?: S["Type"]) { return Effect.runPromise(succeedEffect(input, arguments.length === 1 ? input : expected)) } function failEffect(input: unknown, message: string): Effect.Effect { return assertResult(() => makeEffect(input, options), Result.fail(message)) } return { /** * Asserts that construction succeeds with the input or the explicit expected value. * * @stability unstable */ succeed, /** * Returns a lazy construction success assertion whose mismatches are defects. * * @stability unstable */ succeedEffect, /** * Returns a lazy assertion that construction fails with the expected issue message. * * @stability unstable */ failEffect, /** * Asserts that construction fails with the expected issue message. * * @stability unstable */ async fail(input: unknown, message: string) { return Effect.runPromise(failEffect(input, message)) } } } /** * Runs a property-based test that encodes arbitrary values and then decodes them, asserting the decoded value is equivalent to the original. * * **When to use** * * Use to verify that generated schema values survive an encode-then-decode * round trip. * * **Details** * * The native Schema arbitrary generates values matching the schema's `Type`. The assertion fails with the minimized * shrunk input and replay token if any generated value does not round-trip. * * Uses {@link Schema.toEquivalence}, including custom equivalence annotations, * to compare each decoded value with the original. * * **Example** (Verifying round trips) * * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const asserts = new TestSchema.Asserts(Schema.String) * await asserts.verifyRoundTrip({ seed: 1, runs: 20 }) // => undefined * ``` * * @see {@link arbitrary} for checking that generated values satisfy the schema * @see {@link verifyRoundTripEffect} for the Effect variant * @stability unstable */ verifyRoundTrip>( this: Asserts, options?: Arbitrary.CheckOptions ): Promise { return Effect.runPromise(this.verifyRoundTripEffect(options)) } /** * Returns a lazy property-based assertion that generated values survive an * encode-then-decode round trip. * * **When to use** * * Use to check round trips with the calling fiber's decoding and encoding services. * * **Details** * * Uses {@link Schema.toEquivalence}, including custom equivalence annotations, * to compare each decoded value with the original. * * Assertion mismatches are defects containing the shrunk input and replay token. * Schema defects and interruption propagate without becoming assertion failures. * * **Example** (Composing round-trip assertions) * * ```ts import.meta.vitest * import { Effect, Schema } from "effect" * import { TestSchema } from "effect/testing" * * const asserts = new TestSchema.Asserts(Schema.NumberFromString) * const test = Effect.gen(function*() { * yield* asserts.decoding().succeedEffect("42", 42) * yield* asserts.verifyRoundTripEffect({ seed: 1, runs: 20 }) * }) * await Effect.runPromise(test) // => undefined * ``` * * @see {@link verifyRoundTrip} for the Promise variant * @stability unstable */ verifyRoundTripEffect( options?: Arbitrary.CheckOptions ): Effect.Effect { return Effect.suspend(() => { const decodeUnknownEffect = SchemaParser.decodeUnknownEffect(this.schema) const encodeEffect = SchemaParser.encodeEffect(this.schema) const equivalence = Schema.toEquivalence(this.schema as unknown as Schema.Schema) const arbitrary = Arbitrary.schema(this.schema) return Arbitrary.checkEffect( arbitrary, (value) => Effect.suspend(() => encodeEffect(value)).pipe( Effect.flatMapEager((encoded) => decodeUnknownEffect(encoded)), schemaResult, Effect.mapEager((result) => Result.isSuccess(result) && equivalence(result.success, value)) ), options ).pipe(Effect.mapEager(assertPropertyPassed)) }) } /** * Returns a {@link Decoding} instance for this schema with helpers for decoding assertions. * * **When to use** * * Use to test how unknown input is decoded into the schema's type. * * **Details** * * Pass `parseOptions` to control error reporting, for example `{ errors: "all" }`. * * **Example** (Decoding assertions) * * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const asserts = new TestSchema.Asserts(Schema.NumberFromString) * const decoding = asserts.decoding() * await decoding.succeed("42", 42) // => undefined * await decoding.fail(null, "Expected string") // => undefined * ``` * * @see {@link Decoding} * @see {@link encoding} for assertions in the opposite direction * @stability unstable */ decoding(options?: { readonly parseOptions?: SchemaAST.ParseOptions | undefined }) { return new Decoding(this.schema, options) } /** * Returns an {@link Encoding} instance for this schema with helpers for encoding assertions. * * **When to use** * * Use to test how schema values are encoded into their external form. * * **Details** * * Pass `parseOptions` to control error reporting, for example `{ errors: "all" }`. * * **Example** (Encoding assertions) * * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const asserts = new TestSchema.Asserts(Schema.NumberFromString) * const encoding = asserts.encoding() * await encoding.succeed(42, "42") // => undefined * ``` * * @see {@link Encoding} * @see {@link decoding} for assertions in the opposite direction * @stability unstable */ encoding(options?: { readonly parseOptions?: SchemaAST.ParseOptions | undefined }) { return new Encoding(this.schema, options) } /** * Returns an object with property-based testing helpers for the schema's arbitrary generator. * * **When to use** * * Use to verify that arbitrary values generated for this schema satisfy the * schema's predicate. * * **Details** * * `verifyGeneration()` generates arbitrary values and asserts each value satisfies the schema's `is` predicate. It * defaults to 20 runs. Native checking options can control generation, shrinking, bounded discards, and replay. * * **Example** (Verifying arbitrary generation) * * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const asserts = new TestSchema.Asserts(Schema.String) * asserts.arbitrary().verifyGeneration({ seed: 1, runs: 20 }) // => undefined * ``` * * @see {@link verifyRoundTrip} for property-based round-trip checks * @stability unstable */ arbitrary>(this: Asserts) { const schema = this.schema return { /** * Asserts that generated values satisfy the schema's predicate. * * @stability unstable */ verifyGeneration(options?: Arbitrary.CheckOptions): void { const is = Schema.is(schema) const arbitrary = Arbitrary.schema(schema) assertPropertyPassed(Effect.runSync(Arbitrary.checkEffect(arbitrary, is, { runs: 20, ...options }))) } } } } /** * Provides decoding test assertions through `succeed` and `fail` methods that run the schema's decoder and compare the result. * * **When to use** * * Use when you want to assert that specific inputs decode to expected values, invalid inputs produce specific error messages, or schemas receive required decoding services. * * **Details** * * `succeed` and `fail` return Promises. `succeedEffect` and `failEffect` return lazy * effects that preserve required decoding services. All assertions use * `assert.deepStrictEqual` internally. With one argument, success assertions * compare the output to the input; with two, they compare it to `expected`. * Failure assertions compare the formatted schema issue to `message`. * `provide(key, impl)` returns a new `Decoding` with the service injected. * * **Example** (Decoding with service provision) * * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const asserts = new TestSchema.Asserts(Schema.String) * const decoding = asserts.decoding() * await decoding.succeed("hello") // => undefined * ``` * * @see {@link Asserts} * @see {@link Encoding} * @stability unstable * @category testing * @since 4.0.0 */ export class Decoding { /** * Schema used by these decoding assertions. * * @stability unstable */ readonly schema: S /** * Decoder used by these assertions, preserving schema issues and required services. * * @stability unstable */ readonly decodeUnknownEffect: ( input: unknown, options?: SchemaAST.ParseOptions ) => Effect.Effect /** * Parsing options used by these assertions. * * @stability unstable */ readonly options?: { readonly parseOptions?: SchemaAST.ParseOptions | undefined } | undefined /** * Creates decoding assertions for a schema and optional parsing options. * * @stability unstable */ constructor(schema: S, options?: { readonly parseOptions?: SchemaAST.ParseOptions | undefined }) { this.schema = schema this.decodeUnknownEffect = SchemaParser.decodeUnknownEffect(schema) this.options = options } /** * Asserts that decoding `input` succeeds. With one argument, asserts the * output equals the input. With two arguments, asserts the output equals * `expected`. * * **When to use** * * Use to verify successful decoding for one input case. * * **Example** (Testing identity and transformed decoding) * * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const decoding = new TestSchema.Asserts(Schema.NumberFromString).decoding() * await decoding.succeed("1", 1) // => undefined * ``` * * @see {@link succeedEffect} for the Effect variant * @see {@link fail} for asserting decoding failures * @stability unstable */ async succeed>( this: Decoding, input: unknown ): Promise async succeed>( this: Decoding, input: unknown, expected: S["Type"] ): Promise async succeed>( this: Decoding, input: unknown, expected?: S["Type"] ) { return Effect.runPromise(this.succeedEffect(input, arguments.length === 1 ? input : expected)) } /** * Returns a lazy assertion that decoding succeeds with the input or the * explicit expected value. * * **When to use** * * Use to assert decoding in an Effect test while preserving required decoding services. * * **Details** * * Assertion mismatches are defects. The decoder uses the calling fiber's services. * * @see {@link succeed} for the Promise variant * @see {@link failEffect} for asserting decoding failures * @stability unstable */ succeedEffect(input: unknown): Effect.Effect succeedEffect(input: unknown, expected: S["Type"]): Effect.Effect succeedEffect(input: unknown, expected?: S["Type"]): Effect.Effect { return assertResult( () => this.decodeUnknownEffect(input, this.options?.parseOptions), Result.succeed(arguments.length === 1 ? input : expected) ) } /** * Asserts that decoding `input` fails and the stringified issue equals * `message`. * * **When to use** * * Use to verify that invalid decoding input produces the expected issue text. * * **Example** (Asserting a decoding failure) * * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const decoding = new TestSchema.Asserts(Schema.String).decoding() * await decoding.fail(42, "Expected string") // => undefined * ``` * * @see {@link failEffect} for the Effect variant * @see {@link succeed} for asserting successful decoding * @stability unstable */ async fail>( this: Decoding, input: unknown, message: string ) { return Effect.runPromise(this.failEffect(input, message)) } /** * Returns a lazy assertion that decoding fails with the expected issue message. * * **Details** * * Assertion mismatches are defects. Schema defects and interruption propagate * without satisfying the assertion. Required decoding services come from the * calling fiber. * * @see {@link fail} for the Promise variant * @see {@link succeedEffect} for asserting decoding success * @stability unstable */ failEffect(input: unknown, message: string): Effect.Effect { return assertResult(() => this.decodeUnknownEffect(input, this.options?.parseOptions), Result.fail(message)) } /** * Returns a new {@link Decoding} instance with the given service injected into the decoding effect context. * * **When to use** * * Use when the schema's decoder requires a service dependency. * * @see {@link Encoding.provide} * @stability unstable */ provide( service: Context.Key, implementation: Service ): Decoding>> { return new Decoding( pipe(this.schema, Schema.middlewareDecoding(Effect.provideService(service, implementation))), this.options ) } } /** * Provides encoding test assertions through `succeed` and `fail` methods that run the schema's encoder and compare the result. * * **When to use** * * Use when you want to assert that specific values encode to expected outputs, invalid inputs produce specific error messages, or schemas receive required encoding services. * * **Details** * * `succeed` and `fail` return Promises. `succeedEffect` and `failEffect` return lazy * effects that preserve required encoding services. All assertions use * `assert.deepStrictEqual` internally. With one argument, success assertions * compare the output to the input; with two, they compare it to `expected`. * Failure assertions compare the formatted schema issue to `message`. * `provide(key, impl)` returns a new `Encoding` with the service injected. * * **Example** (Encoding assertions) * * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const encoding = new TestSchema.Asserts(Schema.NumberFromString).encoding() * await encoding.succeed(42, "42") // => undefined * ``` * * @see {@link Asserts} * @see {@link Decoding} * * @stability unstable * @category testing * @since 4.0.0 */ export class Encoding { /** * Schema used by these encoding assertions. * * @stability unstable */ readonly schema: S /** * Encoder used by these assertions, preserving schema issues and required services. * * @stability unstable */ readonly encodeUnknownEffect: ( input: unknown, options?: SchemaAST.ParseOptions ) => Effect.Effect /** * Parsing options used by these assertions. * * @stability unstable */ readonly options?: { readonly parseOptions?: SchemaAST.ParseOptions | undefined } | undefined /** * Creates encoding assertions for a schema and optional parsing options. * * @stability unstable */ constructor(schema: S, options?: { readonly parseOptions?: SchemaAST.ParseOptions | undefined }) { this.schema = schema this.encodeUnknownEffect = SchemaParser.encodeUnknownEffect(schema) this.options = options } /** * Asserts that encoding `input` succeeds. With one argument, asserts the * output equals the input. With two arguments, asserts the output equals * `expected`. * * **When to use** * * Use to verify successful encoding for one input case. * * **Example** (Testing identity and transformed encoding) * * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const encoding = new TestSchema.Asserts(Schema.NumberFromString).encoding() * await encoding.succeed(1, "1") // => undefined * ``` * * @see {@link succeedEffect} for the Effect variant * @see {@link fail} for asserting encoding failures * @stability unstable */ async succeed>( this: Encoding, input: unknown ): Promise async succeed>( this: Encoding, input: unknown, expected: S["Encoded"] ): Promise async succeed>( this: Encoding, input: unknown, expected?: S["Encoded"] ) { return Effect.runPromise(this.succeedEffect(input, arguments.length === 1 ? input : expected)) } /** * Returns a lazy assertion that encoding succeeds with the input or the * explicit expected value. * * **When to use** * * Use to assert encoding in an Effect test while preserving required encoding services. * * **Details** * * Assertion mismatches are defects. The encoder uses the calling fiber's services. * * @see {@link succeed} for the Promise variant * @see {@link failEffect} for asserting encoding failures * @stability unstable */ succeedEffect(input: unknown): Effect.Effect succeedEffect(input: unknown, expected: S["Encoded"]): Effect.Effect succeedEffect(input: unknown, expected?: S["Encoded"]): Effect.Effect { return assertResult( () => this.encodeUnknownEffect(input, this.options?.parseOptions), Result.succeed(arguments.length === 1 ? input : expected) ) } /** * Asserts that encoding `input` fails and the stringified issue equals * `message`. * * **When to use** * * Use to verify that invalid encoding input produces the expected issue text. * * **Example** (Asserting an encoding failure) * * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const encoding = new TestSchema.Asserts(Schema.NumberFromString).encoding() * await encoding.fail("not-a-number", "Expected number") // => undefined * ``` * * @see {@link failEffect} for the Effect variant * @see {@link succeed} for asserting successful encoding * @stability unstable */ async fail>( this: Encoding, input: unknown, message: string ) { return Effect.runPromise(this.failEffect(input, message)) } /** * Returns a lazy assertion that encoding fails with the expected issue message. * * **Details** * * Assertion mismatches are defects. Schema defects and interruption propagate * without satisfying the assertion. Required encoding services come from the * calling fiber. * * @see {@link fail} for the Promise variant * @see {@link succeedEffect} for asserting encoding success * @stability unstable */ failEffect(input: unknown, message: string): Effect.Effect { return assertResult(() => this.encodeUnknownEffect(input, this.options?.parseOptions), Result.fail(message)) } /** * Returns a new {@link Encoding} instance with the given service injected into the encoding effect context. * * **When to use** * * Use when the schema's encoder requires a service dependency. * * @see {@link Decoding.provide} * @stability unstable */ provide( service: Context.Key, implementation: Service ): Encoding>> { return new Encoding( pipe(this.schema, Schema.middlewareEncoding(Effect.provideService(service, implementation))), this.options ) } }