deno.com
On this page

Snapshot testing

Snapshot testing is a testing technique that captures the output of your code and compares it against a stored reference version. Rather than manually writing assertions for each property, you let the test runner record the entire output structure, making it easier to detect any unexpected changes.

The Deno Standard Library has a snapshot module, which enables developers to write tests which assert a value against a reference snapshot. This reference snapshot is a serialized representation of the original value and is stored alongside the test file.

Basic usage Jump to heading

The assertSnapshot function will create a snapshot of a value and compare it to a reference snapshot, which is stored alongside the test file in the __snapshots__ directory.

To create an initial snapshot (or to update an existing snapshot), use the -- --update flag with the deno test command.

Basic snapshot example Jump to heading

The below example shows how to use the snapshot library with the Deno.test API. We can test a snapshot of a basic object, containing string and number properties.

The assertSnapshot(t, a) function compares the object against a stored snapshot. The t parameter is the test context that Deno provides, which the snapshot function uses to determine the test name and location for storing snapshots.

example_test.ts
import { assertSnapshot } from "jsr:@std/testing/snapshot";

Deno.test("isSnapshotMatch", async (t) => {
  const a = {
    hello: "world!",
    example: 123,
  };
  await assertSnapshot(t, a);
});

You will need to grant read and write file permissions in order for Deno to write a snapshot file and then read it to test the assertion. If it is the first time you are running the test a do not already have a snapshot, add the --update flag:

deno test --allow-read --allow-write -- --update

If you already have a snapshot file, you can run the test with:

deno test --allow-read

The test will compare the current output of the object against the stored snapshot. If they match, the test passes; if they differ, the test fails.

The snapshot file will look like this:

__snapshots__/example_test.ts.snap
export const snapshot = {};

snapshot[`isSnapshotMatch 1`] = `
{
  example: 123,
  hello: "world!",
}
`;

You can edit your test to change the hello string to "everyone!" and run the test again with deno test --allow-read. This time the assertSnapshot function will throw an AssertionError, causing the test to fail because the snapshot created during the test does not match the one in the snapshot file.

Updating snapshots Jump to heading

When adding new snapshot assertions to your test suite, or when intentionally making changes which cause your snapshots to fail, you can update your snapshots by running the snapshot tests in update mode. Tests can be run in update mode by passing the --update or -u flag as an argument when running the test. When this flag is passed, then any snapshots which do not match will be updated.

deno test --allow-read --allow-write -- --update

Note

New snapshots will only be created when the --update flag is present.

Permissions Jump to heading

When running snapshot tests, the --allow-read permission must be enabled, or else any calls to assertSnapshot will fail due to insufficient permissions. Additionally, when updating snapshots, the --allow-write permission must be enabled, as this is required in order to update snapshot files.

The assertSnapshot function will only attempt to read from and write to snapshot files. As such, the allow list for --allow-read and --allow-write can be limited to only include existing snapshot files, if desired.

Version Control Jump to heading

Snapshot testing works best when changes to snapshot files are committed alongside other code changes. This allows for changes to reference snapshots to be reviewed along side the code changes that caused them, and ensures that when others pull your changes, their tests will pass without needing to update snapshots locally.

Options Jump to heading

The assertSnapshot function can be called with an options object which offers greater flexibility and enables some non standard use cases:

import { assertSnapshot } from "jsr:@std/testing/snapshot";

Deno.test("isSnapshotMatch", async (t) => {
  const a = {
    hello: "world!",
    example: 123,
  };
  await assertSnapshot(t, a, {/*custom options go here*/});
});

serializer Jump to heading

When you run a test with assertSnapshot, the data you're testing needs to be converted to a string format that can be written to the snapshot file (when creating or updating snapshots) and compared with the existing snapshot (when validating), this is called serialization.

The serializer option allows you to provide a custom serializer function. This custom function will be called by assertSnapshot and be passed the value being asserted. Your custom function must:

  1. Return a string
  2. Be deterministic, (it will always produce the same output, given the same input).

The code below shows a practical example of creating and using a custom serializer function for snapshot testing. This serialiser removes any ANSI colour codes from a string using the stripColour string formatter from the Deno Standard Library.

example_test.ts
import { assertSnapshot, serialize } from "jsr:@std/testing/snapshot";
import { stripColor } from "jsr:@std/fmt/colors";

/**
 * Serializes `actual` and removes ANSI escape codes.
 */
function customSerializer(actual: string) {
  return serialize(stripColor(actual));
}

Deno.test("Custom Serializer", async (t) => {
  const output = "\x1b[34mHello World!\x1b[39m";
  await assertSnapshot(t, output, {
    serializer: customSerializer,
  });
});
__snapshots__/example_test.ts.snap
snapshot = {};

snapshot[`Custom Serializer 1`] = `"Hello World!"`;

Custom serializers can be useful in a variety of scenarios:

  • To remove irrelevant formatting (like ANSI codes shown above) and improve legibility
  • To handle non-deterministic data. Timestamps, UUIDs, or random values can be replaced with placeholders
  • To mask or remove sensitive data that shouldn't be saved in snapshots
  • Custom formatting to present complex objects in a domain-specific format

Serialization with Deno.customInspect Jump to heading

Because the default serializer uses Deno.inspect under the hood, you can set the property Symbol.for("Deno.customInspect") to a custom serialization function if desired:

example_test.ts
// example_test.ts
import { assertSnapshot } from "jsr:@std/testing/snapshot";

class HTMLTag {
  constructor(
    public name: string,
    public children: Array<HTMLTag | string> = [],
  ) {}

  public render(depth: number) {
    const indent = "  ".repeat(depth);
    let output = `${indent}<${this.name}>\n`;
    for (const child of this.children) {
      if (child instanceof HTMLTag) {
        output += `${child.render(depth + 1)}\n`;
      } else {
        output += `${indent}  ${child}\n`;
      }
    }
    output += `${indent}</${this.name}>`;
    return output;
  }

  public [Symbol.for("Deno.customInspect")]() {
    return this.render(0);
  }
}

Deno.test("Page HTML Tree", async (t) => {
  const page = new HTMLTag("html", [
    new HTMLTag("head", [
      new HTMLTag("title", [
        "Simple SSR Example",
      ]),
    ]),
    new HTMLTag("body", [
      new HTMLTag("h1", [
        "Simple SSR Example",
      ]),
      new HTMLTag("p", [
        "This is an example of how Deno.customInspect could be used to snapshot an intermediate SSR representation",
      ]),
    ]),
  ]);

  await assertSnapshot(t, page);
});

This test will produce the following snapshot.

__snapshots__/example_test.ts.snap
export const snapshot = {};

snapshot[`Page HTML Tree 1`] = `
<html>
  <head>
    <title>
      Simple SSR Example
    </title>
  </head>
  <body>
    <h1>
      Simple SSR Example
    </h1>
    <p>
      This is an example of how Deno.customInspect could be used to snapshot an intermediate SSR representation
    </p>
  </body>
</html>
`;

In contrast, when we remove the Deno.customInspect method, the test will produce the following snapshot:

__snapshots__/example_test.ts.snap
export const snapshot = {};

snapshot[`Page HTML Tree 1`] = `HTMLTag {
  children: [
    HTMLTag {
      children: [
        HTMLTag {
          children: [
            "Simple SSR Example",
          ],
          name: "title",
        },
      ],
      name: "head",
    },
    HTMLTag {
      children: [
        HTMLTag {
          children: [
            "Simple SSR Example",
          ],
          name: "h1",
        },
        HTMLTag {
          children: [
            "This is an example of how Deno.customInspect could be used to snapshot an intermediate SSR representation",
          ],
          name: "p",
        },
      ],
      name: "body",
    },
  ],
  name: "html",
}`;

You can see that this second snapshot is much less readable. This is because:

  1. The keys are sorted alphabetically, so the name of the element is displayed after its children
  2. It includes a lot of extra information, causing the snapshot to be more than twice as long
  3. It is not an accurate serialization of the HTML which the data represents

Note that in this example it would be possible to achieve the same result by calling:

await assertSnapshot(t, page.render(0));

However, depending on the public API you choose to expose, this may not be practical.

It is also worth considering that this could have an impact beyond your snapshot testing. For example, Deno.customInspect is also used to serialize objects when calling console.log (and in some other cases). This may or may not be desirable.

dir and path Jump to heading

The dir and path options allow you to control where the snapshot file will be saved to and read from. These can be absolute paths or relative paths. If relative, they will be resolved relative to the test file.

For example, if your test file is located at /path/to/test.ts and the dir option is set to snapshots, then the snapshot file would be written to /path/to/snapshots/test.ts.snap.

  • dir allows you to specify the snapshot directory, while still using the default format for the snapshot file name.

  • path allows you to specify the directory and file name of the snapshot file.

If your test file is located at /path/to/test.ts and the path option is set to snapshots/test.snapshot, then the snapshot file would be written to /path/to/snapshots/test.snapshot.

Note

If both dir and path are specified, the dir option will be ignored and the path option will be handled as normal.

mode Jump to heading

The mode option controls how assertSnapshot behaves regardless of command line flags and has two settings, assert or update:

  • assert: Always performs comparison only, ignoring any --update or -u flags. If snapshots don't match, the test will fail with an AssertionError.

  • update: Always updates snapshots. Any mismatched snapshots will be updated after tests complete.

This option is useful when you need different snapshot behaviors within the same test suite:

// Create a new snapshot or verify an existing one
await assertSnapshot(t, stableComponent);

// Always update this snapshot regardless of command line flags
await assertSnapshot(t, experimentalComponent, {
  mode: "update",
  name: "experimental feature",
});

// Always verify but never update this snapshot regardless of command line flags
await assertSnapshot(t, criticalComponent, {
  mode: "assert",
  name: "critical feature",
});

name Jump to heading

The name of the snapshot. If unspecified, the name of the test step will be used instead.

example_test.ts
import { assertSnapshot } from "jsr:@std/testing/snapshot";

Deno.test("isSnapshotMatch", async (t) => {
  const a = {
    hello: "world!",
    example: 123,
  };
  await assertSnapshot(t, a, {
    name: "Test Name",
  });
});
__snapshots__/example_test.ts.snap
export const snapshot = {};

snapshot[`Test Name 1`] = `
{
  example: 123,
  hello: "world!",
}
`;

When assertSnapshot is run multiple times with the same value for name, then the suffix will be incremented as normal. i.e. Test Name 1, Test Name 2, Test Name 3, etc.

msg Jump to heading

Used to set a custom error message. This will overwrite the default error message, which includes the diff for failed snapshots:

Deno.test("custom error message example", async (t) => {
  const userData = {
    name: "John Doe",
    role: "admin",
  };

  await assertSnapshot(t, userData, {
    msg:
      "User data structure has changed unexpectedly. Please verify your changes are intentional.",
  });
});

When the snapshot fails, instead of seeing the default diff message, you'll see your custom error message.

Testing Different Data Types Jump to heading

Snapshot testing works with various data types and structures:

Deno.test("snapshot various types", async (t) => {
  // Arrays
  await assertSnapshot(t, [1, 2, 3, "four", { five: true }]);

  // Complex objects
  await assertSnapshot(t, {
    user: { name: "Test", roles: ["admin", "user"] },
    settings: new Map([["theme", "dark"], ["language", "en"]]),
  });

  // Error objects
  await assertSnapshot(t, new Error("Test error message"));
});

Working with Asynchronous Code Jump to heading

When testing asynchronous functions, ensure you await the results before passing them to the snapshot:

Deno.test("async function test", async (t) => {
  const fetchData = async () => {
    // Simulate API call
    return { success: true, data: ["item1", "item2"] };
  };

  const result = await fetchData();
  await assertSnapshot(t, result);
});

Best Practices Jump to heading

Keep Snapshots Concise Jump to heading

Avoid capturing large data structures that aren't necessary for your test. Focus on capturing only what's relevant.

Descriptive Test Names Jump to heading

Use descriptive test names that clearly indicate what's being tested:

Deno.test(
  "renders user profile card with all required fields",
  async (t) => {
    // ... test code
    await assertSnapshot(t, component);
  },
);

Review Snapshots During Code Reviews Jump to heading

Always review snapshot changes during code reviews to ensure they represent intentional changes and not regressions.

Snapshot Organization Jump to heading

For larger projects, consider organizing snapshots by feature or component:

await assertSnapshot(t, component, {
  path: `__snapshots__/components/${componentName}.snap`,
});

Snapshot Testing in CI/CD Jump to heading

GitHub Actions Example Jump to heading

When running snapshot tests in CI environments, you'll typically want to verify existing snapshots rather than updating them:

.github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: denoland/setup-deno@v2
        with:
          deno-version: v2.x
      - name: Run tests
        run: deno test --allow-read

For pull requests that intentionally update snapshots, reviewers should verify the changes are expected before merging.

Practical Examples Jump to heading

Testing HTML Output Jump to heading

HTML output testing with snapshots is particularly useful for web applications where you want to ensure your components render the expected markup. This approach allows you to catch unintended changes in your HTML structure, attributes, or content that might affect the visual appearance or functionality of your UI components.

By capturing a snapshot of the HTML output, you can:

  • Verify that UI components render correctly with different props/data
  • Detect regressions when refactoring rendering logic
  • Document the expected output format of components
Deno.test("HTML rendering", async (t) => {
  const renderComponent = () => {
    return `<div class="card">
      <h2>User Profile</h2>
      <p>Username: testuser</p>
    </div>`;
  };

  await assertSnapshot(t, renderComponent());
});

Testing API Responses Jump to heading

When building applications that interact with APIs, snapshot testing helps ensure that the structure and format of API responses remain consistent. This is particularly valuable for:

  • Maintaining backward compatibility when updating API integrations
  • Verifying that your API response parsing logic works correctly
  • Documenting the expected shape of API responses for team collaboration
  • Detecting unexpected changes in API responses that could break your application
Deno.test("API response format", async (t) => {
  const mockApiResponse = {
    status: 200,
    data: {
      users: [
        { id: 1, name: "User 1" },
        { id: 2, name: "User 2" },
      ],
      pagination: { page: 1, total: 10 },
    },
  };

  await assertSnapshot(t, mockApiResponse);
});

🦕 Snapshot testing is a powerful technique that complements traditional unit tests by allowing you to capture and verify complex outputs without writing detailed assertions. By incorporating snapshot tests into your testing strategy, you can catch unintended changes, document expected behavior, and build more resilient applications.

Did you find what you needed?

Privacy policy