Snapshot Testing in C#
Testing has already been a recurring topic on this blog. In addition to property-based testing, I have also covered integration testing in .NET with Testcontainers and architecture testing with ArchUnitNET.
Another useful tool in the testing toolbox is snapshot testing. Snapshot tests do exactly what the name suggests: they create a snapshot of a test result. This makes it possible to freeze the behavior of a system and protect it against unwanted changes and regressions.
The example code for this blog post is available on Github:
The examples shown here use the NuGet package Snapshooter. However, the approach can easily be applied to other libraries as well.
Why Snapshot Testing?
Snapshot tests freeze the behavior of a system at the moment the test is executed. When a snapshot test is run for the first time, the result of the functionality under test is written to a file and then serves as a reference for future test runs. If the result deviates from the content of the reference file, the test is considered to have failed.
The differences can be easily inspected visually in a diff tool, allowing you to quickly decide whether the changes are expected or unexpected. Once you have reviewed the changes and consider them valid, you can accept the updated file as the new reference.
{
"Date": "2025-12-01",
"TemperatureC": 18,
-- "TemperatureF": 59,
++ "TemperatureF": 64,
-- "Summary": ""
++ "Summary": "Mild"
}
Another advantage is that you can easily inspect the contents of large data objects visually. If you want to verify multiple properties of a return object, you would traditionally write several Assert statements:
[Test]
public void GetForecast_Should_ReturnCorrectValues()
{
var tomorrow = new DateOnly(2025, 12, 02);
const int temperatureC = 18;
var forecast = WeatherForecastProvider.GetForecast(tomorrow , temperatureC);
Assert.That(forecast.Date, Is.EqualTo(tomorrow));
Assert.That(forecast.Summary, Is.EqualTo("Mild"));
Assert.That(forecast.TemperatureC, Is.EqualTo(18));
Assert.That(forecast.TemperatureF, Is.EqualTo(64));
}
In contrast, when using a snapshot test, a single call to MatchSnapshot() allows you to capture all properties of the object at once.
[Test]
public void GetForecast_Should_MatchSnapshot()
{
var tomorrow = new DateOnly(2025, 12, 02);
const int temperatureC = 18;
var forecast = WeatherForecastProvider.GetForecast(tomorrow , temperatureC);
forecast.MatchSnapshot();
}
In the corresponding snapshot file __snapshots__/WeatherForecastProviderTests.GetForecast_Should_MatchSnapshot.snap, you’ll find the JSON representation of the object and can immediately see whether the properties contain the expected values.
{
"Date": "2025-12-02",
"TemperatureC": 18,
"TemperatureF": 64,
"Summary": "Mild"
}
Snapshot Testing on the backend
In the world of JavaScript frontend frameworks, snapshot testing has been common for quite some time, for example to verify the HTML output of render functions.
But snapshot testing can also be used effectively to verify SSR-based websites or API endpoints.
private readonly CustomWebApplicationFactory _factory = new();
[Test]
public async Task GetWeatherForecast_Should_MatchSnapshot()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/WeatherForecast");
var content = await response.Content.ReadAsStringAsync();
content.MatchSnapshot();
}
If an API call is not a pure function, some return values may change independently of the given input. For example, the current date. If you cannot control such values in your test setup, for instance by using the FakeTimeProvider, you can filter them out of the snapshot result.
content.MatchSnapshot(o => o.ExcludeField("$[*].date"));
HTML Snapshots
HTML pages can be tested in the same way. It’s often useful to clean up the HTML output before performing the snapshot comparison so that changes of random values are ignored. This includes things like dynamic IDs, anti-forgery tokens, or automatically generated CSS classes.
public static string PrepareMarkup(string markup)
{
var replacements = new Dictionary<string, string>
{
{
"""
<input name="__RequestVerificationToken" type="hidden" value="[\d\w-]+" \/>
""",
"""
<input name="__RequestVerificationToken" type="hidden" value="<replaced>" />
"""
},
{
@"\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b",
"00000000-0000-0000-0000-000000000000"
},
// ...
};
foreach (var (pattern, replacement) in replacements)
{
markup = Regex.Replace(markup, pattern, replacement);
}
return markup;
}
By creating snapshots of entire web pages at this level, you can reliably detect unwanted changes in the generated output and gain effective regression tests.
Working with legacy code
It’s well known that code without tests is considered legacy code. To work quickly and effectively with a legacy codebase, snapshot tests are an excellent way. They allow you to capture the application’s existing behavior, providing a safety net that makes it possible to modify the codebase while detecting unexpected changes.
For web applications, testing HTTP and API endpoints is straightforward. For desktop applications or native mobile apps, you need to work one layer below the UI. For example, you can create a snapshot of a ViewModel before and after changing the code.
If you have the ability to create repeatable integration tests with external resources such as a database, then the contents of that database can also become the target of a snapshot test.
[Test]
public async Task Table_User_Should_MatchSnapshot()
{
// Prepare database
// Do testing operation
var user = Repository.GetUsers();
users.MatchSnapshot();
}