Loris Sigrist looking very handsome Loris Sigrist

Never write Mock-Data again, with Zocker

The trend of zod-driven-development continues! This time, we’re going to use zod to generate sensible mock-data for our tests.

Writing Mock Data is the worst

When writing tests, you often need to provide some mock-data to test your code against. This can be a real pain, especially if you need lot’s of it, and if it’s complex.

Most mock-data generation libraries, such as the excellent faker, supply only individual fields, not entire data-structures.

Manually assembling these fields into a data-structure is tedious, and maintenance-heavy.

Enter Zocker

Zocker is a library I’ve built to forever eliminate the pain of writing and maintaining mock-data. It uses your zod-schemas as a guide to generate sensible and realistic mock-data for you. This way you can focus on writing tests, not on writing mock-data. Data generation does not get harder if you need more data, or if your data gets more complex. It’s all handled for you.

Getting Started

Obviously, install it first:

npm install --save-dev zocker

Then, in your test-file, import the zocker function and pass it your zod-schema:

import { import zz } from 'zod';
import { function zocker<Z extends z.ZodType<any, z.ZodTypeDef, any>>(schema: Z): Zocker<Z>zocker } from 'zocker';

const 
const schema: z.ZodObject<{
    name: z.ZodString;
    age: z.ZodNumber;
    isAwesome: z.ZodBoolean;
}, "strip", z.ZodTypeAny, {
    name: string;
    age: number;
    isAwesome: boolean;
}, {
    name: string;
    age: number;
    isAwesome: boolean;
}>
schema
= import zz.
object<{
    name: z.ZodString;
    age: z.ZodNumber;
    isAwesome: z.ZodBoolean;
}>(shape: {
    name: z.ZodString;
    age: z.ZodNumber;
    isAwesome: z.ZodBoolean;
}, params?: z.RawCreateParams): z.ZodObject<...>
export object
object
({
name: z.ZodStringname: import zz.
function string(params?: ({
    errorMap?: z.ZodErrorMap | undefined;
    invalid_type_error?: string | undefined;
    required_error?: string | undefined;
    message?: string | undefined;
    description?: string | undefined;
} & {
    ...;
}) | undefined): z.ZodString
export string
string
(),
age: z.ZodNumberage: import zz.
function number(params?: ({
    errorMap?: z.ZodErrorMap | undefined;
    invalid_type_error?: string | undefined;
    required_error?: string | undefined;
    message?: string | undefined;
    description?: string | undefined;
} & {
    ...;
}) | undefined): z.ZodNumber
export number
number
(),
isAwesome: z.ZodBooleanisAwesome: import zz.
function boolean(params?: ({
    errorMap?: z.ZodErrorMap | undefined;
    invalid_type_error?: string | undefined;
    required_error?: string | undefined;
    message?: string | undefined;
    description?: string | undefined;
} & {
    ...;
}) | undefined): z.ZodBoolean
export boolean
boolean
()
}); const
const mockData: {
    name: string;
    age: number;
    isAwesome: boolean;
}
mockData
=
zocker<z.ZodObject<{
    name: z.ZodString;
    age: z.ZodNumber;
    isAwesome: z.ZodBoolean;
}, "strip", z.ZodTypeAny, {
    name: string;
    age: number;
    isAwesome: boolean;
}, {
    name: string;
    age: number;
    isAwesome: boolean;
}>>(schema: z.ZodObject<...>): Zocker<...>
zocker
(
const schema: z.ZodObject<{
    name: z.ZodString;
    age: z.ZodNumber;
    isAwesome: z.ZodBoolean;
}, "strip", z.ZodTypeAny, {
    name: string;
    age: number;
    isAwesome: boolean;
}, {
    name: string;
    age: number;
    isAwesome: boolean;
}>
schema
).
Zocker<ZodObject<{ name: ZodString; age: ZodNumber; isAwesome: ZodBoolean; }, "strip", ZodTypeAny, { name: string; age: number; isAwesome: boolean; }, { ...; }>>.generate(): {
    name: string;
    age: number;
    isAwesome: boolean;
}
generate
();
// { name: "Jimmy Smith", age: 42, isAwesome: true }

And voilà! You have your mock-data.

That was obviously a very simple example. Zocker can handle much more complex schemas, including cyclic schemas, anys, unkowns, regular expressions, and much more. This here works just fine:

const const difficult_schema: anydifficult_schema = import zz.
object<{
    id: z.ZodString;
    name: z.ZodString;
    age: z.ZodNumber;
    isAwesome: z.ZodOptional<z.ZodBoolean>;
    friends: z.ZodArray<z.ZodString, "many">;
    zip: z.ZodString;
    children: z.ZodMap<...>;
}>(shape: {
    ...;
}, params?: z.RawCreateParams): z.ZodObject<...>
export object
object
({
id: z.ZodStringid: import zz.
function string(params?: ({
    errorMap?: z.ZodErrorMap | undefined;
    invalid_type_error?: string | undefined;
    required_error?: string | undefined;
    message?: string | undefined;
    description?: string | undefined;
} & {
    ...;
}) | undefined): z.ZodString
export string
string
().ZodString.uuid(message?: errorUtil.ErrMessage | undefined): z.ZodStringuuid(),
name: z.ZodStringname: import zz.
function string(params?: ({
    errorMap?: z.ZodErrorMap | undefined;
    invalid_type_error?: string | undefined;
    required_error?: string | undefined;
    message?: string | undefined;
    description?: string | undefined;
} & {
    ...;
}) | undefined): z.ZodString
export string
string
().ZodString.min(minLength: number, message?: errorUtil.ErrMessage | undefined): z.ZodStringmin(3).ZodString.max(maxLength: number, message?: errorUtil.ErrMessage | undefined): z.ZodStringmax(20),
age: z.ZodNumberage: import zz.
function number(params?: ({
    errorMap?: z.ZodErrorMap | undefined;
    invalid_type_error?: string | undefined;
    required_error?: string | undefined;
    message?: string | undefined;
    description?: string | undefined;
} & {
    ...;
}) | undefined): z.ZodNumber
export number
number
().ZodNumber.int(message?: errorUtil.ErrMessage | undefined): z.ZodNumberint().ZodNumber.min: (value: number, message?: errorUtil.ErrMessage | undefined) => z.ZodNumbermin(0).ZodNumber.max: (value: number, message?: errorUtil.ErrMessage | undefined) => z.ZodNumbermax(120).ZodNumber.multipleOf(value: number, message?: errorUtil.ErrMessage | undefined): z.ZodNumbermultipleOf(10),
isAwesome: z.ZodOptional<z.ZodBoolean>isAwesome: import zz.
function boolean(params?: ({
    errorMap?: z.ZodErrorMap | undefined;
    invalid_type_error?: string | undefined;
    required_error?: string | undefined;
    message?: string | undefined;
    description?: string | undefined;
} & {
    ...;
}) | undefined): z.ZodBoolean
export boolean
boolean
().ZodType<boolean, ZodBooleanDef, boolean>.optional(): z.ZodOptional<z.ZodBoolean>optional(),
friends: z.ZodArray<z.ZodString, "many">friends: import zz.
array<z.ZodString>(schema: z.ZodString, params?: z.RawCreateParams): z.ZodArray<z.ZodString, "many">
export array
array
(import zz.
function string(params?: ({
    errorMap?: z.ZodErrorMap | undefined;
    invalid_type_error?: string | undefined;
    required_error?: string | undefined;
    message?: string | undefined;
    description?: string | undefined;
} & {
    ...;
}) | undefined): z.ZodString
export string
string
().ZodString.min(minLength: number, message?: errorUtil.ErrMessage | undefined): z.ZodStringmin(3).ZodString.max(maxLength: number, message?: errorUtil.ErrMessage | undefined): z.ZodStringmax(20)).ZodArray<ZodString, "many">.min(minLength: number, message?: errorUtil.ErrMessage | undefined): z.ZodArray<z.ZodString, "many">min(1).ZodArray<ZodString, "many">.max(maxLength: number, message?: errorUtil.ErrMessage | undefined): z.ZodArray<z.ZodString, "many">max(10),
zip: z.ZodStringzip: import zz.
function string(params?: ({
    errorMap?: z.ZodErrorMap | undefined;
    invalid_type_error?: string | undefined;
    required_error?: string | undefined;
    message?: string | undefined;
    description?: string | undefined;
} & {
    ...;
}) | undefined): z.ZodString
export string
string
().ZodString.regex(regex: RegExp, message?: errorUtil.ErrMessage | undefined): z.ZodStringregex(/^[0-9]{5}$/),
children: z.ZodMap<z.ZodString, z.ZodLazy<any>>children: import zz.
map<z.ZodString, z.ZodLazy<any>>(keyType: z.ZodString, valueType: z.ZodLazy<any>, params?: z.RawCreateParams): z.ZodMap<z.ZodString, z.ZodLazy<any>>
export map
map
(
import zz.
function string(params?: ({
    errorMap?: z.ZodErrorMap | undefined;
    invalid_type_error?: string | undefined;
    required_error?: string | undefined;
    message?: string | undefined;
    description?: string | undefined;
} & {
    ...;
}) | undefined): z.ZodString
export string
string
(),
import zz.
lazy<any>(getter: () => any, params?: z.RawCreateParams): z.ZodLazy<any>
export lazy
lazy
(() => const difficult_schema: anydifficult_schema)
) }) as any; const const mockData: anymockData = zocker<any>(schema: any): Zocker<any>zocker(const difficult_schema: anydifficult_schema).Zocker<any>.generate(): anygenerate();

Supplying values

When testing specific edge-cases, you often want to supply your own values for certain fields.

This can be done by “supplying” your own value, or generator function, for a schema. That value is then used whenever a value is needed for a (sub)schema that matches the given schema by reference.

This is easier to undestand with an example:

const 
const user: z.ZodObject<{
    name: z.ZodString;
    age: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
    name: string;
    age: number;
}, {
    name: string;
    age: number;
}>
user
= import zz.
object<{
    name: z.ZodString;
    age: z.ZodNumber;
}>(shape: {
    name: z.ZodString;
    age: z.ZodNumber;
}, params?: z.RawCreateParams): z.ZodObject<{
    name: z.ZodString;
    age: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
    ...;
}, {
    ...;
}>
export object
object
({
name: z.ZodStringname: import zz.
function string(params?: ({
    errorMap?: z.ZodErrorMap | undefined;
    invalid_type_error?: string | undefined;
    required_error?: string | undefined;
    message?: string | undefined;
    description?: string | undefined;
} & {
    ...;
}) | undefined): z.ZodString
export string
string
(),
age: z.ZodNumberage: import zz.
function number(params?: ({
    errorMap?: z.ZodErrorMap | undefined;
    invalid_type_error?: string | undefined;
    required_error?: string | undefined;
    message?: string | undefined;
    description?: string | undefined;
} & {
    ...;
}) | undefined): z.ZodNumber
export number
number
()
}); const
const mockData: {
    name: string;
    age: number;
}
mockData
=
zocker<z.ZodObject<{
    name: z.ZodString;
    age: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
    name: string;
    age: number;
}, {
    name: string;
    age: number;
}>>(schema: z.ZodObject<{
    name: z.ZodString;
    age: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
    ...;
}, {
    ...;
}>): Zocker<...>
zocker
(
const user: z.ZodObject<{
    name: z.ZodString;
    age: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
    name: string;
    age: number;
}, {
    name: string;
    age: number;
}>
user
).
Zocker<ZodObject<{ name: ZodString; age: ZodNumber; }, "strip", ZodTypeAny, { name: string; age: number; }, { name: string; age: number; }>>.supply<z.ZodString>(schema: z.ZodString, generator: string | Generator<z.ZodString>): Zocker<z.ZodObject<{
    name: z.ZodString;
    age: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
    ...;
}, {
    ...;
}>>
Supply your own value / function for generating values for a given schema It will be used whenever the given schema matches an encountered schema by referebce
@paramschema - The schema for which this value will be used@paramgenerator - A value, or a function that generates a value that matches the schema
supply
(
const user: z.ZodObject<{
    name: z.ZodString;
    age: z.ZodNumber;
}, "strip", z.ZodTypeAny, {
    name: string;
    age: number;
}, {
    name: string;
    age: number;
}>
user
.
ZodObject<{ name: ZodString; age: ZodNumber; }, "strip", ZodTypeAny, { name: string; age: number; }, { name: string; age: number; }>.shape: {
    name: z.ZodString;
    age: z.ZodNumber;
}
shape
.name: z.ZodStringname, 'Jimmy Smith').
Zocker<ZodObject<{ name: ZodString; age: ZodNumber; }, "strip", ZodTypeAny, { name: string; age: number; }, { name: string; age: number; }>>.generate(): {
    name: string;
    age: number;
}
generate
();
// { name: "Jimmy Smith", age: 42 } - The name is now fixed

Limitations

There are a few limitations though. Zocker will never be able to generate data for preprocess or refinement functions. At least not out of the box. We can however supply our own values for those (sub)schemas, and side-step the issue.

Repeatability

By default, zocker will generate a new random value for each schema. This is great for most cases, but can lead to flaky tests if you’re not careful. If you want to generate the same data every time, you can set a seed using the setSeed method. This will generate the same data every time.

const const mockData: anymockData = zocker<ZodAny>(schema: ZodAny): Zocker<ZodAny>zocker(const schema: ZodAnyschema).Zocker<ZodAny>.setSeed(seed: number): Zocker<ZodAny>setSeed(42).Zocker<ZodAny>.generate(): anygenerate();

Conclusion

I hope this article has given you a taste of what zocker can do. If you want to learn more, check out the documentation. In my own use, zocker has been a huge time-saver. I hope it can help you too!