Skip to content

Commit

Permalink
Add BeJsonSerializable extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
vbreuss committed Jan 14, 2024
1 parent 1c7e207 commit 3e6e081
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 0 deletions.
103 changes: 103 additions & 0 deletions Src/FluentAssertions/Json/ObjectAssertionsJsonExtensions.cs
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
Original file line number Diff line number Diff line change
Expand Up @@ -2115,6 +2115,8 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> BeOneOf(params string[] validValues) { }
public FluentAssertions.AndConstraint<TAssertions> BeOneOf(System.Collections.Generic.IEnumerable<string> validValues, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeUpperCased(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, System.Text.Json.JsonDocument> BeValidJson(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, System.Text.Json.JsonDocument> BeValidJson(System.Text.Json.JsonDocumentOptions options, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> Contain(string expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> Contain(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> ContainAll(params string[] values) { }
Expand Down
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

0 comments on commit 3e6e081

Please sign in to comment.