Skip to content

Commit b8f1e81

Browse files
authored
Add overload of ContainSubtree that takes a config (#79)
1 parent b197b46 commit b8f1e81

File tree

4 files changed

+152
-51
lines changed

4 files changed

+152
-51
lines changed

Src/FluentAssertions.Json/JTokenAssertions.cs

Lines changed: 118 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,7 @@ public JTokenAssertions(JToken subject, AssertionChain assertionChain)
5454
public AndConstraint<JTokenAssertions> BeEquivalentTo(string expected, string because = "",
5555
params object[] becauseArgs)
5656
{
57-
JToken parsedExpected;
58-
try
59-
{
60-
parsedExpected = JToken.Parse(expected);
61-
}
62-
catch (Exception ex)
63-
{
64-
throw new ArgumentException(
65-
$"Unable to parse expected JSON string:{Environment.NewLine}" +
66-
$"{expected}{Environment.NewLine}" +
67-
"Check inner exception for more details.",
68-
nameof(expected), ex);
69-
}
57+
JToken parsedExpected = Parse(expected, nameof(expected));
7058

7159
return BeEquivalentTo(parsedExpected, because, becauseArgs);
7260
}
@@ -150,19 +138,7 @@ private AndConstraint<JTokenAssertions> BeEquivalentTo(JToken expected, bool ign
150138
public AndConstraint<JTokenAssertions> NotBeEquivalentTo(string unexpected, string because = "",
151139
params object[] becauseArgs)
152140
{
153-
JToken parsedUnexpected;
154-
try
155-
{
156-
parsedUnexpected = JToken.Parse(unexpected);
157-
}
158-
catch (Exception ex)
159-
{
160-
throw new ArgumentException(
161-
$"Unable to parse unexpected JSON string:{Environment.NewLine}" +
162-
$"{unexpected}{Environment.NewLine}" +
163-
"Check inner exception for more details.",
164-
nameof(unexpected), ex);
165-
}
141+
JToken parsedUnexpected = Parse(unexpected, nameof(unexpected));
166142

167143
return NotBeEquivalentTo(parsedUnexpected, because, becauseArgs);
168144
}
@@ -385,7 +361,7 @@ public AndWhichConstraint<JTokenAssertions, JToken> NotHaveElement(string unexpe
385361
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
386362
/// </param>
387363
/// <param name="becauseArgs">
388-
/// Zero or more objects to format using the placeholders in <see cref="because" />.
364+
/// Zero or more objects to format using the placeholders in <paramref name="because"/>.
389365
/// </param>
390366
public AndWhichConstraint<JTokenAssertions, JToken> ContainSingleItem(string because = "", params object[] becauseArgs)
391367
{
@@ -407,7 +383,7 @@ public AndWhichConstraint<JTokenAssertions, JToken> ContainSingleItem(string bec
407383
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
408384
/// </param>
409385
/// <param name="becauseArgs">
410-
/// Zero or more objects to format using the placeholders in <see cref="because" />.
386+
/// Zero or more objects to format using the placeholders in <paramref name="because"/>.
411387
/// </param>
412388
public AndConstraint<JTokenAssertions> HaveCount(int expected, string because = "", params object[] becauseArgs)
413389
{
@@ -429,42 +405,75 @@ public AndConstraint<JTokenAssertions> HaveCount(int expected, string because =
429405
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
430406
/// </param>
431407
/// <param name="becauseArgs">
432-
/// Zero or more objects to format using the placeholders in <see cref="because" />.
408+
/// Zero or more objects to format using the placeholders in <paramref name="because"/>.
433409
/// </param>
434410
/// <remarks>Use this method to match the current <see cref="JToken"/> against an arbitrary subtree,
435411
/// permitting it to contain any additional properties or elements. This way we can test multiple properties on a <see cref="JObject"/> at once,
436412
/// 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>
437413
/// <example>
438414
/// This example asserts the values of multiple properties of a child object within a JSON document.
439415
/// <code>
440-
/// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }");
441-
/// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }"));
416+
/// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'John' } }");
417+
/// json.Should().ContainSubtree("{ success: true, data: { type: 'my-type', name: 'John' } }");
442418
/// </code>
443419
/// </example>
444-
/// <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>
420+
/// <example>
421+
/// This example asserts that a <see cref="JArray"/> within a <see cref="JObject"/> has at least one element with at least the given properties
445422
/// <code>
446423
/// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }");
447-
/// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }"));
424+
/// json.Should().ContainSubtree("{ items: [ { type: 'my-type', name: 'Alpha' } ] }");
448425
/// </code>
426+
/// </example>
449427
public AndConstraint<JTokenAssertions> ContainSubtree(string subtree, string because = "", params object[] becauseArgs)
450428
{
451-
JToken subtreeToken;
452-
try
453-
{
454-
subtreeToken = JToken.Parse(subtree);
455-
}
456-
catch (Exception ex)
457-
{
458-
throw new ArgumentException(
459-
$"Unable to parse expected JSON string:{Environment.NewLine}" +
460-
$"{subtree}{Environment.NewLine}" +
461-
"Check inner exception for more details.",
462-
nameof(subtree), ex);
463-
}
429+
JToken subtreeToken = Parse(subtree, nameof(subtree));
464430

465431
return ContainSubtree(subtreeToken, because, becauseArgs);
466432
}
467433

434+
/// <summary>
435+
/// Recursively asserts that the current <see cref="JToken"/> contains at least the properties or elements of the specified <paramref name="subtree"/>.
436+
/// </summary>
437+
/// <param name="subtree">The subtree to search for</param>
438+
/// <param name="config">The options to consider while asserting values</param>
439+
/// <param name="because">
440+
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
441+
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
442+
/// </param>
443+
/// <param name="becauseArgs">
444+
/// Zero or more objects to format using the placeholders in <paramref name="because"/>.
445+
/// </param>
446+
/// <remarks>Use this method to match the current <see cref="JToken"/> against an arbitrary subtree,
447+
/// permitting it to contain any additional properties or elements. This way we can test multiple properties on a <see cref="JObject"/> at once,
448+
/// 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>
449+
/// <example>
450+
/// This example asserts the values of multiple properties of a child object within a JSON document using a specified double precision.
451+
/// <code>
452+
/// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', value: 0.99 } }");
453+
/// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', value: 1.0 } }"), options => options
454+
/// .Using&lt;double&gt;(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-1))
455+
/// .WhenTypeIs&lt;double&gt;());
456+
/// </code>
457+
/// </example>
458+
/// <example>
459+
/// This example asserts that a <see cref="JArray"/> within a <see cref="JObject"/> has at least one element with at least the given properties, using a specified double precision.
460+
/// <code>
461+
/// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', value: 0.99 }, { id: 3, type: 'other-type', value: 3 } ] }");
462+
/// json.Should().ContainSubtree("{ items: [ { type: 'my-type', value: 1 } ] }", options => options
463+
/// .Using&lt;double&gt;(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-1))
464+
/// .WhenTypeIs&lt;double&gt;());
465+
/// </code>
466+
/// </example>
467+
public AndConstraint<JTokenAssertions> ContainSubtree(string subtree,
468+
Func<IJsonAssertionOptions<object>, IJsonAssertionOptions<object>> config,
469+
string because = "",
470+
params object[] becauseArgs)
471+
{
472+
JToken subtreeToken = Parse(subtree, nameof(subtree));
473+
474+
return BeEquivalentTo(subtreeToken, true, config, because, becauseArgs);
475+
}
476+
468477
/// <summary>
469478
/// Recursively asserts that the current <see cref="JToken"/> contains at least the properties or elements of the specified <paramref name="subtree"/>.
470479
/// </summary>
@@ -474,28 +483,87 @@ public AndConstraint<JTokenAssertions> ContainSubtree(string subtree, string bec
474483
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
475484
/// </param>
476485
/// <param name="becauseArgs">
477-
/// Zero or more objects to format using the placeholders in <see cref="because" />.
486+
/// Zero or more objects to format using the placeholders in <paramref name="because"/>.
478487
/// </param>
479488
/// <remarks>Use this method to match the current <see cref="JToken"/> against an arbitrary subtree,
480489
/// permitting it to contain any additional properties or elements. This way we can test multiple properties on a <see cref="JObject"/> at once,
481490
/// 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>
482491
/// <example>
483492
/// This example asserts the values of multiple properties of a child object within a JSON document.
484493
/// <code>
485-
/// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }");
486-
/// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }"));
494+
/// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'John' } }");
495+
/// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'John' } }"));
487496
/// </code>
488497
/// </example>
489-
/// <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>
498+
/// <example>
499+
/// This example asserts that a <see cref="JArray"/> within a <see cref="JObject"/> has at least one element with at least the given properties
490500
/// <code>
491501
/// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }");
492502
/// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }"));
493503
/// </code>
504+
/// </example>
494505
public AndConstraint<JTokenAssertions> ContainSubtree(JToken subtree, string because = "", params object[] becauseArgs)
495506
{
496507
return BeEquivalentTo(subtree, true, options => options, because, becauseArgs);
497508
}
498509

510+
/// <summary>
511+
/// Recursively asserts that the current <see cref="JToken"/> contains at least the properties or elements of the specified <paramref name="subtree"/>.
512+
/// </summary>
513+
/// <param name="subtree">The subtree to search for</param>
514+
/// <param name="config">The options to consider while asserting values</param>
515+
/// <param name="because">
516+
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
517+
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
518+
/// </param>
519+
/// <param name="becauseArgs">
520+
/// Zero or more objects to format using the placeholders in <paramref name="because"/>.
521+
/// </param>
522+
/// <remarks>Use this method to match the current <see cref="JToken"/> against an arbitrary subtree,
523+
/// permitting it to contain any additional properties or elements. This way we can test multiple properties on a <see cref="JObject"/> at once,
524+
/// 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>
525+
/// <example>
526+
/// This example asserts the values of multiple properties of a child object within a JSON document, using a specified double precision.
527+
/// <code>
528+
/// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', value: 0.99 } }");
529+
/// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', value: 1.0 } }"), options => options
530+
/// .Using&lt;double&gt;(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-1))
531+
/// .WhenTypeIs&lt;double&gt;());
532+
/// </code>
533+
/// </example>
534+
/// <example>
535+
/// This example asserts that a <see cref="JArray"/> within a <see cref="JObject"/> has at least one element with at least the given properties, using a specified double precision.
536+
/// <code>
537+
/// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', value: 0.99 }, { id: 3, type: 'other-type', value: 3 } ] }");
538+
/// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', value: 1 } ] }"), options => options
539+
/// .Using&lt;double&gt;(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-1))
540+
/// .WhenTypeIs&lt;double&gt;());
541+
/// </code>
542+
/// </example>
543+
public AndConstraint<JTokenAssertions> ContainSubtree(JToken subtree,
544+
Func<IJsonAssertionOptions<object>, IJsonAssertionOptions<object>> config,
545+
string because = "",
546+
params object[] becauseArgs)
547+
{
548+
return BeEquivalentTo(subtree, true, config, because, becauseArgs);
549+
}
550+
551+
private static JToken Parse(string json, string paramName)
552+
{
553+
try
554+
{
555+
return JToken.Parse(json);
556+
}
557+
catch (Exception ex)
558+
{
559+
throw new ArgumentException(
560+
$"Unable to parse {paramName} JSON string:{Environment.NewLine}" +
561+
$"{json}{Environment.NewLine}" +
562+
"Check inner exception for more details.",
563+
paramName, ex);
564+
}
565+
}
566+
499567
#pragma warning disable CA1822 // Making this method static is a breaking chan
500568
public string Format(JToken value, bool useLineBreaks = false)
501569
{

Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/net47.verified.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ namespace FluentAssertions.Json
2121
public FluentAssertions.AndWhichConstraint<FluentAssertions.Json.JTokenAssertions, Newtonsoft.Json.Linq.JToken> ContainSingleItem(string because = "", params object[] becauseArgs) { }
2222
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, string because = "", params object[] becauseArgs) { }
2323
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> ContainSubtree(string subtree, string because = "", params object[] becauseArgs) { }
24+
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) { }
25+
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> ContainSubtree(string subtree, System.Func<FluentAssertions.Json.IJsonAssertionOptions<object>, FluentAssertions.Json.IJsonAssertionOptions<object>> config, string because = "", params object[] becauseArgs) { }
2426
public string Format(Newtonsoft.Json.Linq.JToken value, bool useLineBreaks = false) { }
2527
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> HaveCount(int expected, string because = "", params object[] becauseArgs) { }
2628
public FluentAssertions.AndWhichConstraint<FluentAssertions.Json.JTokenAssertions, Newtonsoft.Json.Linq.JToken> HaveElement(string expected) { }

Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/netstandard2.0.verified.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ namespace FluentAssertions.Json
2121
public FluentAssertions.AndWhichConstraint<FluentAssertions.Json.JTokenAssertions, Newtonsoft.Json.Linq.JToken> ContainSingleItem(string because = "", params object[] becauseArgs) { }
2222
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, string because = "", params object[] becauseArgs) { }
2323
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> ContainSubtree(string subtree, string because = "", params object[] becauseArgs) { }
24+
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) { }
25+
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> ContainSubtree(string subtree, System.Func<FluentAssertions.Json.IJsonAssertionOptions<object>, FluentAssertions.Json.IJsonAssertionOptions<object>> config, string because = "", params object[] becauseArgs) { }
2426
public string Format(Newtonsoft.Json.Linq.JToken value, bool useLineBreaks = false) { }
2527
public FluentAssertions.AndConstraint<FluentAssertions.Json.JTokenAssertions> HaveCount(int expected, string because = "", params object[] becauseArgs) { }
2628
public FluentAssertions.AndWhichConstraint<FluentAssertions.Json.JTokenAssertions, Newtonsoft.Json.Linq.JToken> HaveElement(string expected) { }

Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -943,10 +943,39 @@ public void When_checking_subtree_with_an_invalid_expected_string_it_should_prov
943943
// Act & Assert
944944
actualJson.Should().Invoking(x => x.ContainSubtree(invalidSubtree))
945945
.Should().Throw<ArgumentException>()
946-
.WithMessage($"Unable to parse expected JSON string:{invalidSubtree}*")
946+
.WithMessage($"Unable to parse subtree JSON string:{invalidSubtree}*")
947947
.WithInnerException<JsonReaderException>();
948948
}
949949

950+
[Fact]
951+
public void Assert_property_with_approximation_succeeds()
952+
{
953+
// Arrange
954+
var actual = JToken.Parse("{ \"id\": 1.1232 }");
955+
var expected = JToken.Parse("{ \"id\": 1.1235 }");
956+
957+
// Act & Assert
958+
actual.Should().ContainSubtree(expected, options => options
959+
.Using<double>(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-3))
960+
.WhenTypeIs<double>());
961+
}
962+
963+
[Fact]
964+
public void Can_assert_on_a_field_with_approximation()
965+
{
966+
// Arrange
967+
var actual = JToken.Parse("{ \"id\": 1.1232 }");
968+
var expected = JToken.Parse("{ \"id\": 1.1235 }");
969+
970+
// Act & Assert
971+
actual.Should().
972+
Invoking(x => x.ContainSubtree(expected, options => options
973+
.Using<double>(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-5))
974+
.WhenTypeIs<double>()))
975+
.Should().Throw<XunitException>()
976+
.WithMessage("JSON document has a different value at $.id.*");
977+
}
978+
950979
#endregion
951980

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

0 commit comments

Comments
 (0)