Certain types, when passed directly to Verify(), are written directly without going through json serialization.
The API for controlling this behavior is TreatAsString()
Example of passing directly to Verify():
[Fact]
public Task Example() =>
Verify(new DateOnly(2020, 10, 4));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 NET6_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.Convert((DateTime) target)
},
{
typeof(DateTimeOffset), (target, _) => DateFormatter.Convert((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 approach bypasses the Guid and DateTime scrubbing.
How DateTimes are converted to a string:
namespace VerifyTests;
public static partial class DateFormatter
{
public static string Convert(DateTime value)
{
var result = GetJsonDatePart(value);
if (value.Kind != DateTimeKind.Unspecified)
{
result += $" {value.Kind}";
}
return result;
}
static string GetJsonDatePart(DateTime value)
{
// ticks, not the Second/Millisecond properties, since sub-millisecond ticks leave both of those zero
var ticks = value.TimeOfDay.Ticks;
if (ticks == 0)
{
return value.ToString("yyyy-MM-dd", Culture.InvariantCulture);
}
if (ticks % TimeSpan.TicksPerMinute == 0)
{
return value.ToString("yyyy-MM-dd HH:mm", Culture.InvariantCulture);
}
if (ticks % TimeSpan.TicksPerSecond == 0)
{
return value.ToString("yyyy-MM-dd HH:mm:ss", Culture.InvariantCulture);
}
return value.ToString("yyyy-MM-dd HH:mm:ss.FFFFFFF", Culture.InvariantCulture);
}
public static string ToParameterString(DateTime value)
{
var result = GetParameterDatePart(value);
if (value.Kind != DateTimeKind.Unspecified)
{
result += value.Kind;
}
return result;
}
static string GetParameterDatePart(DateTime value)
{
var ticks = value.TimeOfDay.Ticks;
if (ticks == 0)
{
return value.ToString("yyyy-MM-dd", Culture.InvariantCulture);
}
if (ticks % TimeSpan.TicksPerMinute == 0)
{
return value.ToString("yyyy-MM-ddTHH-mm", Culture.InvariantCulture);
}
if (ticks % TimeSpan.TicksPerSecond == 0)
{
return value.ToString("yyyy-MM-ddTHH-mm-ss", Culture.InvariantCulture);
}
return value.ToString("yyyy-MM-ddTHH-mm-ss.FFFFFFF", Culture.InvariantCulture);
}
}How DateTimeOffset are converted to a string:
namespace VerifyTests;
public static partial class DateFormatter
{
public static string Convert(DateTimeOffset value)
{
var result = GetJsonDatePart(value);
result += $" {GetDateOffset(value)}";
return result;
}
static string GetJsonDatePart(DateTimeOffset value)
{
// ticks, not the Second/Millisecond properties, since sub-millisecond ticks leave both of those zero
var ticks = value.TimeOfDay.Ticks;
if (ticks == 0)
{
return value.ToString("yyyy-MM-dd", Culture.InvariantCulture);
}
if (ticks % TimeSpan.TicksPerMinute == 0)
{
return value.ToString("yyyy-MM-dd HH:mm", Culture.InvariantCulture);
}
if (ticks % TimeSpan.TicksPerSecond == 0)
{
return value.ToString("yyyy-MM-dd HH:mm:ss", Culture.InvariantCulture);
}
return value.ToString("yyyy-MM-dd HH:mm:ss.FFFFFFF", Culture.InvariantCulture);
}
public static string ToParameterString(DateTimeOffset value)
{
var result = GetParameterDatePart(value);
result += GetDateOffset(value);
return result;
}
static string GetParameterDatePart(DateTimeOffset value)
{
var ticks = value.TimeOfDay.Ticks;
if (ticks == 0)
{
return value.ToString("yyyy-MM-dd", Culture.InvariantCulture);
}
if (ticks % TimeSpan.TicksPerMinute == 0)
{
return value.ToString("yyyy-MM-ddTHH-mm", Culture.InvariantCulture);
}
if (ticks % TimeSpan.TicksPerSecond == 0)
{
return value.ToString("yyyy-MM-ddTHH-mm-ss", Culture.InvariantCulture);
}
return value.ToString("yyyy-MM-ddTHH-mm-ss.FFFFFFF", Culture.InvariantCulture);
}
// Interpolation formats with the current culture, and NumberFormatInfo.NegativeSign is not "-"
// everywhere: sv-SE renders U+2212 and ar-SA prefixes U+061C. The offset reaches both snapshot
// content and parameter file names, so the culture is pinned here the same way it is for every
// date part above, and the sign is written as a literal rather than taken from a negative number.
static string GetDateOffset(DateTimeOffset value)
{
var offset = value.Offset;
if (offset == TimeSpan.Zero)
{
return "+0";
}
// The sign belongs to the offset as a whole. Taking it from the hour component
// instead loses it for a sub hour offset, where that component is zero.
var sign = offset < TimeSpan.Zero ? '-' : '+';
if (offset.Minutes == 0)
{
return FormattableString.Invariant($"{sign}{Math.Abs(offset.TotalHours):0}");
}
return FormattableString.Invariant($"{sign}{Math.Abs(offset.Hours):0}-{Math.Abs(offset.Minutes):00}");
}
}The default TreatAsString behavior can be overridden:
VerifierSettings.TreatAsString<DateTime>(
(target, settings) => target.ToString("D"));Extra types can be added to this mapping:
VerifierSettings.TreatAsString<ClassWithToString>(
(target, settings) => target.Property);Since this approach bypasses json serialization, any json serialization settings are redundant. For example DontScrubDateTimes, UseStrictJson, and DontScrubGuids.
Note that any json serialization settings will still apply to anything amended to the target via Recording or JsonAppenders