Generating test data with Zod

I keep finding amazing things you can do with Zod. In this post I'll show you how to use zod-mock to generate test data that conforms to a Zod schema.

I was recently integrating a web app into a desktop native app. When building the API layer between the two I used zod to validate the data that we receive in the web app. To test the API layer I needed some fake data. My usual approach would be to write some fake API payloads by hand but I found an awesome library that saved me loads of time and a lot of code.

zod-mock

zod-mock is an awesome library that stitches together Zod and Faker.js to generate endless amounts of fake data based on a Zod schema. If you have Zod schemas for data that you receive from an API then it’s so easy to generate test data.

🙏 Thanks to @anatine for creating zod-mock

Setup

First, install Faker.JS and zod-mock (and zod if you haven’t already).

npm install zod @faker-js/faker @anatine/zod-mock

Here’s an example Zod schema for a user:

export const UserSchema = z.object({
  id: z.string(),
  createdAt: z.string().datetime(),
  displayName: z.string(),
  role: z.enum(["Viewer", "Editor", "Admin"]),
});

In our test we can generate fake users:

import { generateMock } from '@anatine/zod-mock';
import { UserSchema } from "./schemas";

const user = generateMock(UserSchema);

This will generate an object like:

{ 
  id: 'qui',
  createdAt: '2023-04-11T12:44:51.332Z',
  displayName: 'deserunt',
  role: 'Viewer',
}

🙏 WARNING: Don’t use zod-mock or faker anywhere in your production code because your JavaScript bundle will be HUGE!

Making values more realistic

You’ll notice that the createdAt and role fields look quite realistic. The id and displayName fields on the other hand… they’re strings sure. But they don’t look much like a real ID or name. Maybe that’s good enough for your test but you can tell Faker to generate more realistic data based on the field.

import { generateMock } from '@anatine/zod-mock';
import { faker } from "@faker-js/faker";
import { UserSchema } from "./schemas";

const user = generateMock(UserSchema, { 
  stringMap: { 
    id: faker.datatype.uuid, 
    displayName: faker.name.fullName, 
  },
});

You’ll now get a user object like:

{
  id: '4cc126a0-f398-466b-af92-1dc9d44e6a4e', 
  createdAt: '2023-04-10T23:45:00.548Z', 
  displayName: 'Leah Larkin', 
  role: 'Admin', 
}

Creating consistent data with a seed

Each time you run generateMock() it will create a completely new item by default. That’s helpful in a unit test because I can easily create three different users:

import { generateMock } from '@anatine/zod-mock';
import { UserSchema } from "./schemas";

const user1 = generateMock(UserSchema);
const user2 = generateMock(UserSchema);
const user3 = generateMock(UserSchema);

But there are times when you need a predictable value. For example, it’s helpful to have predictable values if you’re creating fake data for mock service worker (msw). Fortunately zod-mock has you covered. You can pass a seed.

Let's create three user objects, making sure that the first two are identical.

import { generateMock } from '@anatine/zod-mock';
import { UserSchema } from "./schemas";

const user1 = generateMock(UserSchema, { seed: 1 }); // these two
const user2 = generateMock(UserSchema, { seed: 1 }); // are identical
const user3 = generateMock(UserSchema, { seed: 2 }); // but not this

Overriding fields

I’ve found it can be quite helpful to have the ability to manually specify some fields in the test data. I wrote a small utility to help me do that and I’ll share it with you:

import { GenerateMockOptions, generateMock } from "@anatine/zod-mock";
import { z } from "zod";

export const dataFactory =
  <T extends z.ZodTypeAny>(schema: T) =>
  (overrides?: Partial<z.infer<T>>, options?: GenerateMockOptions) =>
  ({
    ...generateMock(schema, options),
    ...overrides,
  });

This utility takes a Zod schema and returns a data factory function. Calling the function will return test data but you can pass in field values that you want to hardcode.

import { faker } from "@faker-js/faker";
import { dataFactory } from "test/utils";
import { UserSchema } from "./schemas";

const userFactory = dataFactory(UserSchema);

const tim = userFactory({ displayName: "Tim" });
const jenny = userFactory({ displayName: "Jenny", id: "55" });

// you can still use a seed and a faker string map
const paul = userFactory(
  { displayName: "Paul" },
  { 
    seed: 1,
    stringMap: {
      id: faker.datatype.uuid,
    },
  },
);

And that’s it! Let me know if you found this useful.