Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
41 changes: 34 additions & 7 deletions Src/FluentAssertions.Json/JTokenAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ static JTokenAssertions()
/// Initializes a new instance of the <see cref="JTokenAssertions" /> class.
/// </summary>
/// <param name="subject">The subject</param>
/// <param name="orCreate"></param>
/// <param name="assertionChain">The assertion chain</param>
public JTokenAssertions(JToken subject, AssertionChain assertionChain)
: base(subject, assertionChain)
{
Expand Down Expand Up @@ -437,15 +437,17 @@ public AndConstraint<JTokenAssertions> HaveCount(int expected, string because =
/// <example>
/// This example asserts the values of multiple properties of a child object within a JSON document.
/// <code>
/// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }");
/// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }"));
/// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'None' } }");
Copy link
Member

Choose a reason for hiding this comment

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

nit: None is not more correct than Noone. (no one/no-one).
If tools think that Noone is a a typo , then let's just change it to a real name like John or Joe.

/// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'None' } }"));
/// </code>
/// </example>
/// <example>This example asserts that a <see cref="JArray"/> within a <see cref="JObject"/> has at least one element with at least the given properties</example>
/// <example>
/// This example asserts that a <see cref="JArray"/> within a <see cref="JObject"/> has at least one element with at least the given properties
/// <code>
/// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }");
/// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }"));
/// </code>
/// </example>
public AndConstraint<JTokenAssertions> ContainSubtree(string subtree, string because = "", params object[] becauseArgs)
Copy link
Member

Choose a reason for hiding this comment

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

Should we also augment this overload with one that takes a config?

{
JToken subtreeToken;
Expand Down Expand Up @@ -482,20 +484,45 @@ public AndConstraint<JTokenAssertions> ContainSubtree(string subtree, string bec
/// <example>
/// This example asserts the values of multiple properties of a child object within a JSON document.
/// <code>
/// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }");
/// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }"));
/// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'None' } }");
/// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'None' } }"));
/// </code>
/// </example>
/// <example>This example asserts that a <see cref="JArray"/> within a <see cref="JObject"/> has at least one element with at least the given properties</example>
/// <example>
/// This example asserts that a <see cref="JArray"/> within a <see cref="JObject"/> has at least one element with at least the given properties
/// <code>
/// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }");
/// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }"));
/// </code>
/// </example>
public AndConstraint<JTokenAssertions> ContainSubtree(JToken subtree, string because = "", params object[] becauseArgs)
{
return BeEquivalentTo(subtree, true, options => options, because, becauseArgs);
}

/// <summary>
/// Recursively asserts that the current <see cref="JToken"/> contains at least the properties or elements of the specified <paramref name="subtree"/>.
/// </summary>
/// <param name="subtree">The subtree to search for</param>
/// <param name="config">The options to consider while asserting values</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 <see cref="because" />.
/// </param>
/// <remarks>Use this method to match the current <see cref="JToken"/> against an arbitrary subtree,
/// permitting it to contain any additional properties or elements. This way we can test multiple properties on a <see cref="JObject"/> at once,
/// or test if a <see cref="JArray"/> contains any items that match a set of properties, assert that a JSON document has a given shape, etc. </remarks>
Comment on lines +503 to +517
Copy link
Member

Choose a reason for hiding this comment

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

Let's also add the <example> snippets found on the other overloads here.

public AndConstraint<JTokenAssertions> ContainSubtree(JToken subtree,
Func<IJsonAssertionOptions<object>, IJsonAssertionOptions<object>> config,
string because = "",
params object[] becauseArgs)
{
return BeEquivalentTo(subtree, true, config, because, becauseArgs);
}

#pragma warning disable CA1822 // Making this method static is a breaking chan
public string Format(JToken value, bool useLineBreaks = false)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace FluentAssertions.Json
public FluentAssertions.AndWhichConstraint<FluentAssertions.Json.JTokenAssertions, Newtonsoft.Json.Linq.JToken> ContainSingleItem(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> ContainSubtree(string subtree, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, System.Func<FluentAssertions.Json.IJsonAssertionOptions<object>, FluentAssertions.Json.IJsonAssertionOptions<object>> config, string because = "", params object[] becauseArgs) { }
public string Format(Newtonsoft.Json.Linq.JToken value, bool useLineBreaks = false) { }
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> HaveCount(int expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<FluentAssertions.Json.JTokenAssertions, Newtonsoft.Json.Linq.JToken> HaveElement(string expected) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace FluentAssertions.Json
public FluentAssertions.AndWhichConstraint<FluentAssertions.Json.JTokenAssertions, Newtonsoft.Json.Linq.JToken> ContainSingleItem(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> ContainSubtree(string subtree, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, System.Func<FluentAssertions.Json.IJsonAssertionOptions<object>, FluentAssertions.Json.IJsonAssertionOptions<object>> config, string because = "", params object[] becauseArgs) { }
public string Format(Newtonsoft.Json.Linq.JToken value, bool useLineBreaks = false) { }
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> HaveCount(int expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<FluentAssertions.Json.JTokenAssertions, Newtonsoft.Json.Linq.JToken> HaveElement(string expected) { }
Expand Down
30 changes: 30 additions & 0 deletions Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,36 @@ public void When_checking_subtree_with_an_invalid_expected_string_it_should_prov
.WithInnerException<JsonReaderException>();
}

[Fact]
public void Assert_property_with_approximation_succeeds()
{
// Arrange
var actual = JToken.Parse("{ \"id\": 1.1232 }");
var expected = JToken.Parse("{ \"id\": 1.1235 }");

// Act & Assert
actual.Should().ContainSubtree(expected, options => options
.Using<double>(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-3))
.WhenTypeIs<double>());
}

[Fact]
public void Can_assert_on_a_field_with_approximation()
{
// Arrange
var actual = JToken.Parse("{ \"id\": 1.1232 }");
var expected = JToken.Parse("{ \"id\": 1.1235 }");

// Act
Action act = () => actual.Should().ContainSubtree(expected, options => options
.Using<double>(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-5))
.WhenTypeIs<double>());

// Assert
act.Should().Throw<XunitException>()
.WithMessage("JSON document has a different value at $.id.*");
}

#endregion

private static string Format(JToken value, bool useLineBreaks = false)
Expand Down