Snapshot testing in .NET with Meziantou.Framework.SnapshotTesting

 
 
  • Gérald Barré

Snapshot testing is a technique where the output of a function or component is serialized and saved to disk the first time the test runs. On every subsequent run, the library re-serializes the output and compares it against the saved file. If they differ, the test fails.

Meziantou.Framework.SnapshotTesting is a .NET library that makes snapshot testing straightforward. It handles serialization, file management, and comparison, so you can focus on writing tests.

#What is snapshot testing?

In a typical unit test you write an expected value by hand:

C#
Assert.Equal("John", user.Name);
Assert.Equal(42, user.Age);

This works fine for simple cases, but becomes tedious when the object under test is large or complex (think: JSON payloads, HTML fragments, generated code, or images). Snapshot testing inverts this flow:

  1. On the first run, the library captures the actual output and writes it to a *.verified.* file next to your test source.
  2. You review the file, confirm it is correct, and commit it to source control.
  3. On every subsequent run, the library compares the actual output against the committed file. Any difference causes a test failure.

This approach is especially valuable when the expected output is too large or structured to express as inline assertions.

#When to use snapshot testing

Snapshot testing is a good fit when:

  • The output is a complex object (deeply nested, many properties).
  • You are testing serialization or formatting logic (JSON, XML, HTML).
  • You are testing rendered output (images, PDFs, generated code).
  • Regressions are the main concern and the full output shape matters.

It is not the best choice for simple scalar assertions where a single Assert.Equal is clearer, more precise and faster.

#Installation

Install the NuGet package:

dotnet add package Meziantou.Framework.SnapshotTesting

#Basic usage

Call Snapshot.Validate with the value you want to snapshot:

C#
public sealed class UserTests
{
    [Fact]
    public void ValidateUser()
    {
        var value = new { Name = "John", Age = 42 };
        Snapshot.Validate(value);
    }
}

On the first run, the library serializes value using its human-readable serializer and writes the result to:

__snapshots__/UserTests.ValidateUser.verified.txt

Open the file, verify the content looks correct, and commit it. Future runs compare the actual output against this file and fail if they differ.

For typed snapshots (binary or text with a known format), pass a SnapshotType:

C#
Snapshot.Validate(pngBytes, SnapshotType.Png);
Snapshot.Validate(svgText, SnapshotType.Svg);

#File naming convention

Snapshots are stored in a __snapshots__ directory placed next to the test source file:

FilePurpose
*.verified.<ext>The committed golden snapshot
*.actual.<ext>The output from the last failing run

The *.actual.* file is always written when a snapshot does not match. You can use it to inspect the difference or promote it to the verified snapshot.

When a single test assertion serializes multiple files, an index suffix is appended: _0, _1, and so on. An example of this would be a GIF snapshot that produces one file per image instead of a single animated file. Or maybe a file for a document and another for its metadata.

#Accepting snapshots with the CLI tool

After a test run you may have many *.actual.* files to promote. Install the dedicated CLI tool to approve them in bulk:

Shell
dotnet tool install --global Meziantou.Framework.SnapshotTesting.Tool

To approve all *.actual.* files in the current directory (renaming them to *.verified.*):

Shell
Meziantou.Framework.SnapshotTesting.Tool approve

To review each snapshot one by one and decide whether to approve or reject it:

Shell
Meziantou.Framework.SnapshotTesting.Tool approve --interactive

Use --folder <path> to target a specific directory, and --recurse to search all __snapshots__ subdirectories recursively.

#Customizing behavior with SnapshotSettings

SnapshotSettings controls how the library serializes and compares snapshots. You can override the default settings globally or per assertion:

C#
var settings = SnapshotSettings.Default with
{
    SnapshotUpdateStrategy = SnapshotUpdateStrategy.Disallow,
};

Snapshot.Validate(value, SnapshotType.Default, settings);

The available SnapshotUpdateStrategy values are:

StrategyBehavior
DisallowNever write or update snapshot files (useful in CI).
OverwriteAlways overwrite the verified file and fail the test.
OverwriteWithoutFailureOverwrite the verified file without failing the test.
MergeToolOpen a configured merge tool to compare the files.
MergeToolSyncSame as MergeTool but waits for the tool to close.

You can also set the strategy using the SNAPSHOTTESTING_STRATEGY environment variable (case-insensitive, e.g. DISALLOW or overwritewithoutfailure), which is useful in CI pipelines to prevent accidental snapshot updates.

#Scrubbing unstable output

Some values are expected to change on every run (timestamps, GUIDs, machine names, user names, request ids, and so on). If you snapshot those values directly, tests become noisy and fail for non-functional reasons.

Use scrubbers to make snapshots deterministic before comparison:

C#
var settings = SnapshotSettings.Default with { };

// Human-readable serializer scrubbers
settings.ConfigureHumanReadableSerializer(options => options.ScrubGuid());
settings.ConfigureHumanReadableSerializer(options => options.UseRelativeDateTime(DateTime.UtcNow));

// Line-based scrubbers
settings.ScrubLinesContaining("GeneratedAt:", "RequestId:");
settings.ScrubLinesMatching(@"^TraceId:");
settings.ScrubMachineName();
settings.ScrubUserName();

Snapshot.Validate(value, SnapshotType.Default, settings);

If you need full control, ScrubLinesWithReplace lets you transform each line (or remove it by returning null):

C#
settings.ScrubLinesWithReplace(line =>
{
    if (line.StartsWith("TemporaryPath:", StringComparison.Ordinal))
        return null;

    return line.Replace("localhost", "example.test", StringComparison.OrdinalIgnoreCase);
});

This keeps snapshots stable while still preserving meaningful regressions.

#Comparing images

The library supports snapshot testing of images using pixel-by-pixel comparison or a configurable similarity threshold. To enable image comparison, register the image comparer:

C#
SnapshotSettings.Default.Comparers.AddImageComparer();

##Supported formats

The library handles common image formats (PNG, JPG, BMP) and specialized formats:

  • Animated GIFs are compared frame-by-frame by extracting individual PNG frames.
  • ICO files are compared icon-by-icon by extracting individual PNG icons.

Register serializers for these formats:

C#
SnapshotSettings.Default.Serializers.AddGifSerializer();
SnapshotSettings.Default.Serializers.AddIcoSerializer();

##Comparison modes

By default, images are compared pixel-by-pixel for an exact match. For tests where minor rendering differences are acceptable (anti-aliasing, font rendering across platforms), configure a Structural Similarity Index (SSIM) threshold:

C#
SnapshotSettings.Default.Comparers.AddImageComparer(new ImageComparisonSettings
{
    SimilarityThreshold = 0.95f, // 0.0 = completely different, 1.0 = identical
});

##Advanced image loading with ImageSharp

For more advanced image processing, use SixLabors.ImageSharp with the Meziantou.Framework.SnapshotTesting.ImageSharp package:

Shell
dotnet add package Meziantou.Framework.SnapshotTesting.ImageSharp

Then configure it with AddImageSharp():

C#
public sealed class ImageTests
{
    [Fact]
    public void ValidateImage()
    {
        SnapshotSettings.Default.AddImageSharp(new ImageComparisonSettings
        {
            SimilarityThreshold = 0.99f,
        });

        using var image = Image.Load("sample.png");
        Snapshot.Validate(image, SnapshotType.Png);
    }
}

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?