DEV Community

Chris Cook
Chris Cook

Posted on

How To Remove Dynamic Values From Snapshot With Serializers

Snapshot tests in Jest and Vitest are powerful tools for detecting unexpected changes in your code's output. However, they easily break when dealing with dynamic values like generated IDs or timestamps that change with each test run. While mocking these values is possible, it can lead to unintended side effects.

Consider this user object which could be returned from an API call or database query:

const user = {
  id: crypto.randomUUID(),
  name: "John Doe",
  createdAt: new Date().toISOString()
};
Enter fullscreen mode Exit fullscreen mode

Every time you run your tests, the id and createdAt values will be different, causing your snapshots to fail.

Basic Implementation

Here's how to create a custom serializer that replaces dynamic values with consistent placeholders:

const property = 'id';
const placeholder = '[ID]';

expect.addSnapshotSerializer({
  test(val) {
    return val && typeof val === 'object' && Object.hasOwn(val, property) && val[property] !== placeholder
  },
  serialize(val, config, indentation, depth, refs, printer) {
    return printer(
      {
        ...(val as Record<string, unknown>),
        [property]: placeholder,
      },
      config,
      indentation,
      depth,
      refs,
    );
  },
});
Enter fullscreen mode Exit fullscreen mode

You can add a custom snapshot serializer with expect.addSnapshotSerializer().
It expects an object with two functions:

  • test() is used to determine whether this custom serializer should be used. It checks if the value from expect(value) is an object with the property and has not been replaced by the placeholder.

  • serialize() is only called if test() has returned true. It replaces the property with the placeholder and calls the printer() function to serialize the value into a JSON-like string.

Tests

Now, when you run your tests, you will see that id was replaced with the [ID] placeholder:

interface User {
  id: string;
  name: string;
  createdAt: string;
}

expect.addSnapshotSerializer({ /* ... */ });

test('snapshot', () => {
  const user: User = {
    id: '123e4567-e89b-12d3-a456-426614174000',
    name: 'John Doe',
    createdAt: '2024-03-20T12:00:00Z',
  };

  expect(user).toMatchInlineSnapshot(`
    {
      "id": "[ID]",
      "name": "John Doe",
    }
  `);
});
Enter fullscreen mode Exit fullscreen mode

Making it Reusable

What if we need to handle multiple dynamic properties? Let's create a reusable solution:

export const replaceProperty = (
  property: string,
  placeholder: string,
): SnapshotSerializer => {
  return {
    test(val) {
      return val && typeof val === 'object' && Object.hasOwn(val, property) && val[property] !== placeholder
    },
    serialize(val, config, indentation, depth, refs, printer) {
      return printer(
        {
          ...(val as Record<string, unknown>),
          [property]: placeholder,
        },
        config,
        indentation,
        depth,
        refs,
      );
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

In your tests, you can create multiple serializers for different properties:

expect.addSnapshotSerializer(replaceProperty('id', '[ID]'));
expect.addSnapshotSerializer(replaceProperty('createdAt', '[TIMESTAMP]'));
Enter fullscreen mode Exit fullscreen mode

I use these serializers so frequently that I created the npm package snapshot-serializers to make it easier for everyone.

import { replaceProperty, removeProperty } from 'snapshot-serializers';

type User = {
  id: string;
  name: string;
  createdAt: string;
  password?: string;
};

// Type-safe property replacement
expect.addSnapshotSerializer(
  // TypeScript will only allow "id" | "name" | "createdAt" | "password"
  replaceProperty<User>({
    property: 'id',
    placeholder: '[ID]'
  })
);

// Remove properties entirely
expect.addSnapshotSerializer(
  removeProperty<User>({
    property: 'password'
  })
);

// This would cause a TypeScript error:
expect.addSnapshotSerializer(
  replaceProperty<User>({
    property: 'invalid' // Error: Type '"invalid"' is not assignable...
  })
);
Enter fullscreen mode Exit fullscreen mode

It provides a type-safe API to replace or remove properties in your snapshots. You can provide a generic type parameter like removeProperty<User>() and the function will suggest all possible property names based on the User type. Any other property will cause a TypeScript error.

Top comments (0)