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 436a127
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 0 deletions.
85 changes: 85 additions & 0 deletions Src/FluentAssertions/Primitives/ObjectAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,91 @@ public AndConstraint<TAssertions> BeOneOf(params TSubject[] validValues)
return new AndConstraint<TAssertions>((TAssertions)this);
}

#if NET6_0_OR_GREATER
/// <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 AndConstraint<TAssertions> BeJsonSerializable(string because = "", params object[] becauseArgs)
{
return BeJsonSerializable<object>(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 AndConstraint<TAssertions> BeJsonSerializable<T>(string because = "", params object[] becauseArgs)
where T : class
{
return BeJsonSerializable<T>(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 AndConstraint<TAssertions> BeJsonSerializable<T>(Func<EquivalencyOptions<T>, EquivalencyOptions<T>> options,
string because = "", params object[] becauseArgs)
where T : class
{
static object CreateCloneUsingJsonSerializer(object subject)
{
var serializedObject = System.Text.Json.JsonSerializer.Serialize(subject);
return System.Text.Json.JsonSerializer.Deserialize(serializedObject, subject.GetType());
}

Execute.Assertion.ForCondition(Subject is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:object} to be JSON serializable{reason}, but found is null.");

if (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(Subject);
((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}", Subject, exc);
}

return new AndConstraint<TAssertions>((TAssertions)this);
}
#endif

/// <inheritdoc/>
public override bool Equals(object obj) =>
throw new NotSupportedException("Equals is not part of Fluent Assertions. Did you mean Be() or BeSameAs() instead?");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2044,6 +2044,11 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> Be(TSubject expected, System.Collections.Generic.IEqualityComparer<TSubject> comparer, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeEquivalentTo<TExpectation>(TExpectation expectation, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeEquivalentTo<TExpectation>(TExpectation expectation, System.Func<FluentAssertions.Equivalency.EquivalencyOptions<TExpectation>, FluentAssertions.Equivalency.EquivalencyOptions<TExpectation>> config, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeJsonSerializable(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeJsonSerializable<T>(string because = "", params object[] becauseArgs)
where T : class { }
public FluentAssertions.AndConstraint<TAssertions> BeJsonSerializable<T>(System.Func<FluentAssertions.Equivalency.EquivalencyOptions<T>, FluentAssertions.Equivalency.EquivalencyOptions<T>> options, string because = "", params object[] becauseArgs)
where T : class { }
public FluentAssertions.AndConstraint<TAssertions> BeOneOf(params TSubject[] validValues) { }
public FluentAssertions.AndConstraint<TAssertions> BeOneOf(System.Collections.Generic.IEnumerable<TSubject> validValues, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeOneOf(System.Collections.Generic.IEnumerable<TSubject> validValues, System.Collections.Generic.IEqualityComparer<TSubject> comparer, string because = "", params object[] becauseArgs) { }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#if NET6_0_OR_GREATER
using System;
using System.Text.Json.Serialization;
using Xunit;

namespace FluentAssertions.Specs.Primitives;

public partial class ObjectAssertionSpecs
{
public class BeJsonSerializable
{
[Fact]
public void Can_serialize_simple_classes()
{
// Arrange
var subject = new SimpleClassWithPrimitiveTypes()
{
Integer = 1,
Long = long.MaxValue - 2,
Decimal = 0.2M,
Double = 0.3,
Float = 42.0F,
Guid = Guid.NewGuid(),
String = "foo",
DateTime = DateTime.UtcNow,
Bool = true,
ByteArray = [0x20, 0x21, 0x22, 0x23],
Char = 'c'
};

// Act / Assert
subject.Should().BeJsonSerializable();
}

[Fact]
public void Can_serialize_nested_classes()
{
// Arrange
var subject = new ClassWithNestedClasses
{
Id = 1,
MestedClass = new NestedClass
{
Value = "foo"
}
};

// Act / Assert
subject.Should().BeJsonSerializable();
}

[Fact]
public void Cannot_serialize_classes_without_default_constructor()
{
// Arrange
const string reasonText = "this is the reason";
var subject = new ClassWithoutDefaultConstructor(10);

// Act
Action act = () => subject.Should().BeJsonSerializable(reasonText);

// Assert
act.Should().Throw<Xunit.Sdk.XunitException>()
.WithMessage($"*to be JSON serializable*{reasonText}*but serializing*failed with*");
}

[Fact]
public void Cannot_serialize_classes_with_ignored_property()
{
// Arrange
const string reasonText = "it has an ignored property";
var subject = new SimpleClassWithIgnoredProperty()
{
IgnoredProperty = DateTime.Now
};

// Act
Action act = () => subject.Should().BeJsonSerializable(reasonText);

// Assert
act.Should().Throw<Xunit.Sdk.XunitException>()
.WithMessage($"*to be JSON serializable*{reasonText}*but serializing*failed with*");
}

[Fact]
public void Use_explicit_type_for_serialization()
{
// Arrange
var subject = new SimpleClassWithIgnoredProperty()
{
IgnoredProperty = DateTime.Now
};

// Act / Assert
subject.Should().BeJsonSerializable<SimpleClassWithPrimitiveTypes>();
}

// ReSharper disable UnusedAutoPropertyAccessor.Local
private class SimpleClassWithIgnoredProperty : SimpleClassWithPrimitiveTypes
{
[JsonIgnore]
public DateTime IgnoredProperty { get; set; }
}

private class SimpleClassWithPrimitiveTypes
{
public int Integer { get; set; }

public long Long { get; set; }

public Guid Guid { get; set; }

public string String { get; set; }

public DateTime DateTime { get; set; }

public decimal Decimal { get; set; }

public double Double { get; set; }

public float Float { get; set; }

public bool Bool { get; set; }

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

public char Char { get; set; }
}

private class ClassWithoutDefaultConstructor
{
public int Id { get; }

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

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

public NestedClass MestedClass { get; set; }
}

private class NestedClass
{
public string Value { get; set; }
}
}
}
#endif

0 comments on commit 436a127

Please sign in to comment.