Gathering detailed insights and metrics for zod-fixture
Gathering detailed insights and metrics for zod-fixture
Gathering detailed insights and metrics for zod-fixture
Gathering detailed insights and metrics for zod-fixture
zod
TypeScript-first schema declaration and validation library with static type inference
zod-to-json-schema
Converts Zod schemas to Json Schemas
zod-validation-error
Wrap zod validation errors in user-friendly readable messages
@asteasolutions/zod-to-openapi
Builds OpenAPI schemas from Zod schemas
npm install zod-fixture
Module System
Min. Node Version
Typescript Support
Node Version
NPM Version
121 Stars
74 Commits
10 Forks
2 Watching
12 Branches
9 Contributors
Updated on 17 Nov 2024
TypeScript (99.14%)
JavaScript (0.43%)
HTML (0.28%)
Shell (0.14%)
Cumulative downloads
Total Downloads
Last day
52%
3,825
Compared to previous day
Last week
28.8%
14,757
Compared to previous week
Last month
30.1%
57,823
Compared to previous month
Last year
287.6%
433,117
Compared to previous year
Fixture Generation with 1:1 Zod Parity
Creating test fixtures should be easy.
zod-fixture helps with the arrange phase of your tests by creating test fixtures based on a zod schema.
1npm install -D zod-fixture
1pnpm add -D zod-fixture
1yarn add -D zod-fixture
1bun add -d zod-fixture
The easiest way to start using zod-fixture
is to import the pre-configured createFixture
function.
1import { z } from 'zod'; 2import { createFixture } from 'zod-fixture'; 3 4const personSchema = z.object({ 5 name: z.string(), 6 birthday: z.date(), 7 address: z.object({ 8 street: z.string(), 9 city: z.string(), 10 state: z.string(), 11 }), 12 pets: z.array(z.object({ name: z.string(), breed: z.string() })), 13 totalVisits: z.number().int().positive(), 14}); 15 16const person = createFixture(personSchema, { seed: 11 });
1{ 2 address: { 3 city: 'd-iveauywljfifd', 4 state: 'cetuqnbvmbkqwlt', 5 street: 'wyttcnyvxpetrsa', 6 }, 7 birthday: new Date('2089-04-19T20:26:28.411Z'), 8 name: 'barmftzlcngaynw', 9 pets: [ 10 { 11 breed: 'fbmiabahyvsy-vm', 12 name: 'bonzm-sjnglvkbb', 13 }, 14 { 15 breed: 'vifsztjznktjkve', 16 name: 'wqbjuehl-trb-ai', 17 }, 18 { 19 breed: 'cq-jcmhccaduqmk', 20 name: 'brrvbrgzmjhttzh', 21 }, 22 ], 23 totalVisits: 63, 24}, 25
[!NOTE]
The examples make use of the optional seed parameter to generate the same fixture every time. This is useful for our docs, deterministic testing, and to reproduce issues, but is not necessary in your code. Simply calling
createFixture
with no configuration is acceptable.
Take a look at the examples to see how you can use zod-fixture
in your tests.
zod-fixture
is highly customizable. We provide you with the same utility methods we use internally to give you fine-grained support for creating your own fixtures.
The easiset way to start customizing zod-fixture
is to use the Fixture
class directly and extend it with your own generator.
[!NOTE]
createFixture(...)
is just syntactic sugar fornew Fixture().fromSchema(...)
The example below uses 2 custom generators and a typical pattern for filtering based on the keys of an object.
1import { ZodNumber, ZodObject, z } from 'zod'; 2import { Fixture, Generator } from 'zod-fixture'; 3const totalVisitsGenerator = Generator({ 4 schema: ZodNumber, 5 filter: ({ context }) => context.path.at(-1) === 'totalVisits', 6 /** 7 * The `context` provides a path to the current field 8 * 9 * { 10 * totalVisits: ..., 11 * nested: { 12 * totalVisits: ..., 13 * } 14 * } 15 * 16 * Would match twice with the following paths: 17 * ['totalVisits'] 18 * ['nested', 'totalVisits'] 19 */ 20 21 // returns a more realistic number of visits. 22 output: ({ transform }) => transform.utils.random.int({ min: 0, max: 25 }), 23}); 24const addressGenerator = Generator({ 25 schema: ZodObject, 26 filter: ({ context }) => context.path.at(-1) === 'address', 27 // returns a custom address object 28 output: () => ({ 29 street: 'My Street', 30 city: 'My City', 31 state: 'My State', 32 }), 33}); 34 35const personSchema = z.object({ 36 name: z.string(), 37 birthday: z.date(), 38 address: z.object({ 39 street: z.string(), 40 city: z.string(), 41 state: z.string(), 42 }), 43 pets: z.array(z.object({ name: z.string(), breed: z.string() })), 44 totalVisits: z.number().int().positive(), 45}); 46 47const fixture = new Fixture({ seed: 38 }).extend([ 48 addressGenerator, 49 totalVisitsGenerator, 50]); 51const person = fixture.fromSchema(personSchema);
1{ 2 address: { 3 city: 'My City', 4 state: 'My State', 5 street: 'My Street', 6 }, 7 birthday: new Date('1952-01-21T17:32:42.094Z'), 8 name: 'yxyzyskryqofekd', 9 pets: [ 10 { 11 breed: 'dnlwozmxaigobrz', 12 name: 'vhvlrnsxroqpuma', 13 }, 14 { 15 breed: 'ifbgglityarecl-', 16 name: 'c-lmtvotjcevmyi', 17 }, 18 { 19 breed: 'fmylchvprjdgelk', 20 name: 'ydevqfcctdx-lin', 21 }, 22 ], 23 totalVisits: 15, 24}, 25
[!IMPORTANT]
The order the registered generators matters. The first generator that matches the conditions (
schema
andfilter
) is used to create the value.
To generate a value based on a zod type we're using what we call a Generator
.
A Generator
has 3 fundamental parts:
A schema can be provided in the following ways:
ZodString
)z.custom()
)1import { z, ZodString } from 'zod'; 2import { Fixture, Generator } from 'zod-fixture'; 3 4// this is a custom zod type 5const pxSchema = z.custom<`${number}px`>((val) => { 6 return /^\d+px$/.test(val as string); 7}); 8 9const StringGenerator = Generator({ 10 schema: ZodString, 11 output: () => 'John Doe', 12}); 13 14const PixelGenerator = Generator({ 15 schema: pxSchema, 16 output: () => '100px', 17}); 18 19const developerSchema = z.object({ 20 name: z.string().max(10), 21 resolution: z.object({ 22 height: pxSchema, 23 width: pxSchema, 24 }), 25}); 26 27const fixture = new Fixture({ seed: 7 }).extend([ 28 PixelGenerator, 29 StringGenerator, 30]); 31const developer = fixture.fromSchema(developerSchema);
1{ 2 name: 'John Doe', 3 resolution: { 4 height: '100px', 5 width: '100px', 6 }, 7} 8
In addition to matching schemas, zod-fixture
provides robust tools for filtering, allowing you to further narrow the matches for your generator. There are two common patterns for filtering.
In the case where you use a zod
method like z.string().email()
, zod
adds what they call a "check" to the definition. These are additional constraints that are checked during parsing that don't conform to a Typescript type. (ie TS does not have the concept of an email, just a string). zod-fixture
provides a type safe utility called checks
for interacting with these additional constraints.
There are two methods provided by the checks
utility:
has
-- returns a boolean letting you know if a particular check exists on the schema.find
-- returns the full definition of a check, which can be useful for generating output.1import { z, ZodString } from 'zod'; 2import { Fixture, Generator } from 'zod-fixture'; 3 4const EmailGenerator = Generator({ 5 schema: ZodString, 6 filter: ({ transform, def }) => 7 transform.utils.checks(def.checks).has('email'), 8 output: () => 'john.malkovich@gmail.com', 9}); 10 11const StringGenerator = Generator({ 12 schema: ZodString, 13 output: ({ transform, def }) => { 14 let min = transform.utils.checks(def.checks).find('min')?.value; 15 /** 16 * kind: "min"; 17 * value: number; 18 * message?: string | undefined; // a custom error message 19 */ 20 21 let max = transform.utils.checks(def.checks).find('max')?.value; 22 /** 23 * kind: "max"; 24 * value: number; 25 * message?: string | undefined; // a custom error message 26 */ 27 28 const length = transform.utils.checks(def.checks).find('length'); 29 /** 30 * kind: "length"; 31 * value: number; 32 * message?: string | undefined; // a custom error message 33 */ 34 35 if (length) { 36 min = length.value; 37 max = length.value; 38 } 39 40 return transform.utils.random.string({ min, max }); 41 }, 42}); 43 44const personSchema = z.object({ 45 name: z.string().max(10), 46 email: z.string().email(), 47}); 48 49const fixture = new Fixture({ seed: 38 }).extend([ 50 EmailGenerator, 51 StringGenerator, 52]); 53const person = fixture.fromSchema(personSchema);
1{ 2 email: 'john.malkovich@gmail.com', 3 name: 'yxyzyskryq', 4}, 5
Matching keys of an object is another common pattern and a bit tricky if you don't give it enough thought. Every generator is called with a context
and that context includes a path
. The path is an array of keys that got us to this value. Generally speaking, you will only want the last key in the path for matching things like "name", "email", "age", etc in a deeply nested object.
1import { z, ZodString } from 'zod'; 2import { Fixture, Generator } from 'zod-fixture'; 3 4const NameGenerator = Generator({ 5 schema: ZodString, 6 filter: ({ context }) => context.path.at(-1) === 'name', 7 output: () => 'John Doe', 8}); 9 10const personSchema = z.object({ 11 name: z.string(), // this matches ['name'] 12 email: z.string().email(), 13 relatives: z 14 .object({ 15 name: z.string(), // this will match as well ['relatives', 'name'] 16 email: z.string().email(), 17 }) 18 .array(), 19}); 20 21const fixture = new Fixture({ seed: 7 }).extend(NameGenerator); 22const person = fixture.fromSchema(personSchema);
1{ 2 email: 'rando@email.com', 3 name: 'John Doe', 4 relatives: [ 5 { 6 email: 'rando@email.com', 7 name: 'John Doe', 8 }, 9 { 10 email: 'rando@email.com', 11 name: 'John Doe', 12 }, 13 { 14 email: 'rando@email.com', 15 name: 'John Doe', 16 }, 17 ], 18} 19
Output is a function that generates the fixture for any matches. zod-fixture
provides a randomization utility for creating data, in addition to all of the defaults (including the seed).
For example, in the example below we create our own totalVisitsGenerator
to return more realastic numbers using the random
utilities.
1const totalVisitsGenerator = Generator({ 2 schema: ZodNumber, 3 filter: ({ context }) => context.path.at(-1) === 'totalVisits', 4 /** 5 * The `context` provides a path to the current field 6 * 7 * { 8 * totalVisits: ..., 9 * nested: { 10 * totalVisits: ..., 11 * } 12 * } 13 * 14 * Would match twice with the following paths: 15 * ['totalVisits'] 16 * ['nested', 'totalVisits'] 17 */ 18 19 // returns a more realistic number of visits. 20 output: ({ transform }) => transform.utils.random.int({ min: 0, max: 25 }), 21});
zod-fixture
was built with this in mind. Simply define your custom type using zod's z.custom
and pass the resulting schema to your custom generator.
1import { z } from 'zod'; 2import { Fixture, Generator } from 'zod-fixture'; 3 4// Your custom type 5const pxSchema = z.custom<`${number}px`>((val) => { 6 return /^\d+px$/.test(val as string); 7}); 8 9// Your custom generator 10const PixelGenerator = Generator({ 11 schema: pxSchema, 12 output: () => '100px', 13}); 14 15// Example 16const resolutionSchema = z.object({ 17 width: pxSchema, 18 height: pxSchema, 19}); 20 21const fixture = new Fixture().extend([PixelGenerator]); 22const resolution = fixture.fromSchema(resolutionSchema);
1{ 2 width: '100px', 3 height: '100px', 4} 5
z.instanceof
isn't returning what I expected. What gives?z.instanceof
is one of the few schemas that doesn't have first party support in zod
. It's technically a z.custom
under the hood, which means the only way to match is for you to create a custom generator and pass an instance of it as your schema.
1import { z } from 'zod'; 2import { Fixture, Generator } from 'zod-fixture'; 3 4class ExampleClass { 5 id: number; 6 constructor() { 7 this.id = ExampleClass.uuid++; 8 } 9 static uuid = 1; 10} 11 12// Schema from instanceof (remember, this is just a z.custom) 13const exampleSchema = z.instanceof(ExampleClass); 14 15// Your custom generator 16const ExampleGenerator = Generator({ 17 schema: exampleSchema, 18 output: () => new ExampleClass(), 19}); 20 21// Example 22const listSchema = z.object({ 23 examples: exampleSchema.array(), 24}); 25 26const fixture = new Fixture().extend(ExampleGenerator); 27const result = fixture.fromSchema(listSchema);
1{ 2 examples: [ 3 { 4 id: 1, 5 }, 6 { 7 id: 2, 8 }, 9 { 10 id: 3, 11 }, 12 ], 13} 14
The short answer, not yet. We plan to build out pre-defined generators for popular mocking libraries but are currently prioritizing reliability and ease of use. If you'd like to help us build out this functionality, feel free to open a pull request 😀
[!NOTE]
Fixture
is aTransformer
that comes prepackaged with generators for each of the first party types that Zod provides. For most cases, this is all you wil need, and offers a fast and easy way to create fixtures. For building a customTransformer
refer to the Advanced documentation.
We provide sane defaults for the random utilities used by our generators, but these can easily be customized.
1interface Defaults { 2 seed?: number; 3 array: { 4 min: number; 5 max: number; 6 }; 7 map: { 8 min: number; 9 max: number; 10 }; 11 set: { 12 min: number; 13 max: number; 14 }; 15 int: { 16 min: number; 17 max: number; 18 }; 19 float: { 20 min: number; 21 max: number; 22 }; 23 bigint: { 24 min: bigint; 25 max: bigint; 26 }; 27 date: { 28 min: number; 29 max: number; 30 }; 31 string: { 32 min: number; 33 max: number; 34 characterSet: string; 35 }; 36 recursion: { 37 min: number; 38 max: number; 39 }; 40}
A seed can be provided to produce the same results every time.
1const fixture = new Fixture({ seed: number });
Instead of using one of the opinionated Fixture
s, you can extend the unopinionated Transformer
and register the desired generators.
1import { ConstrainedTransformer, UnconstrainedTransformer } from 'zod-fixture'; 2 3/** 4 * Constrained defaults 5 * 6 * { 7 * array: { 8 * min: 3, 9 * max: 3, 10 * }, 11 * // ... 12 * string: { 13 * min: 15, 14 * max: 15, 15 * characterSet: 'abcdefghijklmnopqrstuvwxyz-', 16 * } 17 * } 18 */ 19new ConstrainedTransformer().extend([ 20 /* insert your generators here */ 21]); 22 23/** 24 * Less constrained. Better for mocking APIs. 25 */ 26new UnconstrainedTransformer().extend([ 27 /* insert your generators here */ 28]);
The v2 version is a total rewrite of v1. Thanks for all the help @THEtheChad 🤝
v1 was flexible and allowed that multiple validation libraries could be supported in the future.
But, this made things more complex and I don't think we intended to add more libraries than zod
.
v2 is a full-on zod
version.
This benefits you because we make more use of zod's schema while creating fixtures.
For example, when you want to create a custom generator (previously a customization) you can also access zod's schema definition.
Fixture Generation with 1:1 Zod Parity
createFixture
still exists, but it could be that it generated its output with a slightly different output.
It still is compatible (even more compatible) with zod's schema.
For example, the changes to a string output:
BEFORE:
street-a088e991-896e-458c-bbbd-7045cd880879
AFTER:
fbmiabahyvsy-vm
createFixture
uses a pre-configured Fixture
instance, which cannot be customized anymore.
To create a custom fixture in v2, you need to create your own Fixture
instance, for more info see the docs.
Customization
is renamed to Generator
.
BEFORE:
1const addressCustomization: Customization = { 2 condition: ({ type, propertName }) => 3 type === 'object' && propertName === 'address', 4 generator: () => { 5 return { 6 street: 'My Street', 7 city: 'My City', 8 state: 'My State', 9 }; 10 }, 11};
AFTER:
1const addressGenerator = Generator({ 2 schema: ZodObject, 3 filter: ({ context }) => context.path.at(-1) === 'address', 4 output: () => ({ 5 street: 'My Street', 6 city: 'My City', 7 state: 'My State', 8 }), 9});
To add custom generators to the fixture, you need to create your own fixture instance and extend it with your own generators.
BEFORE:
1const person = createFixture(PersonSchema, { 2 customizations: [addressCustomization], 3});
AFTER:
1const fixture = new Fixture().extend([addressGenerator]); 2const person = fixture.fromSchema(personSchema);
To get started, create a codespace for this repository by clicking this 👇
A codespace will open in a web-based version of Visual Studio Code. The dev container is fully configured with software needed for this project.
Note: Dev containers is an open spec which is supported by GitHub Codespaces and other tools.
This package is inspired on AutoFixture.
No vulnerabilities found.
No security vulnerabilities found.