Verify uses Argon for serialization. See Default Settings for on how Argon is used and instructions on how to control that usage.
Serialization settings can be customized at three levels:
- Method: Will run the verification in the current test method.
- Class: Will run for all verifications in all test methods for a test class.
- Global: Will run for test methods on all tests.
Note that the output is technically not valid json.
- Names and values are not quoted.
- Newlines are not escaped.
The reason for these is that it makes approval files cleaner and easier to read and visualize/understand differences.
To use strict json call VerifierSettings.UseStrictJson
:
[ModuleInitializer]
public static void Init() =>
VerifierSettings.UseStrictJson();
var target = new TheTarget
{
Value = "Foo"
};
var settings = new VerifySettings();
settings.UseStrictJson();
await Verify(target, settings);
var target = new TheTarget
{
Value = "Foo"
};
await Verify(target)
.UseStrictJson();
Then this results in
- The default
.received.
and.verified.
extensions for serialized verification to be.json
. JsonTextWriter.QuoteChar
to be"
.JsonTextWriter.QuoteName
to betrue
.
Then when an object is verified:
var target = new TheTarget
{
Value = "Foo"
};
await Verify(target);
The resulting file will be:
{
"Value": "Foo"
}
Verify uses Argon for serialization.
Argon is a JSON framework for .NET. It is a hard fork of Newtonsoft.Json.
The default JsonSerializerSettings
are:
var settings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
};
VerifierSettings
.AddExtraSettings(_ =>
_.TypeNameHandling = TypeNameHandling.All);
[Fact]
public Task AddExtraSettings()
{
var settings = new VerifySettings();
settings
.AddExtraSettings(
_ => _.SerializeError = (currentObject, originalObject, location, member, exception, handled) =>
Console.WriteLine(member));
return Verify("Value", settings);
}
[Fact]
public Task AddExtraSettingsFluent() =>
Verify("Value")
.AddExtraSettings(
_ => _.SerializeError = (currentObject, originalObject, location, member, exception, handled) =>
Console.WriteLine(member));
JsonTextWriter.QuoteName is set to false. The reason for this is that it makes approval files cleaner and easier to read and visualize/understand differences.
By default empty collections are ignored during verification.
To disable this behavior globally use:
VerifierSettings.DontIgnoreEmptyCollections();
Extra Json.NET settings can be made:
VerifierSettings.AddExtraSettings(
_ => _.TypeNameHandling = TypeNameHandling.All);
var settings = new VerifySettings();
settings.AddExtraSettings(
_ => _.TypeNameHandling = TypeNameHandling.All);
One common use case is to register a custom JsonConverter. As only writing is required, to help with this there is WriteOnlyJsonConverter
, and WriteOnlyJsonConverter<T>
.
class CompanyConverter :
WriteOnlyJsonConverter<Company>
{
public override void Write(VerifyJsonWriter writer, Company company) =>
writer.WriteMember(company, company.Name, "Name");
}
VerifierSettings.AddExtraSettings(
_ => _.Converters.Add(new CompanyConverter()));
VerifyJsonWriter
exposes the following members:
Counter
property that gives programmatic access to the counting behavior used by Guid, Date, and Id scrubbing.Serializer
property that exposes the currentJsonSerializer
.Serialize(object value)
is a convenience method that callsJsonSerializer.Serialize
passing in the writer instance and thevalue
parameter.WriteProperty<T, TMember>(T target, TMember value, string name)
method that writes a property name and value while respecting other custom serialization settings eg member converters, ignore rules etc.
[Fact]
public Task ScopedSerializer()
{
var person = new Person
{
GivenNames = "John",
FamilyName = "Smith"
};
var settings = new VerifySettings();
settings.AddExtraSettings(_ => _.TypeNameHandling = TypeNameHandling.All);
return Verify(person, settings);
}
[Fact]
public Task ScopedSerializerFluent()
{
var person = new Person
{
GivenNames = "John",
FamilyName = "Smith"
};
return Verify(person)
.AddExtraSettings(_ => _.TypeNameHandling = TypeNameHandling.All);
}
Result:
{
$type: VerifyObjectSamples.Person,
GivenNames: John,
FamilyName: Smith
}
To ignore all members that match a certain type:
[Fact]
public Task IgnoreType()
{
var target = new IgnoreTypeTarget
{
ToIgnore = new()
{
Property = "Value"
},
ToIgnoreNullable = new()
{
Property = "Value"
},
ToIgnoreByInterface = new()
{
Property = "Value"
},
ToIgnoreByBase = new()
{
Property = "Value"
},
ToIgnoreByBaseGeneric = new()
{
Property = "Value"
},
ToIgnoreByType = new()
{
Property = "Value"
},
ToInclude = new()
{
Property = "Value"
},
ToIncludeNullable = new()
{
Property = "Value"
},
ToIgnoreStruct = new("Value"),
ToIgnoreStructNullable = new("Value"),
ToIncludeStruct = new("Value"),
ToIncludeStructNullable = new("Value")
};
var settings = new VerifySettings();
settings.IgnoreMembersWithType<ToIgnore>();
settings.IgnoreMembersWithType<ToIgnoreByType>();
settings.IgnoreMembersWithType<InterfaceToIgnore>();
settings.IgnoreMembersWithType<BaseToIgnore>();
settings.IgnoreMembersWithType(typeof(BaseToIgnoreGeneric<>));
settings.IgnoreMembersWithType<ToIgnoreStruct>();
return Verify(target, settings);
}
[Fact]
public Task IgnoreTypeFluent()
{
var target = new IgnoreTypeTarget
{
ToIgnore = new()
{
Property = "Value"
},
ToIgnoreNullable = new()
{
Property = "Value"
},
ToIgnoreByInterface = new()
{
Property = "Value"
},
ToIgnoreByBase = new()
{
Property = "Value"
},
ToIgnoreByBaseGeneric = new()
{
Property = "Value"
},
ToIgnoreByType = new()
{
Property = "Value"
},
ToInclude = new()
{
Property = "Value"
},
ToIncludeNullable = new()
{
Property = "Value"
},
ToIgnoreStruct = new("Value"),
ToIgnoreStructNullable = new("Value"),
ToIncludeStruct = new("Value"),
ToIncludeStructNullable = new("Value")
};
return Verify(target)
.IgnoreMembersWithType<ToIgnore>()
.IgnoreMembersWithType<ToIgnoreByType>()
.IgnoreMembersWithType<InterfaceToIgnore>()
.IgnoreMembersWithType<BaseToIgnore>()
.IgnoreMembersWithType(typeof(BaseToIgnoreGeneric<>))
.IgnoreMembersWithType<ToIgnoreStruct>();
}
Or globally:
VerifierSettings.IgnoreMembersWithType<ToIgnore>();
Result:
{
ToInclude: {
Property: Value
},
ToIncludeNullable: {
Property: Value
},
ToIncludeStruct: {
Property: Value
},
ToIncludeStructNullable: {
Property: Value
}
}
To scrub all members that match a certain type:
[Fact]
public Task ScrubType()
{
var target = new IgnoreTypeTarget
{
ToIgnore = new()
{
Property = "Value"
},
ToIgnoreNullable = new()
{
Property = "Value"
},
ToIgnoreByInterface = new()
{
Property = "Value"
},
ToIgnoreByBase = new()
{
Property = "Value"
},
ToIgnoreByBaseGeneric = new()
{
Property = "Value"
},
ToIgnoreByType = new()
{
Property = "Value"
},
ToInclude = new()
{
Property = "Value"
},
ToIncludeNullable = new()
{
Property = "Value"
},
ToIgnoreStruct = new("Value"),
ToIgnoreStructNullable = new("Value"),
ToIncludeStruct = new("Value"),
ToIncludeStructNullable = new("Value")
};
var settings = new VerifySettings();
settings.ScrubMembersWithType<ToIgnore>();
settings.ScrubMembersWithType<ToIgnoreByType>();
settings.ScrubMembersWithType<InterfaceToIgnore>();
settings.ScrubMembersWithType<BaseToIgnore>();
settings.ScrubMembersWithType(typeof(BaseToIgnoreGeneric<>));
settings.ScrubMembersWithType<ToIgnoreStruct>();
return Verify(target, settings);
}
[Fact]
public Task ScrubTypeFluent()
{
var target = new IgnoreTypeTarget
{
ToIgnore = new()
{
Property = "Value"
},
ToIgnoreNullable = new()
{
Property = "Value"
},
ToIgnoreByInterface = new()
{
Property = "Value"
},
ToIgnoreByBase = new()
{
Property = "Value"
},
ToIgnoreByBaseGeneric = new()
{
Property = "Value"
},
ToIgnoreByType = new()
{
Property = "Value"
},
ToInclude = new()
{
Property = "Value"
},
ToIncludeNullable = new()
{
Property = "Value"
},
ToIgnoreStruct = new("Value"),
ToIgnoreStructNullable = new("Value"),
ToIncludeStruct = new("Value"),
ToIncludeStructNullable = new("Value")
};
return Verify(target)
.ScrubMembersWithType<ToIgnore>()
.ScrubMembersWithType<ToIgnoreByType>()
.ScrubMembersWithType<InterfaceToIgnore>()
.ScrubMembersWithType<BaseToIgnore>()
.ScrubMembersWithType(typeof(BaseToIgnoreGeneric<>))
.ScrubMembersWithType<ToIgnoreStruct>();
}
Or globally:
VerifierSettings.ScrubMembersWithType<ToIgnore>();
Result:
{
ToIgnore: {Scrubbed},
ToIgnoreByType: {Scrubbed},
ToIgnoreByInterface: {Scrubbed},
ToIgnoreByBase: {Scrubbed},
ToIgnoreByBaseGeneric: {Scrubbed},
ToIgnoreNullable: {Scrubbed},
ToIgnoreStruct: {Scrubbed},
ToIgnoreStructNullable: {Scrubbed},
ToInclude: {
Property: Value
},
ToIncludeNullable: {
Property: Value
},
ToIncludeStruct: {
Property: Value
},
ToIncludeStructNullable: {
Property: Value
}
}
To ignore instances of a type based on delegate:
[Fact]
public Task AddIgnoreInstance()
{
var target = new IgnoreInstanceTarget
{
ToIgnore = new()
{
Property = "Ignore"
},
ToInclude = new()
{
Property = "Include"
}
};
var settings = new VerifySettings();
settings.IgnoreInstance<Instance>(_ => _.Property == "Ignore");
return Verify(target, settings);
}
[Fact]
public Task AddIgnoreInstanceFluent()
{
var target = new IgnoreInstanceTarget
{
ToIgnore = new()
{
Property = "Ignore"
},
ToInclude = new()
{
Property = "Include"
}
};
return Verify(target)
.IgnoreInstance<Instance>(_ => _.Property == "Ignore");
}
Or globally:
VerifierSettings.IgnoreInstance<Instance>(_ => _.Property == "Ignore");
Result:
{
ToInclude: {
Property: Include
}
}
To scrub instances of a type based on delegate:
[Fact]
public Task AddScrubInstance()
{
var target = new IgnoreInstanceTarget
{
ToIgnore = new()
{
Property = "Ignore"
},
ToInclude = new()
{
Property = "Include"
}
};
var settings = new VerifySettings();
settings.ScrubInstance<Instance>(_ => _.Property == "Ignore");
return Verify(target, settings);
}
[Fact]
public Task AddScrubInstanceFluent()
{
var target = new IgnoreInstanceTarget
{
ToIgnore = new()
{
Property = "Ignore"
},
ToInclude = new()
{
Property = "Include"
}
};
return Verify(target)
.ScrubInstance<Instance>(_ => _.Property == "Ignore");
}
Or globally:
VerifierSettings.ScrubInstance<Instance>(_ => _.Property == "Ignore");
Result:
{
ToIgnore: {Scrubbed},
ToInclude: {
Property: Include
}
}
To ignore members of a certain type using an expression:
[Fact]
public Task IgnoreMemberByExpression()
{
var target = new IgnoreExplicitTarget
{
Include = "Value",
Field = "Value",
Property = "Value",
PropertyWithPropertyName = "Value"
};
var settings = new VerifySettings();
settings.IgnoreMembers<IgnoreExplicitTarget>(
_ => _.Property,
_ => _.PropertyWithPropertyName,
_ => _.Field,
_ => _.GetOnlyProperty,
_ => _.PropertyThatThrows);
return Verify(target, settings);
}
[Fact]
public Task IgnoreMemberByExpressionFluent()
{
var target = new IgnoreExplicitTarget
{
Include = "Value",
Field = "Value",
Property = "Value"
};
return Verify(target)
.IgnoreMembers<IgnoreExplicitTarget>(
_ => _.Property,
_ => _.Field,
_ => _.GetOnlyProperty,
_ => _.PropertyThatThrows);
}
Or globally
VerifierSettings.IgnoreMembers<IgnoreExplicitTarget>(
_ => _.Property,
_ => _.PropertyWithPropertyName,
_ => _.Field,
_ => _.GetOnlyProperty,
_ => _.PropertyThatThrows);
Result:
{
Include: Value
}
To scrub members of a certain type using an expression:
[Fact]
public Task ScrubMemberByExpression()
{
var target = new IgnoreExplicitTarget
{
Include = "Value",
Field = "Value",
Property = "Value",
PropertyWithPropertyName = "Value"
};
var settings = new VerifySettings();
settings.ScrubMembers<IgnoreExplicitTarget>(
_ => _.Property,
_ => _.PropertyWithPropertyName,
_ => _.Field,
_ => _.GetOnlyProperty,
_ => _.PropertyThatThrows);
return Verify(target, settings);
}
[Fact]
public Task ScrubMemberByExpressionFluent()
{
var target = new IgnoreExplicitTarget
{
Include = "Value",
Field = "Value",
Property = "Value"
};
return Verify(target)
.ScrubMembers<IgnoreExplicitTarget>(
_ => _.Property,
_ => _.Field,
_ => _.GetOnlyProperty,
_ => _.PropertyThatThrows);
}
Or globally
VerifierSettings.ScrubMembers<IgnoreExplicitTarget>(
_ => _.Property,
_ => _.PropertyWithPropertyName,
_ => _.Field,
_ => _.GetOnlyProperty,
_ => _.PropertyThatThrows);
Result:
{
Include: Value,
Field: {Scrubbed},
Property: {Scrubbed},
_Custom: {Scrubbed},
GetOnlyProperty: {Scrubbed},
PropertyThatThrows: {Scrubbed}
}
To ignore members of a certain type using type and name:
[Fact]
public Task IgnoreMemberByName()
{
var target = new IgnoreExplicitTarget
{
Include = "Value",
Field = "Value",
Property = "Value",
PropertyByName = "Value"
};
var settings = new VerifySettings();
// For all types
settings.IgnoreMember("PropertyByName");
// For a specific type
settings.IgnoreMember(typeof(IgnoreExplicitTarget), "Property");
// For a specific type generic
settings.IgnoreMember<IgnoreExplicitTarget>("Field");
// For a specific type with expression
settings.IgnoreMember<IgnoreExplicitTarget>(_ => _.PropertyThatThrows);
return Verify(target, settings);
}
[Fact]
public Task IgnoreMemberByNameFluent()
{
var target = new IgnoreExplicitTarget
{
Include = "Value",
Field = "Value",
Property = "Value",
PropertyByName = "Value"
};
return Verify(target)
// For all types
.IgnoreMember("PropertyByName")
// For a specific type
.IgnoreMember(typeof(IgnoreExplicitTarget), "Property")
// For a specific type generic
.IgnoreMember<IgnoreExplicitTarget>("Field")
// For a specific type with expression
.IgnoreMember<IgnoreExplicitTarget>(_ => _.PropertyThatThrows);
}
Or globally:
// For all types
VerifierSettings.IgnoreMember("PropertyByName");
// For a specific type
VerifierSettings.IgnoreMember(typeof(IgnoreExplicitTarget), "Property");
// For a specific type generic
VerifierSettings.IgnoreMember<IgnoreExplicitTarget>("Field");
// For a specific type with expression
VerifierSettings.IgnoreMember<IgnoreExplicitTarget>(_ => _.PropertyThatThrows);
Result:
{
Include: Value,
GetOnlyProperty: asd
}
To scrub members of a certain type using type and name:
[Fact]
public Task ScrubMemberByName()
{
var target = new IgnoreExplicitTarget
{
Include = "Value",
Field = "Value",
Property = "Value",
PropertyByName = "Value"
};
var settings = new VerifySettings();
// For all types
settings.ScrubMember("PropertyByName");
// For a specific type
settings.ScrubMember(typeof(IgnoreExplicitTarget), "Property");
// For a specific type generic
settings.ScrubMember<IgnoreExplicitTarget>("Field");
// For a specific type with expression
settings.ScrubMember<IgnoreExplicitTarget>(_ => _.PropertyThatThrows);
return Verify(target, settings);
}
[Fact]
public Task ScrubMemberByNameFluent()
{
var target = new IgnoreExplicitTarget
{
Include = "Value",
Field = "Value",
Property = "Value",
PropertyByName = "Value"
};
return Verify(target)
// For all types
.ScrubMember("PropertyByName")
// For a specific type
.ScrubMember(typeof(IgnoreExplicitTarget), "Property")
// For a specific type generic
.ScrubMember<IgnoreExplicitTarget>("Field")
// For a specific type with expression
.ScrubMember<IgnoreExplicitTarget>(_ => _.PropertyThatThrows);
}
Or globally:
// For all types
VerifierSettings.ScrubMember("PropertyByName");
// For a specific type
VerifierSettings.ScrubMember(typeof(IgnoreExplicitTarget), "Property");
// For a specific type generic
VerifierSettings.ScrubMember<IgnoreExplicitTarget>("Field");
// For a specific type with expression
VerifierSettings.ScrubMember<IgnoreExplicitTarget>(_ => _.PropertyThatThrows);
Result:
{
Include: Value,
Field: {Scrubbed},
Property: {Scrubbed},
PropertyByName: {Scrubbed},
GetOnlyProperty: asd,
PropertyThatThrows: {Scrubbed}
}
To ignore members of a certain type using a predicate function:
[Fact]
public Task IgnoreMemberByPredicate()
{
var target = new IgnoreExplicitTarget
{
Include = "Value",
Field = "Value",
Property = "Value",
PropertyByName = "Value"
};
var settings = new VerifySettings();
settings.IgnoreMembers(_ => _ is "Field" or "Property");
settings.IgnoreMembers(_ => _.Name is "PropertyByName" or "PropertyThatThrows");
return Verify(target, settings);
}
[Fact]
public Task IgnoreMemberByPredicateFluent()
{
var target = new IgnoreExplicitTarget
{
Include = "Value",
Field = "Value",
Property = "Value",
PropertyByName = "Value"
};
var settings = new VerifySettings();
return Verify(target, settings)
.IgnoreMembers(_ => _ is "Field" or "Property")
.IgnoreMembers(_ => _.Name is "PropertyByName" or "PropertyThatThrows");
}
[Fact]
public Task IgnoreDictionaryByPredicate()
{
var settings = new VerifySettings();
settings.IgnoreMembers(name => name is "Ignore");
var target = new Dictionary<string, object>
{
{
"Include", new Dictionary<string, string>
{
{
"Ignore", "Value1"
},
{
"Key1", "Value2"
}
}
},
{
"Ignore", "Value3"
},
{
"Key2", "Value4"
}
};
return Verify(target, settings);
}
Or globally:
VerifierSettings.IgnoreMembers(
_=>_.DeclaringType == typeof(TargetClass) &&
_.Name == "Proprty");
Result:
{
Include: Value,
GetOnlyProperty: asd
}
To scrub members of a certain type using a predicate function:
[Fact]
public Task ScrubMemberByPredicate()
{
var target = new IgnoreExplicitTarget
{
Include = "Value",
Field = "Value",
Property = "Value",
PropertyByName = "Value"
};
var settings = new VerifySettings();
settings.ScrubMembers(_ => _ is "Field" or "Property");
settings.ScrubMembers(_ => _.Name is "PropertyByName" or "PropertyThatThrows");
return Verify(target, settings);
}
[Fact]
public Task ScrubMemberByPredicateFluent()
{
var target = new IgnoreExplicitTarget
{
Include = "Value",
Field = "Value",
Property = "Value",
PropertyByName = "Value"
};
var settings = new VerifySettings();
return Verify(target, settings)
.ScrubMembers(name => name is "Field" or "Property")
.ScrubMembers(member => member.Name is "PropertyByName" or "PropertyThatThrows");
}
[Fact]
public Task ScrubDictionaryByPredicate()
{
var settings = new VerifySettings();
settings.ScrubMembers(name => name is "Ignore");
var target = new Dictionary<string, object>
{
{
"Include", new Dictionary<string, string>
{
{
"Ignore", "Value1"
},
{
"Key1", "Value2"
}
}
},
{
"Ignore", "Value3"
},
{
"Key2", "Value4"
}
};
return Verify(target, settings);
}
Or globally:
VerifierSettings.ScrubMembers(
_=>_.DeclaringType == typeof(TargetClass) &&
_.Name == "Proprty");
Result:
{
Include: Value,
Field: {Scrubbed},
Property: {Scrubbed},
PropertyByName: {Scrubbed},
GetOnlyProperty: asd,
PropertyThatThrows: {Scrubbed}
}
Certain types, when passed directly in to Verify, are written directly without going through json serialization.
The default mapping is:
{
typeof(StringBuilder), (target, _) => ((StringBuilder) target).ToString()
},
{
typeof(StringWriter), (target, _) => ((StringWriter) target).ToString()
},
{
typeof(bool), (target, _) => ((bool) target).ToString(Culture.InvariantCulture)
},
{
typeof(short), (target, _) => ((short) target).ToString(Culture.InvariantCulture)
},
{
typeof(ushort), (target, _) => ((ushort) target).ToString(Culture.InvariantCulture)
},
{
typeof(int), (target, _) => ((int) target).ToString(Culture.InvariantCulture)
},
{
typeof(uint), (target, _) => ((uint) target).ToString(Culture.InvariantCulture)
},
{
typeof(long), (target, _) => ((long) target).ToString(Culture.InvariantCulture)
},
{
typeof(ulong), (target, _) => ((ulong) target).ToString(Culture.InvariantCulture)
},
{
typeof(decimal), (target, _) => ((decimal) target).ToString(Culture.InvariantCulture)
},
{
typeof(BigInteger), (target, _) => ((BigInteger) target).ToString(Culture.InvariantCulture)
},
#if NET5_0_OR_GREATER
{
typeof(Half), (target, _) => ((Half) target).ToString(Culture.InvariantCulture)
},
#endif
#if NET6_0_OR_GREATER
{
typeof(Date), (target, _) =>
{
var date = (Date) target;
return date.ToString("yyyy-MM-dd", Culture.InvariantCulture);
}
},
{
typeof(Time), (target, _) =>
{
var time = (Time) target;
return time.ToString("h:mm tt", Culture.InvariantCulture);
}
},
#endif
{
typeof(float), (target, _) => ((float) target).ToString(Culture.InvariantCulture)
},
{
typeof(double), (target, _) => ((double) target).ToString(Culture.InvariantCulture)
},
{
typeof(Guid), (target, _) => ((Guid) target).ToString()
},
{
typeof(DateTime), (target, _) => DateFormatter.ToJsonString((DateTime) target)
},
{
typeof(DateTimeOffset), (target, _) => DateFormatter.ToJsonString((DateTimeOffset) target)
},
{
typeof(XmlNode), (target, _) =>
{
var converted = (XmlNode) target;
var document = XDocument.Parse(converted.OuterXml);
return new(document.ToString(), "xml");
}
},
{
typeof(XElement), (target, settings) =>
{
var converted = (XElement) target;
return new(converted.ToString(), "xml");
}
},
This bypasses the Guid and DateTime scrubbing.
Extra types can be added to this mapping:
VerifierSettings.TreatAsString<ClassWithToString>(
(target, settings) => target.Property);
The value of a member can be mutated before serialization:
[ModuleInitializer]
public static void MemberConverterByExpressionInit()
{
// using only the member
VerifierSettings.MemberConverter<MemberTarget, string>(
expression: _ => _.Field,
converter: _ => $"{_}_Suffix");
// using target and member
VerifierSettings.MemberConverter<MemberTarget, string>(
expression: _ => _.Property,
converter: (target, member) => $"{target}_{member}_Suffix");
}
[Fact]
public Task MemberConverterByExpression()
{
var input = new MemberTarget
{
Field = "FieldValue",
Property = "PropertyValue"
};
return Verify(input);
}