anatine / zod-plugins

Plugins and utilities for Zod
640 stars 89 forks source link

[zod-mock] Generate all Dates in a stable way #196

Open dietrich-iti opened 6 months ago

dietrich-iti commented 6 months ago

When I generateMock with a provided seed option, I want to get the exact same result every time. However, if my schema contains ZodDate elements without a min or a max, I will get different values on subsequent runs.

Cause of the problem

Mock Date values are generated different ways depending whether they have min and/or max constraints (see parseDate in zod-mock.ts). Three of the cases will generate stable dates given the same random seed, but not the case when neither constraint is set. In this case, it simply calls faker.date.soon(), which will add a random (seed-based) offset to the current time. The current time, of course, will change from one run to the next.

Possible fixes

  1. Send an arbitrary (but constant) refDate to soon() in the !min && !max case. This could be done either universally, or only when the user provides their own random seed.
  2. Expose the refDate option as one of the zod-mock options, and document that users should set it to a fixed value when providing their own random seed.

Example

(using Node REPL, edited for clarity)

> const { z } = await import('zod')
> const { generateMock } = await import('@anatine/zod-mock')

> const dateMinMax = z.date().min(new Date('01/01/2023')).max(new Date('01/01/2024'))
> const dateMin = z.date().min(new Date('01/01/2023'))
> const dateMax = z.date().max(new Date('01/01/2024'))
> const dateUnbound = z.date()

> generateMock(dateMinMax, { seed: 123 })
2023-01-20T02:42:18.434Z
> generateMock(dateMinMax, { seed: 123 })
2023-01-20T02:42:18.434Z

> generateMock(dateMin, { seed: 123 })
2023-01-01T01:15:25.162Z
> generateMock(dateMin, { seed: 123 })
2023-01-01T01:15:25.162Z

> generateMock(dateMax, { seed: 123 })
2023-12-31T22:44:34.838Z
> generateMock(dateMax, { seed: 123 })
2023-12-31T22:44:34.838Z

> generateMock(dateUnbound, { seed: 123 })
2024-04-03T00:59:28.801Z
> generateMock(dateUnbound, { seed: 123 })
2024-04-03T00:59:30.597Z
ColinCee commented 2 months ago

Also having this issue, I looked into this and it seems the root cause is the config of faker. It's not immediately clear that setting a seed doesn't affect certain date functions

See issues:

Forunately as a result of the issues, the docs have been updated see: https://next.fakerjs.dev/guide/usage.html#reproducible-results

There are a few methods which use relative dates for which setting a random seed is not sufficient to have reproducible results, for example: faker.date.past, faker.date.future, faker.date.birthdate, faker.date.recent, faker.date.soon and faker.git.commitEntry. This is because these methods default to creating a date before or after "today", and "today" depends on when the code is run. To fix this, you can specify a fixed reference date as a Date or string, for example: // creates a date soon after 2023-01-01 faker.date.soon({ refDate: '2023-01-01T00:00:00.000Z' }); or alternatively you can set a default reference date for all these methods: // affects all future faker.date.* calls faker.setDefaultRefDate('2023-01-01T00:00:00.000Z');

Most likely we want to mirror faker's api so we should expose defaultRefDate in the generateMock options and use faker.setDefaultRefDate

ColinCee commented 2 months ago

To get around the current issue you can pass in an instance of faker

import { faker } from '@faker-js/faker';

faker.seed(123);
faker.setDefaultRefDate(new Date('2023-01-01'));

generateMock(SomeSchema, { faker });