Skip to content

Latest commit

 

History

History
1224 lines (1027 loc) · 40.1 KB

serializer-settings.md

File metadata and controls

1224 lines (1027 loc) · 40.1 KB

Serializer settings

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.

Not valid json

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.

UseStrictJson

To use strict json call VerifierSettings.UseStrictJson:

VerifierSettings.UseStrictJson();

snippet source | anchor

Then this result in

  • The default .received. and .verified. extensions for serialized verification to be .json.
  • JsonTextWriter.QuoteChar to be ".
  • JsonTextWriter.QuoteName to be true.

Then when an object is verified:

var target = new TheTarget
{
    Value = "Foo"
};
await Verifier.Verify(target);

snippet source | anchor

The resulting file will be:

{
  "Value": "Foo"
}

snippet source | anchor

Default settings

The default JsonSerializerSettings are:

var settings = new JsonSerializerSettings
{
    Formatting = Formatting.Indented,
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
    DefaultValueHandling = DefaultValueHandling.Ignore,
    Culture = CultureInfo.InvariantCulture
};

snippet source | anchor

Modify Defaults

Globally

VerifierSettings.ModifySerialization(settings =>
    settings.AddExtraSettings(serializerSettings =>
        serializerSettings.DateFormatHandling = DateFormatHandling.MicrosoftDateFormat));

snippet source | anchor

On Settings

[Fact]
public Task AddExtraSettings()
{
    var target = new DateOnly(2000, 1, 1);

    var verifySettings = new VerifySettings();
    verifySettings.ModifySerialization(settings =>
        settings.AddExtraSettings(serializerSettings =>
            serializerSettings.DateFormatHandling = DateFormatHandling.MicrosoftDateFormat));
    return Verifier.Verify(target, verifySettings);
}

snippet source | anchor

On Settings Fluent

[Fact]
public Task AddExtraSettingsFluent()
{
    var target = new DateOnly(2000, 1, 1);

    return Verifier.Verify(target)
        .ModifySerialization(settings =>
            settings.AddExtraSettings(serializerSettings =>
                serializerSettings.DateFormatHandling = DateFormatHandling.MicrosoftDateFormat));
}

snippet source | anchor

QuoteName is false

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.

Empty collections are ignored

By default empty collections are ignored during verification.

To disable this behavior globally use:

VerifierSettings.ModifySerialization(_ => _.DontIgnoreEmptyCollections());

snippet source | anchor

Guids are scrubbed

By default guids are sanitized during verification. This is done by finding each guid and taking a counter based that that specific guid. That counter is then used replace the guid values. This allows for repeatable tests when guid values are changing.

var guid = Guid.NewGuid();
var target = new GuidTarget
{
    Guid = guid,
    GuidNullable = guid,
    GuidString = guid.ToString(),
    OtherGuid = Guid.NewGuid(),
};

await Verifier.Verify(target);

snippet source | anchor

Results in the following:

{
  Guid: Guid_1,
  GuidNullable: Guid_1,
  GuidString: Guid_1,
  OtherGuid: Guid_2
}

snippet source | anchor

Strings containing inline Guids can also be scrubbed. To enable this behavior, use:

VerifierSettings.ScrubInlineGuids();

snippet source | anchor

Disable

To disable this behavior use:

var settings = new VerifySettings();
settings.ModifySerialization(_ => _.DontScrubGuids());
await Verifier.Verify(target, settings);

snippet source | anchor

Or with the fluent api:

await Verifier.Verify(target)
    .ModifySerialization(_ => _.DontScrubGuids());

snippet source | anchor

To disable this behavior globally use:

VerifierSettings.ModifySerialization(_ => _.DontScrubGuids());

snippet source | anchor

Dates are scrubbed

By default dates (DateTime and DateTimeOffset) are sanitized during verification. This is done by finding each date and taking a counter based that that specific date. That counter is then used replace the date values. This allows for repeatable tests when date values are changing.

var dateTime = DateTime.Now;
var dateTimeOffset = DateTimeOffset.Now;
var target = new DateTimeTarget
{
    DateTime = dateTime,
    DateOnly = new(dateTime.Year,dateTime.Month,dateTime.Day),
    DateOnlyNullable = new(dateTime.Year,dateTime.Month,dateTime.Day),
    DateOnlyString = new DateOnly(dateTime.Year,dateTime.Month,dateTime.Day).ToString(),
    DateTimeNullable = dateTime,
    DateTimeString = dateTime.ToString("F"),
    DateTimeOffset = dateTimeOffset,
    DateTimeOffsetNullable = dateTimeOffset,
    DateTimeOffsetString = dateTimeOffset.ToString("F"),
};

await Verifier.Verify(target);

snippet source | anchor

Results in the following:

{
  DateTime: DateTime_1,
  DateTimeNullable: DateTime_1,
  DateOnly: Date_1,
  DateOnlyNullable: Date_1,
  DateTimeOffset: DateTimeOffset_1,
  DateTimeOffsetNullable: DateTimeOffset_1,
  DateTimeString: DateTimeOffset_2,
  DateTimeOffsetString: DateTimeOffset_2,
  DateOnlyString: Date_1
}

snippet source | anchor

To disable this behavior globally use:

VerifierSettings.ModifySerialization(_ => _.DontScrubDateTimes());

snippet source | anchor

Numeric Ids are scrubbed

By default numeric properties (int, uint, long, ulong, and their nullable counterparts) suffixed with Id are sanitized during verification. This is done by finding each id and taking a counter based that that specific id. That counter is then used replace the id values. This allows for repeatable tests when id are bing generated.

[Fact]
public Task NumericIdScrubbing()
{
    var target = new
    {
        Id = 5,
        OtherId = 5,
        YetAnotherId = 4,
        PossibleNullId = (int?)5,
        ActualNullId = (int?)null
    };

    return Verifier.Verify(target);
}

snippet source | anchor

Results in the following:

{
  Id: Id_1,
  OtherId: Id_1,
  YetAnotherId: Id_2,
  PossibleNullId: Id_1
}

snippet source | anchor

To disable this globally use:

[Fact]
public Task NumericIdScrubbingDisabled()
{
    var target = new
    {
        Id = 5,
        OtherId = 5,
        YetAnotherId = 4,
        PossibleNullId = (int?)5,
        ActualNullId = (int?)null
    };
    return Verifier.Verify(target)
        .ModifySerialization(settings => settings.DontScrubNumericIds());
}

snippet source | anchor

[Fact]
public Task NumericIdScrubbingDisabledGlobal()
{
    VerifierSettings.ModifySerialization(settings => settings.DontScrubNumericIds());
    return Verifier.Verify(
        new
        {
            Id = 5,
            OtherId = 5,
            YetAnotherId = 4,
            PossibleNullId = (int?)5,
            ActualNullId = (int?)null
        });
}

snippet source | anchor

To disable this behavior globally use:

[Fact]
public Task NumericIdScrubbingDisabled()
{
    var target = new
    {
        Id = 5,
        OtherId = 5,
        YetAnotherId = 4,
        PossibleNullId = (int?)5,
        ActualNullId = (int?)null
    };
    return Verifier.Verify(target)
        .ModifySerialization(settings => settings.DontScrubNumericIds());
}

snippet source | anchor

[Fact]
public Task NumericIdScrubbingDisabledGlobal()
{
    VerifierSettings.ModifySerialization(settings => settings.DontScrubNumericIds());
    return Verifier.Verify(
        new
        {
            Id = 5,
            OtherId = 5,
            YetAnotherId = 4,
            PossibleNullId = (int?)5,
            ActualNullId = (int?)null
        });
}

snippet source | anchor

Default Booleans are ignored

By default values of bool and bool? are ignored during verification. So properties that equate to 'false' will not be written,

To disable this behavior globally use:

VerifierSettings.ModifySerialization(_ => _.DontIgnoreFalse());

snippet source | anchor

Change defaults at the verification level

DateTime, DateTimeOffset, Guid, bool, and empty collection behavior can also be controlled at the verification level:

var settings = new VerifySettings();

settings.ModifySerialization(_ =>
{
    _.DontIgnoreEmptyCollections();
    _.DontScrubGuids();
    _.DontScrubDateTimes();
    _.DontIgnoreFalse();
});
await Verify(target, settings);

snippet source | anchor

await Verify(target)
    .ModifySerialization(_ =>
    {
        _.DontIgnoreEmptyCollections();
        _.DontScrubGuids();
        _.DontScrubDateTimes();
        _.DontIgnoreFalse();
    });

snippet source | anchor

await Verifier.Verify(target)
    .ModifySerialization(_ =>
    {
        _.DontIgnoreEmptyCollections();
        _.DontScrubGuids();
        _.DontScrubDateTimes();
        _.DontIgnoreFalse();
    });

snippet source | anchor

Changing Json.NET settings

Extra Json.NET settings can be made:

Globally

VerifierSettings.AddExtraSettings(_ =>
{
    _.DateFormatHandling = DateFormatHandling.MicrosoftDateFormat;
    _.TypeNameHandling = TypeNameHandling.All;
});

snippet source | anchor

Instance

var settings = new VerifySettings();
settings.AddExtraSettings(_ =>
{
    _.DateFormatHandling = DateFormatHandling.MicrosoftDateFormat;
    _.TypeNameHandling = TypeNameHandling.All;
});

snippet source | anchor

Json.NET Converter

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 WriteJson(
        JsonWriter writer,
        Company company,
        JsonSerializer serializer,
        IReadOnlyDictionary<string, object> context)
    {
        serializer.Serialize(writer, company.Name);
    }
}

snippet source | anchor

VerifierSettings.AddExtraSettings(
    _ =>
    {
        _.Converters.Add(new CompanyConverter());
    });

snippet source | anchor

Scoped settings

[Fact]
public Task ScopedSerializer()
{
    var person = new Person
    {
        GivenNames = "John",
        FamilyName = "Smith"
    };
    var settings = new VerifySettings();
    settings.AddExtraSettings(
        _ => { _.TypeNameHandling = TypeNameHandling.All; });
    return Verifier.Verify(person, settings);
}

[Fact]
public Task ScopedSerializerFluent()
{
    var person = new Person
    {
        GivenNames = "John",
        FamilyName = "Smith"
    };
    return Verifier.Verify(person)
        .AddExtraSettings(
            _ => { _.TypeNameHandling = TypeNameHandling.All; });
}

snippet source | anchor

Result:

{
  $type: VerifyObjectSamples.Person,
  GivenNames: John,
  FamilyName: Smith
}

snippet source | anchor

Ignoring a type

To ignore all members that match a certain type:

[Fact]
public Task IgnoreType()
{
    var target = new IgnoreTypeTarget
    {
        ToIgnore = new()
        {
            Property = "Value"
        },
        ToInclude = new()
        {
            Property = "Value"
        }
    };
    var settings = new VerifySettings();
    settings.ModifySerialization(_ => _.IgnoreMembersWithType<ToIgnore>());
    return Verifier.Verify(target, settings);
}

[Fact]
public Task IgnoreTypeFluent()
{
    var target = new IgnoreTypeTarget
    {
        ToIgnore = new()
        {
            Property = "Value"
        },
        ToInclude = new()
        {
            Property = "Value"
        }
    };
    return Verifier.Verify(target)
        .ModifySerialization(_ => _.IgnoreMembersWithType<ToIgnore>());

}

snippet source | anchor

Result:

{
  ToInclude: {
    Property: Value
  }
}

snippet source | anchor

Ignoring a instance

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.ModifySerialization(
        _ => { _.IgnoreInstance<Instance>(x => x.Property == "Ignore"); });
    return Verifier.Verify(target, settings);
}

[Fact]
public Task AddIgnoreInstanceFluent()
{
    var target = new IgnoreInstanceTarget
    {
        ToIgnore = new()
        {
            Property = "Ignore"
        },
        ToInclude = new()
        {
            Property = "Include"
        }
    };
    return Verifier.Verify(target)
        .ModifySerialization(
            _ => { _.IgnoreInstance<Instance>(x => x.Property == "Ignore"); });
}

snippet source | anchor

Result:

{
  ToInclude: {
    Property: Include
  }
}

snippet source | anchor

Obsolete members ignored

Members with an ObsoleteAttribute are ignored:

class WithObsolete
{
    [Obsolete]
    public string ObsoleteProperty { get; set; }

    public string OtherProperty { get; set; }
}

[Fact]
public Task WithObsoleteProp()
{
    var target = new WithObsolete
    {
        ObsoleteProperty = "value1",
        OtherProperty = "value2"
    };
    return Verifier.Verify(target);
}

snippet source | anchor

Result:

{
  OtherProperty: value2
}

snippet source | anchor

Including Obsolete members

Obsolete members can be included using IncludeObsoletes:

[Fact]
public Task WithObsoletePropIncluded()
{
    var target = new WithObsolete
    {
        ObsoleteProperty = "value1",
        OtherProperty = "value2"
    };
    var settings = new VerifySettings();
    settings.ModifySerialization(_ => { _.IncludeObsoletes(); });
    return Verifier.Verify(target, settings);
}

[Fact]
public Task WithObsoletePropIncludedFluent()
{
    var target = new WithObsolete
    {
        ObsoleteProperty = "value1",
        OtherProperty = "value2"
    };
    return Verifier.Verify(target)
        .ModifySerialization(_ => { _.IncludeObsoletes(); });
}

snippet source | anchor

Result:

{
  ObsoleteProperty: value1,
  OtherProperty: value2
}

snippet source | anchor

Ignore member by expressions

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.ModifySerialization(_ =>
    {
        _.IgnoreMember<IgnoreExplicitTarget>(x => x.Property);
        _.IgnoreMember<IgnoreExplicitTarget>(x => x.PropertyWithPropertyName);
        _.IgnoreMember<IgnoreExplicitTarget>(x => x.Field);
        _.IgnoreMember<IgnoreExplicitTarget>(x => x.GetOnlyProperty);
        _.IgnoreMember<IgnoreExplicitTarget>(x => x.PropertyThatThrows);
    });
    return Verifier.Verify(target, settings);
}

[Fact]
public Task IgnoreMemberByExpressionFluent()
{
    var target = new IgnoreExplicitTarget
    {
        Include = "Value",
        Field = "Value",
        Property = "Value"
    };
    return Verifier.Verify(target)
        .ModifySerialization(_ =>
        {
            _.IgnoreMember<IgnoreExplicitTarget>(x => x.Property);
            _.IgnoreMember<IgnoreExplicitTarget>(x => x.Field);
            _.IgnoreMember<IgnoreExplicitTarget>(x => x.GetOnlyProperty);
            _.IgnoreMember<IgnoreExplicitTarget>(x => x.PropertyThatThrows);
        });
}

snippet source | anchor

Result:

{
  Include: Value
}

snippet source | anchor

Ignore member by name

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();
    settings.ModifySerialization(_ =>
    {
        _.IgnoreMember("PropertyByName");
        var type = typeof(IgnoreExplicitTarget);
        _.IgnoreMember(type, "Property");
        _.IgnoreMember(type, "Field");
        _.IgnoreMember(type, "GetOnlyProperty");
        _.IgnoreMember(type, "PropertyThatThrows");
    });
    return Verifier.Verify(target, settings);
}

[Fact]
public Task IgnoreMemberByNameFluent()
{
    var target = new IgnoreExplicitTarget
    {
        Include = "Value",
        Field = "Value",
        Property = "Value",
        PropertyByName = "Value"
    };
    return Verifier.Verify(target)
        .ModifySerialization(_ =>
        {
            _.IgnoreMember("PropertyByName");
            var type = typeof(IgnoreExplicitTarget);
            _.IgnoreMember(type, "Property");
            _.IgnoreMember(type, "Field");
            _.IgnoreMember(type, "GetOnlyProperty");
            _.IgnoreMember(type, "PropertyThatThrows");
        });
}

snippet source | anchor

Result:

{
  Include: Value
}

snippet source | anchor

Members that throw

Members that throw exceptions can be excluded from serialization based on the exception type or properties.

By default members that throw NotImplementedException or NotSupportedException are ignored.

Note that this is global for all members on all types.

Ignore by exception type:

[Fact]
public Task CustomExceptionProp()
{
    var target = new WithCustomException();
    var settings = new VerifySettings();
    settings.ModifySerialization(_ => _.IgnoreMembersThatThrow<CustomException>());
    return Verifier.Verify(target, settings);
}

[Fact]
public Task CustomExceptionPropFluent()
{
    var target = new WithCustomException();
    return Verifier.Verify(target)
        .ModifySerialization(_ => _.IgnoreMembersThatThrow<CustomException>());
}

snippet source | anchor

Result:

{}

snippet source | anchor

Ignore by exception type and expression:

[Fact]
public Task ExceptionMessageProp()
{
    var target = new WithExceptionIgnoreMessage();

    var settings = new VerifySettings();
    settings.ModifySerialization(
        _ => _.IgnoreMembersThatThrow<Exception>(x => x.Message == "Ignore"));
    return Verifier.Verify(target, settings);

}

[Fact]
public Task ExceptionMessagePropFluent()
{
    var target = new WithExceptionIgnoreMessage();

    return Verifier.Verify(target)
        .ModifySerialization(
            _ => _.IgnoreMembersThatThrow<Exception>(x => x.Message == "Ignore"));
}

snippet source | anchor

Result:

{}

snippet source | anchor

TreatAsString

Certain types, when passed directly in to Verify, are written directly without going through json serialization.

The default mapping is:

{typeof(string), (target, _) => (string) target},
{typeof(StringBuilder), (target, _) => ((StringBuilder) target).ToString()},
{typeof(bool), (target, _) => ((bool) target).ToString(CultureInfo.InvariantCulture)},
{typeof(short), (target, _) => ((short) target).ToString(CultureInfo.InvariantCulture)},
{typeof(ushort), (target, _) => ((ushort) target).ToString(CultureInfo.InvariantCulture)},
{typeof(int), (target, _) => ((int) target).ToString(CultureInfo.InvariantCulture)},
{typeof(uint), (target, _) => ((uint) target).ToString(CultureInfo.InvariantCulture)},
{typeof(long), (target, _) => ((long) target).ToString(CultureInfo.InvariantCulture)},
{typeof(ulong), (target, _) => ((ulong) target).ToString(CultureInfo.InvariantCulture)},
{typeof(decimal), (target, _) => ((decimal) target).ToString(CultureInfo.InvariantCulture)},
#if NET5_0_OR_GREATER
{typeof(Half), (target, settings) => ((Half) target).ToString(CultureInfo.InvariantCulture)},
#endif
#if NET6_0_OR_GREATER
{
    typeof(DateOnly), (target, _) =>
    {
        var date = (DateOnly) target;
        return date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
    }
},
{
    typeof(TimeOnly), (target, _) =>
    {
        var time = (TimeOnly) target;
        return time.ToString("h:mm tt", CultureInfo.InvariantCulture);
    }
},
#endif
{typeof(float), (target, _) => ((float) target).ToString(CultureInfo.InvariantCulture)},
{typeof(double), (target, _) => ((double) target).ToString(CultureInfo.InvariantCulture)},
{typeof(Guid), (target, _) => ((Guid) target).ToString()},
{
    typeof(DateTime), (target, _) =>
    {
        var dateTime = (DateTime) target;
        return dateTime.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFFz");
    }
},
{
    typeof(DateTimeOffset), (target, _) =>
    {
        var dateTimeOffset = (DateTimeOffset) target;
        return dateTimeOffset.ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFFz", CultureInfo.InvariantCulture);
    }
},
{
    typeof(XmlNode), (target, settings) =>
    {
        var converted = (XmlNode) target;
        var document = XDocument.Parse(converted.OuterXml);
        return new(document.ToString(), "xml");
    }
},
{
    typeof(XDocument), (target, settings) =>
    {
        var converted = (XDocument) target;
        return new(converted.ToString(), "xml");
    }
},
{
    typeof(XElement), (target, settings) =>
    {
        var converted = (XElement) target;
        return new(converted.ToString(), "xml");
    }
}

snippet source | anchor

This bypasses the Guid and DateTime scrubbing mentioned above.

Extra types can be added to this mapping:

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

snippet source | anchor

JsonAppender

A JsonAppender allows extra content (key value pairs) to be optionally appended to the output being verified. JsonAppenders can use the current context to determine what should be appended or if anything should be appended.

Register a JsonAppender:

VerifierSettings.RegisterJsonAppender(
    context =>
    {
        if (ShouldInclude(context))
        {
            return new ToAppend("theData", "theValue");
        }

        return null;
    });

snippet source | anchor

When when content is verified:

[Fact]
public Task WithJsonAppender()
{
    return Verifier.Verify("TheValue");
}

snippet source | anchor

The content from RegisterJsonAppender will be included in the output:

{
  target: TheValue,
  theData: theValue
}

snippet source | anchor

If the target is a stream or binary file:

[Fact]
public Task Stream()
{
    return Verifier.Verify(FileHelpers.OpenRead("sample.txt"));
}

snippet source | anchor

Then the appended content will be added to the *.00.verified.txt file:

{
  target: null,
  theData: theValue
}

snippet source | anchor

See Converters for more information on *.00.verified.txt files.

Examples of extensions using JsonAppenders are Recorders in Verify.SqlServer and Recorders in Verify.EntityFramework.