Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,22 @@ var expected = JToken.Parse(@"{ ""value"" : 1.4 }");
actual.Should().BeEquivalentTo(expected, options => options
.Using<double>(d => d.Subject.Should().BeApproximately(d.Expectation, 0.1))
.WhenTypeIs<double>());
```
```

Also, there is `WithoutStrictOrdering()` which allows you to compare JSON arrays while ignoring the order of their elements.
This is useful when the sequence of items is not important for your test scenario. When applied, assertions like `BeEquivalentTo()` will
succeed as long as the arrays contain the same elements, regardless of their order.

Example:

```c#
using FluentAssertions;
using FluentAssertions.Json;
using Newtonsoft.Json.Linq;

...
var actual = JToken.Parse(@"{ ""array"" : [1, 2, 3] }");
var expected = JToken.Parse(@"{ ""array"" : [3, 2, 1] }");
actual.Should().BeEquivalentTo(expected, options => options
.WithoutStrictOrdering());
```
23 changes: 23 additions & 0 deletions Src/FluentAssertions.Json/Common/JTokenExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Linq;
using Newtonsoft.Json.Linq;

namespace FluentAssertions.Json.Common
{
internal static class JTokenExtensions
{
/// <summary>
/// Recursively sorts the properties of JObject instances by name and
/// the elements of JArray instances by their string representation,
/// producing a normalized JToken for consistent comparison
/// </summary>
public static JToken Normalize(this JToken token)
{
return token switch
{
JObject obj => new JObject(obj.Properties().OrderBy(p => p.Name).Select(p => new JProperty(p.Name, Normalize(p.Value)))),
JArray array => new JArray(array.Select(Normalize).OrderBy(x => x.ToString(Newtonsoft.Json.Formatting.None))),
_ => token
Comment on lines 21 to 23
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ (nitpick) I would prefer to have this spelled out instead of a switch statement for readability

};
}
}
}
5 changes: 5 additions & 0 deletions Src/FluentAssertions.Json/IJsonAssertionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@ public interface IJsonAssertionOptions<T>
/// The assertion to execute when the predicate is met.
/// </param>
IJsonAssertionRestriction<T, TProperty> Using<TProperty>(Action<IAssertionContext<TProperty>> action);

/// <summary>
/// Configures the JSON assertion to ignore the order of elements in arrays or collections during comparison, allowing for equivalency checks regardless of element sequence.
/// </summary>
IJsonAssertionOptions<T> WithoutStrictOrdering();
}
}
9 changes: 9 additions & 0 deletions Src/FluentAssertions.Json/JTokenDifferentiator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions.Execution;
using FluentAssertions.Json.Common;
using Newtonsoft.Json.Linq;

namespace FluentAssertions.Json
Expand All @@ -11,12 +12,14 @@ internal class JTokenDifferentiator
private readonly bool ignoreExtraProperties;

private readonly Func<IJsonAssertionOptions<object>, IJsonAssertionOptions<object>> config;
private readonly JsonAssertionOptions<object> options;

public JTokenDifferentiator(bool ignoreExtraProperties,
Func<IJsonAssertionOptions<object>, IJsonAssertionOptions<object>> config)
{
this.ignoreExtraProperties = ignoreExtraProperties;
this.config = config;
this.options = (JsonAssertionOptions<object>)config(new JsonAssertionOptions<object>());
}

public Difference FindFirstDifference(JToken actual, JToken expected)
Expand All @@ -38,6 +41,12 @@ public Difference FindFirstDifference(JToken actual, JToken expected)
return new Difference(DifferenceKind.ExpectedIsNull, path);
}

if (!options.IsStrictlyOrdered)
{
actual = actual.Normalize();
expected = expected.Normalize();
}

return FindFirstDifference(actual, expected, path);
}

Expand Down
14 changes: 12 additions & 2 deletions Src/FluentAssertions.Json/JsonAssertionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,27 @@
namespace FluentAssertions.Json
{
/// <summary>
/// Represents the run-time type-specific behavior of a JSON structural equivalency assertion. It is the equivalent of <see cref="FluentAssertions.Equivalency.EquivalencyAssertionOptions{T}"/>
/// Represents the run-time type-specific behavior of a JSON structural equivalency assertion. It is the equivalent of <see cref="FluentAssertions.Equivalency.EquivalencyOptions{T}"/>
/// </summary>
public sealed class JsonAssertionOptions<T> : EquivalencyOptions<T>, IJsonAssertionOptions<T>
{
internal JsonAssertionOptions() { }

public JsonAssertionOptions(EquivalencyOptions<T> equivalencyAssertionOptions) : base(equivalencyAssertionOptions)
{

}

internal bool IsStrictlyOrdered { get; private set; } = true;

public new IJsonAssertionRestriction<T, TProperty> Using<TProperty>(Action<IAssertionContext<TProperty>> action)
{
return new JsonAssertionRestriction<T, TProperty>(base.Using(action));
}

public new IJsonAssertionOptions<T> WithoutStrictOrdering()
{
IsStrictlyOrdered = false;
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace FluentAssertions.Json
public interface IJsonAssertionOptions<T>
{
FluentAssertions.Json.IJsonAssertionRestriction<T, TProperty> Using<TProperty>(System.Action<FluentAssertions.Equivalency.IAssertionContext<TProperty>> action);
FluentAssertions.Json.IJsonAssertionOptions<T> WithoutStrictOrdering();
}
public interface IJsonAssertionRestriction<T, TMember>
{
Expand Down Expand Up @@ -50,6 +51,7 @@ namespace FluentAssertions.Json
{
public JsonAssertionOptions(FluentAssertions.Equivalency.EquivalencyOptions<T> equivalencyAssertionOptions) { }
public FluentAssertions.Json.IJsonAssertionRestriction<T, TProperty> Using<TProperty>(System.Action<FluentAssertions.Equivalency.IAssertionContext<TProperty>> action) { }
public FluentAssertions.Json.IJsonAssertionOptions<T> WithoutStrictOrdering() { }
}
public sealed class JsonAssertionRestriction<T, TProperty> : FluentAssertions.Json.IJsonAssertionRestriction<T, TProperty>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace FluentAssertions.Json
public interface IJsonAssertionOptions<T>
{
FluentAssertions.Json.IJsonAssertionRestriction<T, TProperty> Using<TProperty>(System.Action<FluentAssertions.Equivalency.IAssertionContext<TProperty>> action);
FluentAssertions.Json.IJsonAssertionOptions<T> WithoutStrictOrdering();
}
public interface IJsonAssertionRestriction<T, TMember>
{
Expand Down Expand Up @@ -50,6 +51,7 @@ namespace FluentAssertions.Json
{
public JsonAssertionOptions(FluentAssertions.Equivalency.EquivalencyOptions<T> equivalencyAssertionOptions) { }
public FluentAssertions.Json.IJsonAssertionRestriction<T, TProperty> Using<TProperty>(System.Action<FluentAssertions.Equivalency.IAssertionContext<TProperty>> action) { }
public FluentAssertions.Json.IJsonAssertionOptions<T> WithoutStrictOrdering() { }
}
public sealed class JsonAssertionRestriction<T, TProperty> : FluentAssertions.Json.IJsonAssertionRestriction<T, TProperty>
{
Expand Down
69 changes: 69 additions & 0 deletions Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Xunit;
using Xunit.Sdk;

namespace FluentAssertions.Json.Specs
{
public class WithoutStrictOrderingSpecs
{
[Theory]
[MemberData(nameof(When_ignoring_ordering_BeEquivalentTo_should_succeed_sample_data))]
public void When_ignoring_ordering_BeEquivalentTo_should_succeed(string subject, string expectation)
{
// Arrange
var subjectJToken = JToken.Parse(subject);
var expectationJToken = JToken.Parse(expectation);

// Act
subjectJToken.Should().BeEquivalentTo(expectationJToken, opt => opt.WithoutStrictOrdering());

// Assert
}

public static IEnumerable<object[]> When_ignoring_ordering_BeEquivalentTo_should_succeed_sample_data()
{
yield return new object[] { @"{""ids"":[1,2,3]}", @"{""ids"":[3,2,1]}" };
yield return new object[] { @"{""ids"":[1,2,3]}", @"{""ids"":[1,2,3]}" };
yield return new object[] { @"{""type"":2,""name"":""b""}", @"{""name"":""b"",""type"":2}" };
yield return new object[] { @"{""names"":[""a"",""b""]}", @"{""names"":[""b"",""a""]}" };
yield return new object[]
{
@"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}",
@"{""vals"":[{""type"":2,""name"":""b""},{""name"":""a"",""type"":1}]}"
};
yield return new object[]
{
@"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}",
@"{""vals"":[{""name"":""a"",""type"":1},{""type"":2,""name"":""b""}]}"
};
}

[Theory]
[MemberData(nameof(When_not_ignoring_ordering_BeEquivalentTo_should_throw_sample_data))]
public void When_not_ignoring_ordering_BeEquivalentTo_should_throw(string subject, string expectation)
{
// Arrange
var subjectJToken = JToken.Parse(subject);
var expectationJToken = JToken.Parse(expectation);

// Act
var action = new Func<AndConstraint<JTokenAssertions>>(() => subjectJToken.Should().BeEquivalentTo(expectationJToken));

// Assert
action.Should().Throw<XunitException>();
}

public static IEnumerable<object[]> When_not_ignoring_ordering_BeEquivalentTo_should_throw_sample_data()
{
yield return new object[] { @"{""ids"":[1,2,3]}", @"{""ids"":[3,2,1]}" };
yield return new object[] { @"{""names"":[""a"",""b""]}", @"{""names"":[""b"",""a""]}" };
yield return new object[]
{
@"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}",
@"{""vals"":[{""type"":2,""name"":""b""},{""name"":""a"",""type"":1}]}"
};
}
}
}
Loading