Comparer

April 16, 2026 ยท View on GitHub

Comparers are used to compare non-text files.

Custom Comparer

Using a custom comparer can be helpful when a result has changed, but not enough to fail verification. For example when rendering images/forms on different operating systems.

For samples purposes only the image sizes will be compared:

static Task<CompareResult> CompareImages(
    Stream received,
    Stream verified,
    IReadOnlyDictionary<string, object> context)
{
    // Fake comparison
    if (received.Length == verified.Length)
    {
        return Task.FromResult(CompareResult.Equal);
    }

    var result = CompareResult.NotEqual();
    return Task.FromResult(result);
}

snippet source | anchor

The returned CompareResult.NotEqual takes an optional message that will be rendered in the resulting text displayed to the user on test failure.

If an input is split into multiple files, and a text file fails, then all subsequent binary comparisons will revert to the default comparison.

Instance comparer

[Fact]
public Task InstanceComparer()
{
    var settings = new VerifySettings();
    settings.UseStreamComparer(CompareImages);
    return VerifyFile("sample.png", settings);
}

[Fact]
public Task InstanceComparerFluent() =>
    VerifyFile("sample.png")
        .UseStreamComparer(CompareImages);

snippet source | anchor

Static comparer

VerifierSettings.RegisterStreamComparer(
    extension: "png",
    compare: CompareImages);
await VerifyFile("TheImage.png");

snippet source | anchor

Default Comparison

const int bufferSize = 1024 * sizeof(long);

public static async Task<CompareResult> AreEqual(Stream stream1, Stream stream2)
{
    EnsureAtStart(stream1);
    EnsureAtStart(stream2);

    var buffer1 = new byte[bufferSize];
    var buffer2 = new byte[bufferSize];

    while (true)
    {
        var count = await ReadBufferAsync(stream1, buffer1);

        //no need to compare size here since only enter on files being same size

        if (count == 0)
        {
            return CompareResult.Equal;
        }

        await ReadBufferAsync(stream2, buffer2);

        for (var i = 0; i < count; i += sizeof(long))
        {
            if (BitConverter.ToInt64(buffer1, i) != BitConverter.ToInt64(buffer2, i))
            {
                return CompareResult.NotEqual();
            }
        }
    }
}

static void EnsureAtStart(Stream stream)
{
    if (stream.CanSeek &&
        stream.Position != 0)
    {
        throw new("Expected stream to be at position 0.");
    }
}

static async Task<int> ReadBufferAsync(Stream stream, byte[] buffer)
{
    var bytesRead = 0;
    while (bytesRead < buffer.Length)
    {
        var read = await stream.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead);
        if (read == 0)
        {
            // Reached end of stream.
            return bytesRead;
        }

        bytesRead += read;
    }

    return bytesRead;
}

snippet source | anchor

PNG SSIM comparer

Verify includes a built-in Structural Similarity Index (SSIM) comparer for PNG files. It is opt-in and, when enabled, replaces the default byte-for-byte comparison for the .png extension.

This is useful when rendered images differ slightly between runs (e.g. anti-aliasing, font hinting, platform-specific rasterization) but are perceptually identical.

public static class ModuleInitializer
{
    [ModuleInitializer]
    public static void Init() =>
        VerifierSettings.UseSsimForPng();
}

snippet source | anchor

The default threshold is 0.98. SSIM scores range from 0 (completely different) to 1 (identical). A custom threshold can be supplied:

public static class ModuleInitializer
{
    [ModuleInitializer]
    public static void Init() =>
        VerifierSettings.UseSsimForPng(threshold: 0.995);
}

snippet source | anchor

Dimension mismatches between the received and verified images are always reported as not equal, regardless of threshold.

Supported PNG variants

The bundled decoder targets the common subset of PNGs produced by test scenarios:

  • 8-bit bit depth
  • Color types: grayscale, RGB, RGBA, grayscale+alpha, and paletted (with optional tRNS transparency)
  • Non-interlaced images

Unsupported variants (16-bit, Adam7 interlacing) cause the decoder to throw. For scenarios that require full PNG support, use one of the below comparers.

Pre-packaged comparers