Arbitrary
On this page
Generating Arbitraries
The Arbitrary.make
function allows for the creation of random values that align with a specific Schema<A, I, R>
.
This function returns an Arbitrary<A>
from the fast-check library,
which is particularly useful for generating random test data that adheres to the defined schema constraints.
Example
ts
import {Arbitrary ,FastCheck ,Schema } from "@effect/schema"constPerson =Schema .Struct ({name :Schema .NonEmptyString ,age :Schema .NumberFromString .pipe (Schema .int (),Schema .between (0, 200))})// This will generate an Arbitrary for the Person schema.constPersonArbitraryType =Arbitrary .make (Person )console .log (FastCheck .sample (PersonArbitraryType , 2))/*Example Output:[ { name: 'q r', age: 1 }, { name: '&|', age: 133 } ]*/
ts
import {Arbitrary ,FastCheck ,Schema } from "@effect/schema"constPerson =Schema .Struct ({name :Schema .NonEmptyString ,age :Schema .NumberFromString .pipe (Schema .int (),Schema .between (0, 200))})// This will generate an Arbitrary for the Person schema.constPersonArbitraryType =Arbitrary .make (Person )console .log (FastCheck .sample (PersonArbitraryType , 2))/*Example Output:[ { name: 'q r', age: 1 }, { name: '&|', age: 133 } ]*/
The entirety of fast-check
's API is accessible via the FastCheck
export,
allowing direct use of all its functionalities within your projects.
Transformations and Arbitrary Generation
The generation of arbitrary data requires a clear understanding of how transformations and filters are considered within a schema:
Filters applied before the last transformation in the transformation chain are not considered during the generation of arbitrary data.
Illustrative Example
ts
import {Arbitrary ,FastCheck ,Schema } from "@effect/schema"constschema1 =Schema .compose (Schema .NonEmptyString ,Schema .Trim ).pipe (Schema .maxLength (500))// This might produce empty strings, despite the `NonEmpty` filter, due to the sequence of filters.console .log (FastCheck .sample (Arbitrary .make (schema1 ), 2))/*Example Output:[ '', '"Ry' ]*/constschema2 =Schema .Trim .pipe (Schema .nonEmptyString (),Schema .maxLength (500))// This configuration ensures no empty strings are produced, adhering to the `nonEmpty()` filter properly.console .log (FastCheck .sample (Arbitrary .make (schema2 ), 2))/*Example Output:[ ']H+MPXgZKz', 'SNS|waP~\\' ]*/
ts
import {Arbitrary ,FastCheck ,Schema } from "@effect/schema"constschema1 =Schema .compose (Schema .NonEmptyString ,Schema .Trim ).pipe (Schema .maxLength (500))// This might produce empty strings, despite the `NonEmpty` filter, due to the sequence of filters.console .log (FastCheck .sample (Arbitrary .make (schema1 ), 2))/*Example Output:[ '', '"Ry' ]*/constschema2 =Schema .Trim .pipe (Schema .nonEmptyString (),Schema .maxLength (500))// This configuration ensures no empty strings are produced, adhering to the `nonEmpty()` filter properly.console .log (FastCheck .sample (Arbitrary .make (schema2 ), 2))/*Example Output:[ ']H+MPXgZKz', 'SNS|waP~\\' ]*/
Explanation:
- Schema 1: Takes into account
Schema.maxLength(500)
since it is applied after theSchema.Trim
transformation, but ignores theSchema.NonEmptyString
as it precedes the transformations. - Schema 2: Adheres fully to all filters because they are correctly sequenced after transformations, preventing the generation of undesired data.
Best Practices
For effective and clear data generation, it's advisable to organize transformations and filters methodically. A suggested pattern is:
- Filters for the initial type (
I
). - Followed by transformations.
- And then filters for the transformed type (
A
).
This setup ensures that each stage of data processing is precise and well-defined.
Illustrative Example
Avoid haphazard combinations of transformations and filters:
ts
import {Schema } from "@effect/schema"// Example of less optimal structuring where transformations and filters are mixed:constschema =Schema .compose (Schema .Lowercase ,Schema .Trim )
ts
import {Schema } from "@effect/schema"// Example of less optimal structuring where transformations and filters are mixed:constschema =Schema .compose (Schema .Lowercase ,Schema .Trim )
Prefer a structured approach by separating transformation steps from filter applications:
ts
import {Schema } from "@effect/schema"// Recommended approach: Separate transformations from filtersconstschema =Schema .transform (Schema .String ,Schema .String .pipe (Schema .trimmed (),Schema .lowercased ()),{strict : true,decode : (s ) =>s .trim ().toLowerCase (),encode : (s ) =>s })
ts
import {Schema } from "@effect/schema"// Recommended approach: Separate transformations from filtersconstschema =Schema .transform (Schema .String ,Schema .String .pipe (Schema .trimmed (),Schema .lowercased ()),{strict : true,decode : (s ) =>s .trim ().toLowerCase (),encode : (s ) =>s })
Customizing Arbitrary Data Generation
You can define how arbitrary data is generated by utilizing the arbitrary
annotation in your schema definitions.
Example
ts
import {Schema } from "@effect/schema"// Define a schema with a custom generator for natural numbers.constschema =Schema .Number .annotations ({arbitrary : (/**typeParameters**/) => (fc ) =>fc .nat ()})
ts
import {Schema } from "@effect/schema"// Define a schema with a custom generator for natural numbers.constschema =Schema .Number .annotations ({arbitrary : (/**typeParameters**/) => (fc ) =>fc .nat ()})
The annotation allows access to any type parameters via the first argument (typeParameters
) and the complete export of the fast-check library (fc
).
This setup enables you to return an Arbitrary
that precisely generates the type of data desired.
Customizing a schema can disrupt previously applied filters. Filters set after the customization will remain effective, while those applied before will be disregarded.
Illustrative Example
ts
import {Arbitrary ,FastCheck ,Schema } from "@effect/schema"// Here, the 'positive' filter is overridden by the custom arbitrary definitionconstproblematic =Schema .Number .pipe (Schema .positive ()).annotations ({arbitrary : () => (fc ) =>fc .integer ()})console .log (FastCheck .sample (Arbitrary .make (problematic ), 2))/*Example Output:[ -1600163302, -6 ]*/// Here, the 'positive' filter is applied after the arbitrary customization, ensuring it is consideredconstimproved =Schema .Number .annotations ({arbitrary : () => (fc ) =>fc .integer ()}).pipe (Schema .positive ())console .log (FastCheck .sample (Arbitrary .make (improved ), 2))/*Example Output:[ 7, 1518247613 ]*/
ts
import {Arbitrary ,FastCheck ,Schema } from "@effect/schema"// Here, the 'positive' filter is overridden by the custom arbitrary definitionconstproblematic =Schema .Number .pipe (Schema .positive ()).annotations ({arbitrary : () => (fc ) =>fc .integer ()})console .log (FastCheck .sample (Arbitrary .make (problematic ), 2))/*Example Output:[ -1600163302, -6 ]*/// Here, the 'positive' filter is applied after the arbitrary customization, ensuring it is consideredconstimproved =Schema .Number .annotations ({arbitrary : () => (fc ) =>fc .integer ()}).pipe (Schema .positive ())console .log (FastCheck .sample (Arbitrary .make (improved ), 2))/*Example Output:[ 7, 1518247613 ]*/