ApprovalTests.Net and Verify: Snapshot Testing in C#

ApprovalTests.Net and Verify: Snapshot Testing in C#

The .NET ecosystem has two mature approval/snapshot testing libraries: ApprovalTests.Net (the original) and Verify (the modern alternative with better async support and scrubbing). Both capture output to files and compare on subsequent runs. This guide covers both libraries with practical C# examples for NUnit and xUnit.

Key Takeaways

Verify is the modern choice for new projects. Better async support, built-in scrubbers for timestamps and GUIDs, and cleaner xUnit/NUnit integration than ApprovalTests.Net.

ApprovalTests.Net has a larger library of reporters. BeyondCompare, WinMerge, and other diff tool integrations are better supported in ApprovalTests.Net.

Approved files go in source control. Both libraries create .approved.txt (ApprovalTests) or .verified.txt (Verify) files alongside your test files. Commit these.

Use scrubbers for non-deterministic data. Verify has built-in scrubbers for DateTime, Guid, and common patterns. ApprovalTests.Net requires custom string manipulation. Always scrub before approving.

Initial test run always fails. The first run creates the "received" file. You review it, copy to "approved"/"verified", and re-run. This is by design.

Choosing Between ApprovalTests.Net and Verify

Both libraries implement the same pattern but with different APIs and ecosystems:

Feature ApprovalTests.Net Verify
NuGet package ApprovalTests Verify.Xunit / Verify.NUnit
File extension .approved.txt .verified.txt
Async support Limited First-class
Built-in scrubbers None DateTime, Guid, IDs
Diff tool integration Many Via DiffEngine
Activity Mature, slower updates Actively developed

For new projects, start with Verify. For projects already using ApprovalTests.Net, stay with it unless you have a specific reason to migrate.

ApprovalTests.Net Setup

<!-- Install via NuGet -->
<PackageReference Include="ApprovalTests" Version="5.9.0" />
<PackageReference Include="ApprovalTests.NUnit" Version="5.9.0" />

Basic Usage with NUnit

// OrderApprovalTests.cs
using ApprovalTests;
using ApprovalTests.Reporters;
using NUnit.Framework;
using System.Text.Json;

[TestFixture]
[UseReporter(typeof(DiffReporter))]
public class OrderApprovalTests
{
    [Test]
    public void ApproveOrderSummary()
    {
        var order = new Order
        {
            Id = "ORD-001",
            Customer = "Alice",
            Items = new[] { new Item("Widget", 2, 14.99m) },
            Total = 29.98m
        };

        var summary = OrderFormatter.FormatSummary(order);

        Approvals.Verify(summary);
        // Creates: OrderApprovalTests.ApproveOrderSummary.approved.txt
    }

    [Test]
    public void ApproveOrderAsJson()
    {
        var order = BuildTestOrder();
        var json = JsonSerializer.Serialize(order, new JsonSerializerOptions
        {
            WriteIndented = true
        });

        Approvals.Verify(json);
    }
}

On first run, the test fails and creates:

tests/OrderApprovalTests.ApproveOrderSummary.received.txt

Review the file. If it looks correct, copy it to:

tests/OrderApprovalTests.ApproveOrderSummary.approved.txt

Commit both to source control. Re-run — the test passes.

Approving Combinations

ApprovalTests.Net has a powerful combination approver for testing multiple input combinations:

[Test]
public void ApprovePricingForAllTiers()
{
    var customers = new[] { "standard", "premium", "enterprise" };
    var quantities = new[] { 1, 10, 100 };

    ApprovalTests.Combination.CombinationApprovals.VerifyAllCombinations(
        (customer, qty) => CalculatePrice(customer, qty),
        customers,
        quantities
    );
}

This generates a grid of all combinations and stores the entire grid in one approved file — excellent for pricing tables, permission matrices, and rule engines.

Verify Setup

<PackageReference Include="Verify.Xunit" Version="23.0.0" />
<!-- Or for NUnit: -->
<PackageReference Include="Verify.NUnit" Version="23.0.0" />

Verify requires a one-time module initializer:

// VerifyInitializer.cs (one per project)
[ModuleInitializer]
public static class VerifyInitializer
{
    [ModuleInitializer]
    public static void Init() => VerifierSettings.InitializePlugins();
}

Basic Usage with xUnit

using VerifyXunit;

[UsesVerify]
public class OrderVerifyTests
{
    [Fact]
    public async Task VerifyOrderSummary()
    {
        var order = new Order
        {
            Id = "ORD-001",
            Customer = "Alice",
            Total = 29.98m
        };

        await Verify(order);
        // Creates: OrderVerifyTests.VerifyOrderSummary.verified.txt
    }

    [Fact]
    public async Task VerifyOrderAsJson()
    {
        var result = OrderService.Process(BuildTestOrder());
        await Verify(result);
    }
}

Verify automatically serializes objects to JSON. You don't need to serialize manually.

Scrubbers for Non-Deterministic Data

The killer feature of Verify is built-in scrubbing:

[Fact]
public async Task VerifyOrderWithTimestamp()
{
    var result = OrderService.CreateOrder(new OrderRequest
    {
        CustomerId = "c1",
        Items = new[] { new Item("Widget", 1, 9.99m) }
    });

    // result.CreatedAt = DateTime.Now — non-deterministic!

    await Verify(result)
        .ScrubMember("CreatedAt")  // Replace with <CreatedAt>
        .ScrubMember("OrderId");   // Replace with <OrderId>
}

Or configure globally:

// In VerifyInitializer
VerifierSettings.AddExtraSettings(settings =>
{
    settings.Converters.Add(new OrderIdScrubber());
});

// Or use built-in date scrubbing
VerifierSettings.ScrubInlineGuids();
VerifierSettings.ScrubInlineDateTimes();

Testing HTTP Responses with Verify

[UsesVerify]
public class ApiVerifyTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public ApiVerifyTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task VerifyGetOrdersResponse()
    {
        var response = await _client.GetAsync("/api/orders");
        var content = await response.Content.ReadAsStringAsync();

        await Verify(new
        {
            StatusCode = (int)response.StatusCode,
            Body = content
        })
        .ScrubLinesContaining("\"id\":")  // Scrub generated IDs
        .ScrubLinesContaining("\"createdAt\":"); // Scrub timestamps
    }
}

Approving Files Efficiently

CLI Approval (Cross-Platform)

# Install the Verify.Net.Tool globally
dotnet tool install -g verify.tool

<span class="hljs-comment"># After running tests, approve all received files:
verify approve --pattern <span class="hljs-string">"**/*.received.txt"

<span class="hljs-comment"># Or ApprovalTests equivalent — copy received to approved:
<span class="hljs-keyword">for file <span class="hljs-keyword">in **/*.received.txt; <span class="hljs-keyword">do
    <span class="hljs-built_in">cp <span class="hljs-string">"$file" <span class="hljs-string">"${file/.received./.approved.}"
<span class="hljs-keyword">done

Inline Snapshots with Verify

For small outputs, use inline snapshots instead of files:

[Fact]
public async Task VerifySmallOutput()
{
    var result = GetSimpleValue();

    await Verify(result)
        .UseTextForParameters("inline")
        .ToMatchSnapshot("""
        {
          "value": 42,
          "status": "ok"
        }
        """);
}

Combining with Test Data Builders

Approval tests work best when paired with builder patterns for test data:

public class OrderBuilder
{
    private string _customerId = "test-customer";
    private List<Item> _items = new();
    private string _discountCode = null;

    public OrderBuilder WithCustomer(string customerId)
    {
        _customerId = customerId;
        return this;
    }

    public OrderBuilder WithItem(string productId, int qty, decimal price)
    {
        _items.Add(new Item(productId, qty, price));
        return this;
    }

    public OrderBuilder WithDiscount(string code)
    {
        _discountCode = code;
        return this;
    }

    public Order Build() => new Order
    {
        CustomerId = _customerId,
        Items = _items,
        DiscountCode = _discountCode
    };
}

// In tests:
[Fact]
public async Task VerifyDiscountedOrder()
{
    var order = new OrderBuilder()
        .WithCustomer("premium-user")
        .WithItem("widget", 2, 19.99m)
        .WithDiscount("PREMIUM20")
        .Build();

    var result = OrderService.Process(order);
    await Verify(result).ScrubMember("ProcessedAt");
}

File Organization

Both libraries create test data files alongside your tests by default. Recommended structure:

tests/
  OrderTests/
    OrderApprovalTests.cs
    OrderApprovalTests.ApproveOrderSummary.approved.txt
    OrderApprovalTests.ApproveCombinations.approved.txt
  OrderVerifyTests/
    OrderVerifyTests.cs
    OrderVerifyTests.VerifyOrderSummary.verified.txt

Add received files to .gitignore:

*.received.txt

Never commit .received.txt — only .approved.txt and .verified.txt.

CI Configuration

Approval tests should fail loudly in CI when output changes:

# .github/workflows/test.yml
- name: Run approval tests
  run: dotnet test --configuration Release
  # If approved files are committed and output changes,
  # the test fails and shows the diff in CI output

On CI, no diff tool opens (DiffReporter falls back to file output). Configure a CI-friendly reporter:

// For CI environments
[UseReporter(typeof(ClipboardReporter))]  // Outputs diff to console
// Or:
[UseReporter(typeof(QuietReporter))]      // Just fails without opening a tool

Summary

Both ApprovalTests.Net and Verify implement the approval testing pattern in C# with good tooling:

  • Verify for new projects: better async, built-in scrubbers, active development
  • ApprovalTests.Net for existing codebases or if you need specific diff tool reporters
  • Approved files belong in source control — they are specifications
  • Scrub non-deterministic fields before approving to avoid flaky tests
  • Use combination approvers (ApprovalTests) or parameterized tests for input grids

Start with a single approval test for your most complex output format. That first approved file will show you immediately whether the pattern fits your codebase.

Read more