-
Notifications
You must be signed in to change notification settings - Fork 30
Added ShouldBeJsonSerializable #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cf2ad50
9fbe7d6
c4ee603
7efc004
f5e633f
5f79e0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| using System; | ||
| using FluentAssertions.Equivalency; | ||
| using FluentAssertions.Execution; | ||
| using FluentAssertions.Primitives; | ||
| using Newtonsoft.Json; | ||
|
|
||
| namespace FluentAssertions.Json | ||
| { | ||
| /// <summary> | ||
| /// Contains extension methods for JSON serialization assertion methods | ||
| /// </summary> | ||
| public static class ObjectAssertionsExtensions | ||
| { | ||
| /// <summary> | ||
| /// Asserts that an object can be serialized and deserialized using the JSON serializer and that it stills retains | ||
| /// the values of all members. | ||
| /// </summary> | ||
| /// <param name="assertions"></param> | ||
| /// <param name="because"> | ||
| /// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion | ||
| /// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically. | ||
| /// </param> | ||
| /// <param name="becauseArgs"> | ||
| /// Zero or more objects to format using the placeholders in <see cref="because" />. | ||
| /// </param> | ||
| [CustomAssertion] | ||
| public static AndConstraint<ObjectAssertions> BeJsonSerializable(this ObjectAssertions assertions, string because = "", params object[] becauseArgs) | ||
| { | ||
| return BeJsonSerializable<object>(assertions, options => options, because, becauseArgs); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Asserts that an object can be serialized and deserialized using the JSON serializer and that it stills retains | ||
| /// the values of all members. | ||
| /// </summary> | ||
| /// <param name="assertions"></param> | ||
| /// <param name="because"> | ||
| /// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion | ||
| /// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically. | ||
| /// </param> | ||
| /// <param name="becauseArgs"> | ||
| /// Zero or more objects to format using the placeholders in <see cref="because" />. | ||
| /// </param> | ||
| [CustomAssertion] | ||
| public static AndConstraint<ObjectAssertions> BeJsonSerializable<T>(this ObjectAssertions assertions, string because = "", params object[] becauseArgs) | ||
| { | ||
| return BeJsonSerializable<T>(assertions, options => options, because, becauseArgs); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Asserts that an object can be serialized and deserialized using the JSON serializer and that it stills retains | ||
| /// the values of all members. | ||
| /// </summary> | ||
| /// <param name="options"></param> | ||
| /// <param name="because"> | ||
| /// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion | ||
| /// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically. | ||
| /// </param> | ||
| /// <param name="becauseArgs"> | ||
| /// Zero or more objects to format using the placeholders in <see cref="because" />. | ||
| /// </param> | ||
| /// <param name="assertions"></param> | ||
| [CustomAssertion] | ||
| public static AndConstraint<ObjectAssertions> BeJsonSerializable<T>(this ObjectAssertions assertions, Func<EquivalencyAssertionOptions<T>, EquivalencyAssertionOptions<T>> options, string because = "", params object[] becauseArgs) | ||
| { | ||
| Execute.Assertion.ForCondition(assertions.Subject != null) | ||
| .BecauseOf(because, becauseArgs) | ||
| .FailWith("Expected {context:object} to be JSON serializable{reason}, but the value is null. Please provide a value for the assertion."); | ||
|
|
||
| Execute.Assertion.ForCondition(assertions.Subject is T) | ||
| .BecauseOf(because, becauseArgs) | ||
| .FailWith("Expected {context:object} to be JSON serializable{reason}, but {context:object} is not assignable to {0}", typeof(T)); | ||
|
|
||
| try | ||
| { | ||
| var deserializedObject = CreateCloneUsingJsonSerializer(assertions.Subject); | ||
|
|
||
| var defaultOptions = AssertionOptions.CloneDefaults<T>() | ||
| .RespectingRuntimeTypes() | ||
| .IncludingFields() | ||
| .IncludingProperties(); | ||
|
|
||
| var typedSubject = (T)assertions.Subject; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 What if the subject is not of type
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With respect to what to do if the subject is Not quite sure on the fail message, but it gets the point across I think
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that's sloppy old code. The main library will do exactly what you suggested.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done :-) |
||
| ((T)deserializedObject).Should().BeEquivalentTo(typedSubject, _ => options(defaultOptions)); | ||
| } | ||
| catch (Exception exc) | ||
| { | ||
| Execute.Assertion | ||
| .BecauseOf(because, becauseArgs) | ||
| .FailWith("Expected {context:object} to be JSON serializable{reason}, but serializing {0} failed with {1}", assertions.Subject, exc); | ||
| } | ||
|
|
||
| return new AndConstraint<ObjectAssertions>(assertions); | ||
| } | ||
|
|
||
| private static object CreateCloneUsingJsonSerializer(object subject) | ||
| { | ||
| var serializedObject = JsonConvert.SerializeObject(subject); | ||
| var cloneUsingJsonSerializer = JsonConvert.DeserializeObject(serializedObject, subject.GetType()); | ||
| return cloneUsingJsonSerializer; | ||
| } | ||
|
Comment on lines
+96
to
+101
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make any difference if we use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like using typeof(T) causes issues with the non-generic overload of BeJsonSerialisable as |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| namespace FluentAssertions.Json.Specs.Models | ||
| { | ||
| // ReSharper disable UnusedMember.Global | ||
|
|
||
| public class AddressDto | ||
| { | ||
| public string AddressLine1{ get; set; } | ||
| public string AddressLine2{ get; set; } | ||
| public string AddressLine3{ get; set; } | ||
| } | ||
| // ReSharper restore UnusedMember.Global | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| namespace FluentAssertions.Json.Specs.Models | ||
| { | ||
| // ReSharper disable UnusedMember.Global | ||
|
|
||
| public class EmploymentDto | ||
| { | ||
| public string JobTitle{ get; set; } | ||
| public string PhoneNumber { get; set; } | ||
| } | ||
| // ReSharper restore UnusedMember.Global | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| using Newtonsoft.Json; | ||
|
|
||
| namespace FluentAssertions.Json.Specs.Models | ||
| { | ||
| public class PocoWithIgnoredProperty | ||
| { | ||
| public int Id { get; set; } | ||
|
|
||
| [JsonIgnore] | ||
| public string Name{ get; set; } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| namespace FluentAssertions.Json.Specs.Models | ||
| { | ||
| public class PocoWithNoDefaultConstructor | ||
| { | ||
| public int Id { get; } | ||
|
|
||
| /// <summary> | ||
| /// Newtonsoft.Json will deserialise this successfully if the parameter name id the same as the property | ||
| /// </summary> | ||
| /// <param name="value">DO NOT CHANGE THE NAME OF THIS PARAMETER</param> | ||
| public PocoWithNoDefaultConstructor(int value) | ||
| { | ||
| Id = value; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| namespace FluentAssertions.Json.Specs.Models | ||
| { | ||
| // ReSharper disable UnusedMember.Global | ||
| public class PocoWithStructure | ||
| { | ||
| public int Id{ get; set; } | ||
| public AddressDto Address { get; set; } | ||
| public EmploymentDto Employment { get; set; } | ||
| } | ||
| // ReSharper restore UnusedMember.Global | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| using System; | ||
|
|
||
| namespace FluentAssertions.Json.Specs.Models | ||
| { | ||
| // ReSharper disable UnusedMember.Global | ||
| public class SimplePocoWithPrimitiveTypes | ||
| { | ||
| public int Id { get; set; } | ||
| public Guid GlobalId { get; set; } | ||
| public string Name { get; set; } | ||
| public DateTime DateOfBirth { get; set; } | ||
| public decimal Height { get; set; } | ||
| public double Weight { get; set; } | ||
| public float ShoeSize { get; set; } | ||
| public bool IsActive { get; set; } | ||
|
|
||
| public byte[] Image { get; set; } | ||
| public char Category { get; set; } | ||
| } | ||
| // ReSharper restore UnusedMember.Global | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| using System; | ||
| using AutoFixture; | ||
| using FluentAssertions; | ||
| using FluentAssertions.Json; | ||
| using FluentAssertions.Json.Specs.Models; | ||
| using Xunit; | ||
|
|
||
| // NOTE that we are using both namespaces 'FluentAssertions' & 'FluentAssertions.Json' from an external namespace to force compiler disambiguation warnings | ||
| // ReSharper disable CheckNamespace | ||
| namespace SomeOtherNamespace | ||
| // ReSharper restore CheckNamespace | ||
| { | ||
| public class ShouldBeJsonSerializableTests | ||
| { | ||
| private readonly Fixture _fixture; | ||
|
|
||
| public ShouldBeJsonSerializableTests() | ||
| { | ||
| _fixture = new Fixture(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Simple_poco_should_be_serializable() | ||
| { | ||
| // arrange | ||
| var target = _fixture.Create<SimplePocoWithPrimitiveTypes>(); | ||
|
|
||
| // act | ||
| Action act = () => target.Should().BeJsonSerializable(); | ||
|
|
||
| // assert | ||
| act.Should().NotThrow(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Complex_poco_should_be_serializable() | ||
| { | ||
| // arrange | ||
| var target = _fixture.Create<PocoWithStructure>(); | ||
|
|
||
| // act | ||
| Action act = () => target.Should().BeJsonSerializable(); | ||
|
|
||
| // assert | ||
| act.Should().NotThrow(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Class_that_does_not_have_default_constructor_should_not_be_serializable() | ||
| { | ||
| // arrange | ||
| const string reasonText = "this is the reason"; | ||
| var target = _fixture.Create<PocoWithNoDefaultConstructor>(); | ||
|
|
||
| // act | ||
| Action act = () => target.Should().BeJsonSerializable(reasonText); | ||
|
|
||
| // assert | ||
| act.Should().Throw<Xunit.Sdk.XunitException>() | ||
| .Which.Message.Should() | ||
| .Contain("to be JSON serializable") | ||
| .And.Contain(reasonText) | ||
| .And.Contain("but serializing") | ||
| .And.Contain("failed with"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Class_that_has_ignored_property_should_not_be_serializable_if_equivalency_options_are_not_configured() | ||
| { | ||
| // arrange | ||
| const string reasonText = "this is the reason"; | ||
| var target = _fixture.Create<PocoWithIgnoredProperty>(); | ||
|
|
||
| // act | ||
| Action act = () => target.Should().BeJsonSerializable(reasonText); | ||
|
|
||
| // assert | ||
| act.Should().Throw<Xunit.Sdk.XunitException>() | ||
| .Which.Message.Should() | ||
| .Contain("to be JSON serializable") | ||
| .And.Contain(reasonText) | ||
| .And.Contain("but serializing") | ||
| .And.Contain("failed with"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Class_that_has_ignored_property_should_be_serializable_when_equivalency_options_are_configured() | ||
| { | ||
| // arrange | ||
| var target = _fixture.Create<PocoWithIgnoredProperty>(); | ||
|
|
||
| // act | ||
| Action act = () => target.Should().BeJsonSerializable<PocoWithIgnoredProperty>(opts => opts.Excluding(p => p.Name)); | ||
|
|
||
| // assert | ||
| act.Should().NotThrow(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_fail_when_instance_is_null() | ||
| { | ||
| // arrange | ||
| const SimplePocoWithPrimitiveTypes target = null; | ||
|
|
||
| // act | ||
| Action act = () => target.Should().BeJsonSerializable(); | ||
|
|
||
| // assert | ||
| act.Should() | ||
| .Throw<Xunit.Sdk.XunitException>(because:"This is consistent with BeBinarySerializable() and BeDataContractSerializable()") | ||
| .Which.Message | ||
| .Should().Contain("value is null") | ||
| .And.Contain("Please provide a value for the assertion"); | ||
|
Comment on lines
+111
to
+113
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tip, it should be possible to shorten this to: .WithMessage("*value is null*Please provide a value for the assertion*"); |
||
| } | ||
|
|
||
| [Fact] | ||
| public void Should_fail_when_subject_is_not_same_type_as_the_specified_generic_type() | ||
| { | ||
| // arrange | ||
| var target = new AddressDto(); | ||
|
|
||
| // act | ||
| Action act = () => target.Should().BeJsonSerializable<SimplePocoWithPrimitiveTypes>(); | ||
|
|
||
| // assert | ||
| act.Should().Throw<Xunit.Sdk.XunitException>(because: "This is consistent with BeBinarySerializable() and BeDataContractSerializable()") | ||
| .Which.Message | ||
| .Should().Contain("is not assignable to") | ||
| .And.Contain(nameof(SimplePocoWithPrimitiveTypes)); | ||
| ; | ||
| } | ||
|
|
||
| } | ||
|
|
||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"stills retains" -> "still retains"