Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ This import enhances the standard `it` function from `@rstest/core` with several
| `it.effect` | Runs a scoped test with test services such as `TestClock` and `TestConsole`. |
| `it.live` | Runs a scoped test with the live Effect environment. |
| `it.layer` | Shares a `Layer` between multiple tests. |
| `it.prop` | Runs property tests using Effect `Schema` values or FastCheck arbitraries. |
| `it.prop` | Runs property tests using Effect `Schema` values or `Arbitrary` inputs. |
| `it.flakyTest` | Retries an Effect that might occasionally fail until it succeeds or reaches the configured timeout. |

The package also re-exports everything from `@rstest/core` (`describe`, `expect`, `assert`, hooks, `rs`, ...), so a single import usually suffices, and provides the same assertion helpers as `@effect/vitest/utils` under `effect-rstest/utils`.
Expand Down Expand Up @@ -390,33 +390,38 @@ it.effect("retrying until success or timeout", () => it.flakyTest(flaky, "5 seco

## Property Testing with `it.prop`

`it.prop`, `it.effect.prop` and `it.live.prop` run property tests using FastCheck arbitraries (from `effect/testing/FastCheck`) or Effect `Schema` values:
`it.prop`, `it.effect.prop` and `it.live.prop` run property tests using Effect `Schema` values or arbitraries from `effect/unstable/arbitrary`:

```ts
import { assert, it } from "effect-rstest"
import { Effect, Schema } from "effect"
import { FastCheck } from "effect/testing"
import { Arbitrary } from "effect/unstable/arbitrary"

const realNumber = FastCheck.float({ noNaN: true, noDefaultInfinity: true })
const realNumber = Schema.Finite
const letter = Arbitrary.schema(Schema.Literals(["a", "b"]))

// synchronous properties
it.prop("symmetry", [realNumber, FastCheck.integer()], ([a, b]) => a + b === b + a)
it.prop("symmetry", [realNumber, Schema.Int], ([a, b]) => a + b === b + a)

// named arbitraries
// named inputs, mixing schemas and arbitraries
it.prop(
"symmetry with object",
{ a: realNumber, b: FastCheck.integer() },
({ a, b }) => a + b === b + a
"letters",
{ count: Schema.Int, text: letter },
({ count, text }) => Number.isInteger(count) && ["a", "b"].includes(text)
)

// effectful properties, with Schema-derived arbitraries
// effectful properties
it.effect.prop("schema with object", { value: Schema.Int }, ({ value }) =>
Effect.sync(() => assert.isTrue(Number.isInteger(value))))
```

All three helpers accept both tuple and record inputs, mixing schemas and FastCheck arbitraries. Schemas are converted with `Schema.toArbitrary(schema)(FastCheck)`; FastCheck arbitraries are used directly. For example, a synchronous property can use `[Schema.Literal("schema"), FastCheck.integer()]` or `{ label: Schema.Literal("schema"), count: FastCheck.integer() }`. A schema must support arbitrary generation; this does not make every possible schema generatable.
All three helpers accept tuple and record inputs. Schemas are converted with `Arbitrary.schema(schema)`; `Arbitrary` values are used directly. A schema must support arbitrary generation.

FastCheck parameters can be passed through the options argument: `{ fastCheck: { numRuns: 200 } }`.
Returning `false`, throwing, or failing the Effect (including failed assertions) falsifies the property and shrinks the input; interruption still interrupts the test. The test timeout interrupts generation, evaluation, and shrinking, and Effect finalizers run. A timeout cannot preempt a synchronous callback that never returns.

Check options are passed as `arbitrary` in the options argument: `{ arbitrary: { runs: 200, seed: "repro" } }` (see `Arbitrary.CheckOptions`).

Requires `effect` `4.0.0-rc.113` or later. Earlier releases of `effect-rstest` used `effect/testing/FastCheck`, which Effect removed in rc.113; migrate `FastCheck.*` inputs to schemas or `Arbitrary`, and `{ fastCheck: { numRuns } }` to `{ arbitrary: { runs } }`.

## Differences from `@effect/vitest`

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@
},
"peerDependencies": {
"@rstest/core": "^0.11.11",
"effect": "^4.0.0-rc.108"
"effect": "^4.0.0-rc.113"
},
"devDependencies": {
"@rstest/core": "^0.11.11",
"@types/node": "^26.4.0",
"effect": "4.0.0-rc.112",
"effect": "4.0.0-rc.117",
"pkg-pr-new": "0.0.88",
"publint": "^0.3.24",
"tsdown": "^0.22.14",
Expand Down
113 changes: 5 additions & 108 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 28 additions & 20 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type * as Effect from "effect/Effect"
import type * as Layer from "effect/Layer"
import type * as Schema from "effect/Schema"
import type * as Scope from "effect/Scope"
import type * as FC from "effect/testing/FastCheck"
import type * as Arbitrary from "effect/unstable/arbitrary/Arbitrary"
import * as Rs from "@rstest/core"
import * as internal from "./internal/internal.js"

Expand Down Expand Up @@ -58,8 +58,12 @@ export namespace Vitest {
* @since 0.1.0
*/
export type Arbitraries =
| Array<Schema.Schema<any> | FC.Arbitrary<any>>
| { [K in string]: Schema.Schema<any> | FC.Arbitrary<any> }
| Array<Schema.Schema<any> | Arbitrary.Arbitrary<any>>
| { [K in string]: Schema.Schema<any> | Arbitrary.Arbitrary<any> }

type ArbitraryValue<A> = A extends Schema.Schema<infer T> ? T
: A extends Arbitrary.Arbitrary<infer T> ? T
: never

/**
* @since 0.1.0
Expand All @@ -75,6 +79,15 @@ export namespace Vitest {
fails: Vitest.Test<R>

/**
* Runs an Effectful property test using Schema or Arbitrary inputs.
*
* Returning `false` or completing with any non-interruption failure falsifies the property and triggers
* shrinking. This includes typed Effect failures, thrown exceptions, and defects such as failed assertions.
* Effect interruption continues to interrupt the test.
*
* The test timeout interrupts the Effect fiber running generation, property evaluation, and shrinking. Effect
* finalizers run during that interruption. A timeout cannot preempt a synchronous callback that never returns.
*
* @since 0.1.0
*/
prop: <const Arbs extends Arbitraries, A, E>(
Expand All @@ -86,22 +99,15 @@ export namespace Vitest {
R,
[
{
[K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T
: Arbs[K] extends Schema.Schema<infer T> ? T
: never
[K in keyof Arbs]: ArbitraryValue<Arbs[K]>
},
Rs.TestContext
]
>,
timeout?:
| number
| Rs.TestOptions & {
fastCheck?: FC.Parameters<
{
[K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Arbs[K] extends Schema.Schema<infer T> ? T
: never
}
>
arbitrary?: Arbitrary.CheckOptions
}
) => void
}
Expand All @@ -127,27 +133,29 @@ export namespace Vitest {
}

/**
* Runs a synchronous property test using Schema or Arbitrary inputs.
*
* Returning `false` or throwing falsifies the property and triggers shrinking. A callback that returns
* normally without returning `false` passes for that generated input.
*
* The test timeout interrupts the Effect fiber running generation and shrinking. A timeout cannot preempt a
* synchronous callback that never returns.
*
* @since 0.1.0
*/
readonly prop: <const Arbs extends Arbitraries>(
name: string,
arbitraries: Arbs,
self: (
properties: {
[K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Arbs[K] extends Schema.Schema<infer T> ? T
: never
[K in keyof Arbs]: ArbitraryValue<Arbs[K]>
},
ctx: Rs.TestContext
) => void,
timeout?:
| number
| Rs.TestOptions & {
fastCheck?: FC.Parameters<
{
[K in keyof Arbs]: Arbs[K] extends FC.Arbitrary<infer T> ? T : Arbs[K] extends Schema.Schema<infer T> ? T
: never
}
>
arbitrary?: Arbitrary.CheckOptions
}
) => void
}
Expand Down
Loading
Loading