Snapshot testing can be useful in many cases, as it enables catching a wide array of bugs with very little code. It is particularly helpful in situations where it is difficult to precisely express what should be asserted, without requiring a prohibitive amount of code, or where the assertions a test makes are expected to change often. It therefore lends itself especially well to use in the development of front ends and CLIs.

    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.

    1. // __snapshots__/example_test.ts.snap
    2. export const snapshot = {};
    3. snapshot[`isSnapshotMatch 1`] = `
    4. {
    5. example: 123,
    6. hello: "world!",
    7. }
    8. `;

    Calling assertSnapshot in a test will throw an AssertionError, causing the test to fail, if the snapshot created during the test does not match the one in the snapshot file.

    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.

    Additionally, new snapshots will only be created when this flag is present.

    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 also 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 so desired.

    Snapshot testing works best when changes to snapshot files are comitted 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

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

    ```ts, ignore import { assertSnapshot } from “https://deno.land/std@$STD_VERSION/testing/snapshot.ts“;

    Deno.test(“isSnapshotMatch”, async function (t): Promise { const a = { hello: “world!”, example: 123, }; await assertSnapshot(t, a, { // options }); });

    1. **`serializer`**
    2. The `serializer` option allows you to provide a custom serializer function. This
    3. will be called by `assertSnapshot` and be passed the value being asserted. It
    4. should return a string. It is important that the serializer function is
    5. deterministic i.e. that it will always produce the same output, given the same
    6. input.
    7. The result of the serializer function will be written to the snapshot file in
    8. update mode, and in assert mode will be compared to the snapshot stored in the
    9. snapshot file.
    10. ```ts, ignore
    11. // example_test.ts
    12. import { assertSnapshot, serialize } from "https://deno.land/std@$STD_VERSION/testing/snapshot.ts";
    13. /**
    14. * Serializes `actual` and removes ANSI escape codes.
    15. */
    16. function customSerializer(actual: string) {
    17. return serialize(stripColor(actual));
    18. }
    19. Deno.test("Custom Serializer", async function (t): Promise<void> {
    20. const output = "\x1b[34mHello World!\x1b[39m";
    21. await assertSnapshot(t, output, {
    22. serializer: customSerializer,
    23. });

    Custom serializers can be useful in a variety of cases. One possible use case is to discard information which is not relevant and/or to present the serialized output in a more human readable form.

    For example, the above code snippet shows how a custom serializer could be used to remove ANSI escape codes (which encode font color and styles in CLI applications), making the snapshot more readable than it would be otherwise.

    Other common use cases would be to obfuscate values which are non-deterministic or which you may not want to write to disk for other reasons. For example, timestamps or file paths.

    Note that the default serializer is exported from the snapshot module so that its functionality can be easily extended.

    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, the 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.

    As shown in the above example, the dir option allows you to specify the snapshot directory, while still using the default format for the snapshot file name.

    In contrast, the path option allows you to specify the directory and file name of the snapshot file.

    For example, 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.

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

    mode

    The mode option can be either assert or update. When set, the --update and -u flags will be ignored.

    If the mode option is set to assert, then assertSnapshot will always behave as though the update flag is not passed i.e. if the snapshot does not match the one saved in the snapshot file, then an AssertionError will be thrown.

    If the mode option is set to update, then assertSnapshot will always behave as though the update flag has been passed i.e. if the snapshot does not match the one saved in the snapshot file, then the snapshot will be updated after all tests have run.

    name

    The name option specifies the name of the snapshot. By default, the name of the test step will be used. However, if specified, the name option will be used instead.

    ```ts, ignore // example_test.ts import { assertSnapshot } from ““;

    Deno.test(“isSnapshotMatch”, async function (t): Promise { const a = { hello: “world!”, example: 123, }; await assertSnapshot(t, a, { name: “Test Name” }); });

    1. ```js
    2. // __snapshots__/example_test.ts.snap
    3. export const snapshot = {};
    4. snapshot[`Test Name 1`] = `
    5. {
    6. example: 123,
    7. hello: "world!",
    8. }
    9. `;

    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.

    Allows setting a custom error message to use. This will overwrite the default error message, which includes the diff for failed snapshots.

    Serialization with Deno.customInspect

    The default serialization behaviour can be customised in two ways. The first is by specifying the option. This allows you to control the serialisation of any value which is passed to a specific assertSnapshot call. See the on the correct usage of this option.

    The second option is to make use of Deno.customInspect. Because the default serializer used by assertSnapshot uses Deno.inspect under the hood, you can set property Symbol.for("Deno.customInspect") to a custom serialization function.

    Doing so will ensure that the custom serialization will, by default, be used whenever the object is passed to assertSnapshot. This can be useful in many cases. One example is shown in the code snippet below.

    ```ts, ignore // example_test.ts import { assertSnapshot } from “https://deno.land/std@$STD_VERSION/testing/snapshot.ts“;

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

    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 { 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); });

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

    1. // __snapshots__/example_test.ts.snap
    2. export const snapshot = {};
    3. snapshot[`Page HTML Tree 1`] = `
    4. HTMLTag {
    5. children: [
    6. HTMLTag {
    7. HTMLTag {
    8. children: [
    9. "Simple SSR Example",
    10. ],
    11. name: "title",
    12. },
    13. ],
    14. name: "head",
    15. },
    16. HTMLTag {
    17. children: [
    18. HTMLTag {
    19. children: [
    20. "Simple SSR Example",
    21. ],
    22. name: "h1",
    23. },
    24. HTMLTag {
    25. children: [
    26. "This is an example of how Deno.customInspect could be used to snapshot an intermediate SSR representation",
    27. ],
    28. name: "p",
    29. },
    30. ],
    31. name: "body",
    32. },
    33. ],
    34. name: "html",
    35. }
    36. `;

    You can see that this 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

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

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

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