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 656af71
Show file tree
Hide file tree
Showing 3 changed files with 242 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;

Check warning on line 7 in Src/FluentAssertions/Json/ObjectAssertionsJsonExtensions.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Namespace does not correspond to file location

Namespace does not correspond to file location, must be: 'FluentAssertions.Json'

/// <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>()

Check warning on line 78 in Src/FluentAssertions/Json/ObjectAssertionsJsonExtensions.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Unused local variable

Local variable 'defaultOptions' is never used
.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 @@ -334,6 +334,14 @@ namespace FluentAssertions
public static FluentAssertions.AndConstraint<FluentAssertions.Primitives.ObjectAssertions> BeDataContractSerializable<T>(this FluentAssertions.Primitives.ObjectAssertions assertions, System.Func<FluentAssertions.Equivalency.EquivalencyOptions<T>, FluentAssertions.Equivalency.EquivalencyOptions<T>> options, string because = "", params object[] becauseArgs) { }
public static FluentAssertions.AndConstraint<FluentAssertions.Primitives.ObjectAssertions> BeXmlSerializable(this FluentAssertions.Primitives.ObjectAssertions assertions, string because = "", params object[] becauseArgs) { }
}
public static class ObjectAssertionsJsonExtensions
{
public static FluentAssertions.AndConstraint<FluentAssertions.Primitives.ObjectAssertions> BeJsonSerializable(this FluentAssertions.Primitives.ObjectAssertions assertions, string because = "", params object[] becauseArgs) { }
public static FluentAssertions.AndConstraint<FluentAssertions.Primitives.ObjectAssertions> BeJsonSerializable<T>(this FluentAssertions.Primitives.ObjectAssertions assertions, string because = "", params object[] becauseArgs)
where T : class { }
public static FluentAssertions.AndConstraint<FluentAssertions.Primitives.ObjectAssertions> BeJsonSerializable<T>(this FluentAssertions.Primitives.ObjectAssertions assertions, System.Func<FluentAssertions.Equivalency.EquivalencyOptions<T>, FluentAssertions.Equivalency.EquivalencyOptions<T>> options, string because = "", params object[] becauseArgs)
where T : class { }
}
public abstract class OccurrenceConstraint
{
protected OccurrenceConstraint(int expectedCount) { }
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; }

Check warning on line 77 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'Id.get' is never used

public Guid GlobalId { get; set; }

Check warning on line 79 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'GlobalId.get' is never used

public string Name { get; set; }

Check warning on line 81 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'Name.get' is never used

public DateTime DateOfBirth { get; set; }

Check warning on line 83 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'DateOfBirth.get' is never used

public decimal Height { get; set; }

Check warning on line 85 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'Height.get' is never used

public double Weight { get; set; }

Check warning on line 87 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'Weight.get' is never used

public float ShoeSize { get; set; }

Check warning on line 89 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'ShoeSize.get' is never used

public bool IsActive { get; set; }

Check warning on line 91 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'IsActive.get' is never used

#pragma warning disable CA1819 // Properties should not return arrays
public byte[] Image { get; set; }

Check warning on line 94 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'Image.get' is never used
#pragma warning restore CA1819 // Properties should not return arrays

public char Category { get; set; }

Check warning on line 97 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'Category.get' is never used
}

private class ClassWithoutDefaultConstructor
{
public int Id { get; }

Check warning on line 102 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'Id.get' is never used

public ClassWithoutDefaultConstructor(int value)
{
Id = value;
}
}

private class ClassWithNestedClasses
{
public int Id { get; set; }

Check warning on line 112 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'Id.get' is never used

public AddressDto Address { get; set; }

Check warning on line 114 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'Address.get' is never used

public EmploymentDto Employment { get; set; }

Check warning on line 116 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'Employment.get' is never used
}

private class AddressDto
{
public string AddressLine { get; set; }

Check warning on line 121 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'AddressLine.get' is never used
}

private class EmploymentDto
{
public string JobTitle { get; set; }

Check warning on line 126 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'JobTitle.get' is never used

public string PhoneNumber { get; set; }

Check warning on line 128 in Tests/FluentAssertions.Specs/Json/ObjectAssertionsJsonExtensionsSpecs.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (private accessibility)

Auto-property accessor 'PhoneNumber.get' is never used
}
}
#endif

0 comments on commit 656af71

Please sign in to comment.