Skip to content

Latest commit

 

History

History
349 lines (280 loc) · 10.1 KB

File metadata and controls

349 lines (280 loc) · 10.1 KB

Type to string mapping

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));

Default type mapping

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");
    }
},

snippet source | anchor

Scrubbing is bypassed

This approach bypasses the Guid and DateTime scrubbing.

DateTime formatting

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);
    }
}

snippet source | anchor

DateTimeOffset formatting

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}");
    }
}

snippet source | anchor

Override TreatAsString defaults

The default TreatAsString behavior can be overridden:

VerifierSettings.TreatAsString<DateTime>(
    (target, settings) => target.ToString("D"));

snippet source | anchor

Extra Types

Extra types can be added to this mapping:

VerifierSettings.TreatAsString<ClassWithToString>(
    (target, settings) => target.Property);

snippet source | anchor

Redundant json serialization settings

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

See also