Meziantou.Framework.InlineSnapshotTesting
is a snapshot testing library that simplifies the assertion of complex data models and documents. It is inspired by Verify.
InlineSnapshot
is called on the test result during the assertion phase. It serializes that result and update the expected value. On the next test execution, the result is again serialized and compared to the existing value. The test will fail if the two snapshots do not match: either the change is unexpected, or the reference snapshot needs to be updated to the new result.
On the development machine, a diff tool prompt to compare the expected snapshot with the current snapshot. So, you can accept the new value or cancel. So, you can quickly iterate on your code and update snapshots.
Blog post: Inline Snapshot testing in .NET
First, you can write a test with the following code:
var data = new
{
FirstName = "Gérald",
LastName = "Barré",
NickName = "meziantou",
};
// No need to write the expected value
InlineSnapshot.Validate(data);
Then, run the tests. It will show you a diff tool where you can compare the expected value and the new value. Once you accept the change, the source code is updated:
var data = new
{
FirstName = "Gérald",
LastName = "Barré",
NickName = "meziantou",
};
InlineSnapshot.Validate(data, """
FirstName: Gérald,
LastName: Barré,
NickName: meziantou
""");
You can configure the default behavior of Validate()
by settings InlineSnapshotSettings.Default
. In the case of unit tests, you may want to update the configuration before running tests. You can use a ModuleInitializer
to do so.
static class AssemblyInitializer
{
[ModuleInitializer]
public static void Initialize()
{
InlineSnapshotSettings.Default = InlineSnapshotSettings.Default with
{
SnapshotUpdateStrategy = SnapshotUpdateStrategy.MergeTool,
MergeTools = [MergeTool.VisualStudioCode],
};
}
}
You can also set the configuration per assert:
// InlineSnapshotSettings is a record, so you can use the "with" keyword to create a new instance
var settings = InlineSnapshotSettings.Default with
{
SnapshotUpdateStrategy = SnapshotUpdateStrategy.Overwrite,
};
InlineSnapshot.CreateBuilder()
.WithSettings(settings)
.Validate(data, "");
If you prefer, you can use the alternative syntax:
InlineSnapshot.CreateBuilder()
.WithSettings(settings => settings.SnapshotUpdateStrategy = SnapshotUpdateStrategy.Overwrite)
.Validate(data, "");
By default, InlineSnapshot
uses the HumanReadableSerializer
to serialize the object. This is the recommended serializer for most cases. However, you can provide your own serializer if needed.
// Configure the HumanReadableSerializer
InlineSnapshot.CreateBuilder()
.WithSerializer(options => options.PropertyOrder = StringComparer.Ordinal)
.Validate(data);
// Use System.Text.Json
InlineSnapshot.CreateBuilder()
.WithSerializer(new JsonSnapshotSerializer())
.Validate(data);
If you use Verify and want to use the same serializer, you can use the Meziantou.Framework.InlineSnapshotTesting.Serializers.Argon
package.
InlineSnapshot.CreateBuilder()
.WithSerializer(new ArgonSnapshotSerializer())
.Validate(data);
The HumanReadableSerializer has many options to make the snapshot deterministic and easy to read. You can scrub values, scrub lines, show invisible characters, and more.
-
Ordering properties: Recent versions of .NET have a deterministic order of properties. However, .NET Framework does not have a deterministic order. You can use the
PropertyOrder
option to order the properties alphabetically:InlineSnapshot .WithSerializer(options => { options.PropertyOrder = StringComparer.Ordinal; options.DictionaryKeyOrder = StringComparer.Ordinal; }) .Validate(...);
-
Formatting content
InlineSnapshot .WithSerializer(options => { options.AddJsonFormatter(new JsonFormatterOptions { OrderProperties = true, WriteIndented = true, FormatAsStandardObject = false, }); options.AddXmlFormatter(new XmlFormatterOptions { OrderAttributes = true, WriteIndented = true, }); options.AddHtmlFormatter(new HtmlFormatterOptions { OrderAttributes = true, AttributeQuote = HtmlAttributeQuote.DoubleQuote, RedactContentSecurityPolicyNonce = true, }); options.AddUrlEncodedFormFormatter(new UrlEncodedFormFormatterOptions { OrderProperties = true, UnescapeValues = true, PrettyFormat = true, }); }) .Validate(...);
-
Ignore members
InlineSnapshot .WithSerializer(options => { options.IgnoreMember<TestClass>(x => x.Property); options.IgnoreMember<TestClass>(x => new { x.Property1, x.Property2 }); options.IgnoreMembersWithType<int>(); // ignore all properties of type int // ignore properties that throw an exception when accessed options.IgnoreMembersThatThrow(); options.IgnoreMembersThatThrow<NotImplementedException>(); }) .Validate(...);
-
Ignoring null/default values
InlineSnapshot .WithSerializer(options => options.DefaultIgnoreCondition = HumanReadableIgnoreCondition.WhenWritingDefault) .Validate(...);
When a snapshot is updated, a diff tool is used to compare the expected value and the new value. By default, it uses one of the following tools
- The diff tool configured by the
DiffEngine_Tool
environment variable - The merge tool from the local git configuration
- The diff tool from the local git configuration
- The diff tool from the current IDE (support VS Code, VS, Rider)
- The first available diff tool (rely on DiffEngine)
You can disable the diff tool by setting the DiffEngine_Disabled
environment variable.
If you want to use helper methods before calling Validate()
, you need to decorate the methods with [InlineSnapshotAssertion]
and use the [CallerFilePath]
and [CallerLineNumber]
attribute.
var instance = new { FirstName = "Gérald", LastName = "Barré" };
Helper(instance, ""); // This string will be updated
[InlineSnapshotAssertion(nameof(expected))] // name of the parameter that contains the snapshot
static void Helper(object data, string expected, [CallerFilePath] string filePath = null, [CallerLineNumber] int lineNumber = -1)
{
InlineSnapshot
.WithSerializer(options => options.ScrubValue<string>())
.Validate(data, expected, filePath, lineNumber);
}
Some data are not deterministic and should not be part of the snapshot. You can scrub the data at two locations. First, you can scrub the data during serialization. You have access to the actual values which can be useful. Second, you can scrub the data after serialization.
var data = new string[] { "a", "a", "b" };
InlineSnapshot
.WithSerializer(options => options.ScrubValue<string>())
.Validate(data, """
- String_0
- String_0
- String_1
""");
var data = new string[] { "a", "A", "b" };
InlineSnapshot
.WithSerializer(options => options.ScrubValue<string>(StringComparer.OrdinalIgnoreCase))
.Validate(data, """
- String_0
- String_0
- String_1
""");
var data = new string[] { "a", "A", "b" };
InlineSnapshot
.WithSerializer(options => options.ScrubValue<string>((value, index) => $"{value}_{index}", StringComparer.OrdinalIgnoreCase))
.Validate(data, """
- a_0
- a_0
- b_1
""");
var data = new Guid[] { Guid.NewGuid(), Guid.NewGuid(), Guid.Empty };
InlineSnapshot
.WithSerializer(options => options.ScrubGuid())
.Validate(data, """
- 00000000-0000-0000-0000-000000000001
- 00000000-0000-0000-0000-000000000002
- 00000000-0000-0000-0000-000000000000
""");
var now = DateTime.UtcNow;
var data = now.AddSeconds(10);
InlineSnapshot
.WithSerializer(options => options.UseRelativeDateTime(now))
.Validate(data, "00:00:10"); // TimeSpan relative to the now variable
InlineSnapshot
.WithSettings(settings => settings.ScrubLines(line => line.Contains("dummy")))
.Validate("abc\ndummy", "abc");
InlineSnapshot
.WithSettings(settings => settings.ScrubLinesContaining("dummy"))
.Validate("abc\ndummy", "abc");
InlineSnapshot
.WithSettings(settings => settings.ScrubLinesMatching("d.*y"))
.Validate("abc\ndummy", "abc");
InlineSnapshot
.WithSettings(settings => settings.ScrubLinesWithReplace(line => line.Replace("abc", "123")))
.Validate("abcdef", "123def");
var data = JsonNode.Parse("""{ "prop": "value" }""");
InlineSnapshot
.WithSettings(settings => settings.ScrubJsonValue("$.prop", node => "[redacted]"))
.Validate(data, """
{
"prop": "[redacted]"
}
""");
var data = XDocument.Parse("""
<root>
<item attr="1">test1</item>
</root>
""");
InlineSnapshot
.WithSettings(settings => settings.ScrubXmlAttribute("//item/@attr", attribute => "[redacted]"))
.Validate(data, """
<root>
<item attr="[redacted]">test1</item>
</root>
""");
If spaces or new lines are important, you can display them as visible characters.
InlineSnapshot
.WithSerializer(options => options.ShowInvisibleCharactersInValues = true)
.Validate("line 1\r\nline\t2", """
line␠1␍␊
line␉2
""");
By default, the snapshot can use string, verbatim string, or raw string. It uses the information from the PDB file to determine which C# features are available. You can override the default behavior by setting the CSharpStringFormat
property.
InlineSnapshot
.WithSerializer(options => options.AllowedStringFormats = CSharpStringFormats.Quoted | CSharpStringFormats.Verbatim | CSharpStringFormats.Raw)
.Validate(...);
You can also change the indentation of raw strings:
CSharpStringFormats.Raw
: Same indentation as the calling methodCSharpStringFormats.LeftAlignedRaw
: Align the raw string to the left (first column)
InlineSnapshot
.WithSerializer(options => options.AllowedStringFormats = CSharpStringFormats.Raw)
.Validate(new object(), """
{}
""");
InlineSnapshot
.WithSerializer(options => options.AllowedStringFormats = CSharpStringFormats.LeftAlignedRaw)
.Validate(new object(), """
{}
""");
You can also change the indentation, end of line, and the encoding of the file if the default behavior does not suit your needs.
InlineSnapshot
.WithSerializer(options => options.CSharpStringFormat = new CSharpStringFormat
{
Indentation = " ",
EndOfLine = "\r\n",
FileEncoding = Encoding.UTF8,
})
.Validate(...);
When running in a CI environment, the snapshot is never updated. To detect CI environment, the library uses the environment variables created by the major CI tools (GitHub Actions, Azure Pipelines, TeamCity etc.).
You can disable this behavior by setting AutoDetectContinuousEnvironment
to false
.
InlineSnapshot
.WithSettings(settings => settings.AutoDetectContinuousEnvironment = false)
.Validate(...);
You can also set the strategy to SnapshotUpdateStrategy.Disallow
to disable updating snapshots.
InlineSnapshot
.WithSettings(settings => settings.SnapshotUpdateStrategy = SnapshotUpdateStrategy.Disallow)
.Validate(...);
SnapshotUpdateStrategy.Overwrite
: Overwrite the snapshot with the new valueSnapshotUpdateStrategy.OverwriteWithoutFailure
: Overwrite the snapshot with the new value without failing the testSnapshotUpdateStrategy.MergeTool
: Use a merge tool to compare the snapshot with the new valueSnapshotUpdateStrategy.MergeToolSync
: Use a merge tool to compare the snapshot with the new value and wait for the merge tool to closeSnapshotUpdateStrategy.Disallow
: Do not update the snapshot
You can force the update of all snapshots by setting the InlineSnapshotSettings.ForceUpdateSnapshots
property to true
and setting the update strategy to OverwriteWithoutFailure
.
InlineSnapshotSettings.Default = InlineSnapshotSettings.Default with
{
ForceUpdateSnapshots = true, // Override the snapshot even if the value matches the expected value
SnapshotUpdateStrategy = SnapshotUpdateStrategy.OverwriteWithoutFailure,
};