-
-
Notifications
You must be signed in to change notification settings - Fork 539
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
236 additions
and
0 deletions.
There are no files selected for viewing
103 changes: 103 additions & 0 deletions
103
Src/FluentAssertions/Json/ObjectAssertionsJsonExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
#if NET6_0_OR_GREATER | ||
using System; | ||
using FluentAssertions.Equivalency; | ||
using FluentAssertions.Execution; | ||
using FluentAssertions.Primitives; | ||
|
||
namespace FluentAssertions; | ||
|
||
/// <summary> | ||
/// Extension methods on <see cref="ObjectAssertions"/> for JSON functionality. | ||
/// </summary> | ||
public static class ObjectAssertionsJsonExtensions | ||
{ | ||
/// <summary> | ||
/// Asserts that an object can be serialized to and deserialized from JSON and retains the values of all members. | ||
/// </summary> | ||
/// <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 <paramref name="because" />. | ||
/// </param> | ||
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 to and deserialized from JSON and retains the values of all members. | ||
/// </summary> | ||
/// <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 <paramref name="because" />. | ||
/// </param> | ||
public static AndConstraint<ObjectAssertions> BeJsonSerializable<T>(this ObjectAssertions assertions, string because = "", params object[] becauseArgs) | ||
where T : class | ||
{ | ||
return BeJsonSerializable<T>(assertions, options => options, because, becauseArgs); | ||
} | ||
|
||
/// <summary> | ||
/// Asserts that an object can be serialized to and deserialized from JSON and retains the values of all members. | ||
/// </summary> | ||
/// <param name="options"> | ||
/// The equivalency 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 <paramref name="because" />. | ||
/// </param> | ||
public static AndConstraint<ObjectAssertions> BeJsonSerializable<T>(this ObjectAssertions assertions, Func<EquivalencyOptions<T>, EquivalencyOptions<T>> options, | ||
string because = "", params object[] becauseArgs) | ||
where T : class | ||
{ | ||
Execute.Assertion.ForCondition(assertions.Subject is not null) | ||
.BecauseOf(because, becauseArgs) | ||
.FailWith("Expected {context:object} to be JSON serializable{reason}, but found is null."); | ||
|
||
if (assertions.Subject is not T typedSubject) | ||
{ | ||
Execute.Assertion | ||
.BecauseOf(because, becauseArgs) | ||
.FailWith("Expected {context:object} to be JSON serializable{reason}, but {context:object} is not assignable to {0}", typeof(T)); | ||
typedSubject = default; | ||
} | ||
|
||
try | ||
{ | ||
var deserializedObject = CreateCloneUsingJsonSerializer(assertions.Subject); | ||
|
||
var defaultOptions = AssertionOptions.CloneDefaults<T>() | ||
.RespectingRuntimeTypes() | ||
.IncludingFields() | ||
.IncludingProperties(); | ||
|
||
((T)deserializedObject).Should().BeEquivalentTo(typedSubject, options); | ||
} | ||
#pragma warning disable CA1031 // Ignore catching general exception | ||
catch (Exception exc) | ||
#pragma warning restore CA1031 // Ignore catching general exception | ||
{ | ||
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 = System.Text.Json.JsonSerializer.Serialize(subject); | ||
return System.Text.Json.JsonSerializer.Deserialize(serializedObject, subject.GetType()); | ||
} | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
131 changes: 131 additions & 0 deletions
131
Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
#if NET6_0_OR_GREATER | ||
using System; | ||
using Xunit; | ||
|
||
namespace FluentAssertions.Specs.Json; | ||
|
||
public class ObjectAssertionsJsonExtensionsSpecs | ||
{ | ||
public class BeJsonSerializable | ||
{ | ||
[Fact] | ||
public void Can_serialize_simple_classes() | ||
{ | ||
// Arrange | ||
var target = new SimpleClassWithPrimitiveTypes() | ||
{ | ||
Id = 1, | ||
GlobalId = Guid.NewGuid(), | ||
Name = "foo", | ||
DateOfBirth = DateTime.UtcNow, | ||
Height = 0.1M, | ||
Weight = 0.2, | ||
ShoeSize = 42.0F, | ||
IsActive = true, | ||
Image = [0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26], | ||
Category = 'c' | ||
}; | ||
|
||
// Act / Assert | ||
target.Should().BeJsonSerializable(); | ||
} | ||
|
||
[Fact] | ||
public void Can_serialize_nested_classes() | ||
{ | ||
// Arrange | ||
var target = new ClassWithNestedClasses | ||
{ | ||
Id = 1, | ||
Address = new AddressDto | ||
{ | ||
AddressLine = "foo" | ||
}, | ||
Employment = new EmploymentDto | ||
{ | ||
JobTitle = "foo", | ||
PhoneNumber = "bar" | ||
} | ||
}; | ||
|
||
// Act / Assert | ||
target.Should().BeJsonSerializable(); | ||
} | ||
|
||
[Fact] | ||
public void Cannot_serialize_classes_without_default_constructor() | ||
{ | ||
// Arrange | ||
const string reasonText = "this is the reason"; | ||
var target = new ClassWithoutDefaultConstructor(10); | ||
|
||
// 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"); | ||
} | ||
} | ||
|
||
private class SimpleClassWithPrimitiveTypes | ||
{ | ||
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; } | ||
|
||
#pragma warning disable CA1819 // Properties should not return arrays | ||
public byte[] Image { get; set; } | ||
#pragma warning restore CA1819 // Properties should not return arrays | ||
|
||
public char Category { get; set; } | ||
} | ||
|
||
private class ClassWithoutDefaultConstructor | ||
{ | ||
public int Id { get; } | ||
|
||
public ClassWithoutDefaultConstructor(int value) | ||
{ | ||
Id = value; | ||
} | ||
} | ||
|
||
private class ClassWithNestedClasses | ||
{ | ||
public int Id { get; set; } | ||
|
||
public AddressDto Address { get; set; } | ||
|
||
public EmploymentDto Employment { get; set; } | ||
} | ||
|
||
private class AddressDto | ||
{ | ||
public string AddressLine { get; set; } | ||
} | ||
|
||
private class EmploymentDto | ||
{ | ||
public string JobTitle { get; set; } | ||
|
||
public string PhoneNumber { get; set; } | ||
} | ||
} | ||
#endif |