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 z
z } 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 z
z.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.ZodString
name: import z
z.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.ZodNumber
age: import z
z.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.ZodBoolean
isAwesome: import z
z.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: any
difficult_schema = import z
z.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.ZodString
id: import z
z.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.ZodString
uuid(),
name: z.ZodString
name: import z
z.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.ZodString
min(3).ZodString.max(maxLength: number, message?: errorUtil.ErrMessage | undefined): z.ZodString
max(20),
age: z.ZodNumber
age: import z
z.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.ZodNumber
int().ZodNumber.min: (value: number, message?: errorUtil.ErrMessage | undefined) => z.ZodNumber
min(0).ZodNumber.max: (value: number, message?: errorUtil.ErrMessage | undefined) => z.ZodNumber
max(120).ZodNumber.multipleOf(value: number, message?: errorUtil.ErrMessage | undefined): z.ZodNumber
multipleOf(10),
isAwesome: z.ZodOptional<z.ZodBoolean>
isAwesome: import z
z.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 z
z.array<z.ZodString>(schema: z.ZodString, params?: z.RawCreateParams): z.ZodArray<z.ZodString, "many">
export array
array(import z
z.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.ZodString
min(3).ZodString.max(maxLength: number, message?: errorUtil.ErrMessage | undefined): z.ZodString
max(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.ZodString
zip: import z
z.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.ZodString
regex(/^[0-9]{5}$/),
children: z.ZodMap<z.ZodString, z.ZodLazy<any>>
children: import z
z.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 z
z.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 z
z.lazy<any>(getter: () => any, params?: z.RawCreateParams): z.ZodLazy<any>
export lazy
lazy(() => const difficult_schema: any
difficult_schema)
)
}) as any;
const const mockData: any
mockData = zocker<any>(schema: any): Zocker<any>
zocker(const difficult_schema: any
difficult_schema).Zocker<any>.generate(): any
generate();
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 z
z.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.ZodString
name: import z
z.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.ZodNumber
age: import z
z.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 referebcesupply(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.ZodString
name, '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: any
mockData = zocker<ZodAny>(schema: ZodAny): Zocker<ZodAny>
zocker(const schema: ZodAny
schema).Zocker<ZodAny>.setSeed(seed: number): Zocker<ZodAny>
setSeed(42).Zocker<ZodAny>.generate(): any
generate();
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!