Skip to content

Commit 7bfe503

Browse files
authored
Added ContainSubtree constraint (#16)
See #6
1 parent b8f4450 commit 7bfe503

2 files changed

Lines changed: 243 additions & 0 deletions

File tree

Src/FluentAssertions.Json/JTokenAssertions.cs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics;
2+
using System.Linq;
23
using FluentAssertions.Collections;
34
using FluentAssertions.Json.Common;
45
using FluentAssertions.Execution;
@@ -283,6 +284,112 @@ public AndConstraint<JTokenAssertions> HaveCount(int expected, string because =
283284
}
284285
}
285286

287+
/// <summary>
288+
/// Recursively asserts that the current <see cref="JToken"/> contains at least the properties or elements of the specified <see cref="JToken"/>.
289+
/// </summary>
290+
/// <param name="subtree">The subtree to search for</param>
291+
/// <param name="because">
292+
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
293+
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
294+
/// </param>
295+
/// <param name="becauseArgs">
296+
/// Zero or more objects to format using the placeholders in <see cref="because" />.
297+
/// </param>
298+
/// <remarks>Use this method to match the current <see cref="JToken"/> against an arbitrary subtree,
299+
/// permitting it to contain any additional properties or elements. This way we can test multiple properties on a <see cref="JObject"/> at once,
300+
/// 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>
301+
/// <example>
302+
/// This example asserts the values of multiple properties of a child object within a JSON document.
303+
/// <code>
304+
/// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }");
305+
/// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }"));
306+
/// </code>
307+
/// </example>
308+
/// <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>
309+
/// <code>
310+
/// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }");
311+
/// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }"));
312+
/// </code>
313+
public AndConstraint<JTokenAssertions> ContainSubtree(JToken subtree, string because = "", params object[] becauseArgs)
314+
{
315+
Execute.Assertion
316+
.ForCondition(JTokenContainsSubtree(Subject, subtree))
317+
.BecauseOf(because, becauseArgs)
318+
.FailWith("Expected JSON document to contain subtree {0} {reason}, but some elements were missing.", subtree); // todo: report exact cause of failure, eg. name of the missing property, etc.
319+
320+
return new AndConstraint<JTokenAssertions>(this);
321+
}
322+
323+
/// <summary>
324+
/// Recursively asserts that the current <see cref="JToken"/> contains at least the properties or elements of the specified <see cref="JToken"/>.
325+
/// </summary>
326+
/// <param name="subtree">The subtree to search for</param>
327+
/// <param name="because">
328+
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
329+
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
330+
/// </param>
331+
/// <param name="becauseArgs">
332+
/// Zero or more objects to format using the placeholders in <see cref="because" />.
333+
/// </param>
334+
/// <remarks>Use this method to match the current <see cref="JToken"/> against an arbitrary subtree,
335+
/// permitting it to contain any additional properties or elements. This way we can test multiple properties on a <see cref="JObject"/> at once,
336+
/// 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>
337+
/// <example>
338+
/// This example asserts the values of multiple properties of a child object within a JSON document.
339+
/// <code>
340+
/// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }");
341+
/// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }"));
342+
/// </code>
343+
/// </example>
344+
/// <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>
345+
/// <code>
346+
/// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }");
347+
/// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }"));
348+
/// </code>
349+
public AndConstraint<JTokenAssertions> ContainSubtree(string subtree, string because = "", params object[] becauseArgs)
350+
{
351+
return ContainSubtree(JToken.Parse(subtree), because, becauseArgs);
352+
}
353+
354+
private bool JTokenContainsSubtree(JToken token, JToken subtree)
355+
{
356+
switch (subtree.Type)
357+
{
358+
case JTokenType.Object:
359+
{
360+
var sub = (JObject)subtree;
361+
var obj = token as JObject;
362+
if (obj == null)
363+
return false;
364+
foreach (var subProp in sub.Properties())
365+
{
366+
var prop = obj.Property(subProp.Name);
367+
if (prop == null)
368+
return false;
369+
if (!JTokenContainsSubtree(prop.Value, subProp.Value))
370+
return false;
371+
}
372+
return true;
373+
}
374+
case JTokenType.Array:
375+
{
376+
var sub = (JArray)subtree;
377+
var arr = token as JArray;
378+
if (arr == null)
379+
return false;
380+
foreach (var subItem in sub)
381+
{
382+
if (!arr.Any(item => JTokenContainsSubtree(item, subItem)))
383+
return false;
384+
}
385+
return true;
386+
}
387+
default:
388+
return JToken.DeepEquals(token, subtree);
389+
390+
}
391+
}
392+
286393
public string Format(JToken value, bool useLineBreaks = false)
287394
{
288395
return new JTokenFormatter().Format(value, new FormattingContext

Tests/FluentAssertions.Json.Shared.Specs/JTokenAssertionsSpecs.cs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,142 @@ public void When_expecting_a_different_number_of_array_items_than_the_actual_num
826826

827827
#endregion HaveCount
828828

829+
#region ContainSubtree
830+
831+
[Fact]
832+
public void When_all_expected_subtree_properties_match_ContainSubtree_should_succeed()
833+
{
834+
//-----------------------------------------------------------------------------------------------------------
835+
// Arrange
836+
//-----------------------------------------------------------------------------------------------------------
837+
var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', baz: 'baz'} ");
838+
839+
//-----------------------------------------------------------------------------------------------------------
840+
// Act
841+
//-----------------------------------------------------------------------------------------------------------
842+
Action act = () => subject.Should().ContainSubtree(" { foo: 'foo', baz: 'baz' } ");
843+
844+
//-----------------------------------------------------------------------------------------------------------
845+
// Assert
846+
//-----------------------------------------------------------------------------------------------------------
847+
act.Should().NotThrow();
848+
}
849+
850+
[Fact]
851+
public void When_subtree_properties_are_missing_ContainSubtree_should_fail()
852+
{
853+
//-----------------------------------------------------------------------------------------------------------
854+
// Arrange
855+
//-----------------------------------------------------------------------------------------------------------
856+
var subject = JToken.Parse("{ foo: 'foo', bar: 'bar' } ");
857+
858+
//-----------------------------------------------------------------------------------------------------------
859+
// Act
860+
//-----------------------------------------------------------------------------------------------------------
861+
Action act = () => subject.Should().ContainSubtree(" { baz: 'baz' } ");
862+
863+
//-----------------------------------------------------------------------------------------------------------
864+
// Assert
865+
//-----------------------------------------------------------------------------------------------------------
866+
act.Should().Throw<XunitException>();
867+
}
868+
869+
[Fact]
870+
public void When_deep_subtree_matches_ContainSubtree_should_succeed()
871+
{
872+
//-----------------------------------------------------------------------------------------------------------
873+
// Arrange
874+
//-----------------------------------------------------------------------------------------------------------
875+
var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', child: { x: 1, y: 2, grandchild: { tag: 'abrakadabra' } }} ");
876+
877+
//-----------------------------------------------------------------------------------------------------------
878+
// Act
879+
//-----------------------------------------------------------------------------------------------------------
880+
Action act = () => subject.Should().ContainSubtree(" { child: { grandchild: { tag: 'abrakadabra' } } } ");
881+
882+
//-----------------------------------------------------------------------------------------------------------
883+
// Assert
884+
//-----------------------------------------------------------------------------------------------------------
885+
act.Should().NotThrow();
886+
}
887+
888+
[Fact]
889+
public void When_deep_subtree_does_not_match_ContainSubtree_should_fail()
890+
{
891+
//-----------------------------------------------------------------------------------------------------------
892+
// Arrange
893+
//-----------------------------------------------------------------------------------------------------------
894+
var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', child: { x: 1, y: 2, grandchild: { tag: 'abrakadabra' } }} ");
895+
896+
//-----------------------------------------------------------------------------------------------------------
897+
// Act
898+
//-----------------------------------------------------------------------------------------------------------
899+
Action act = () => subject.Should().ContainSubtree(" { child: { grandchild: { tag: 'ooops' } } } ");
900+
901+
//-----------------------------------------------------------------------------------------------------------
902+
// Assert
903+
//-----------------------------------------------------------------------------------------------------------
904+
act.Should().Throw<XunitException>();
905+
}
906+
907+
[Fact]
908+
public void When_array_elements_are_matching_ContainSubtree_should_succeed()
909+
{
910+
//-----------------------------------------------------------------------------------------------------------
911+
// Arrange
912+
//-----------------------------------------------------------------------------------------------------------
913+
var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', items: [ { id: 1 }, { id: 2 }, { id: 3 } ] } ");
914+
915+
//-----------------------------------------------------------------------------------------------------------
916+
// Act
917+
//-----------------------------------------------------------------------------------------------------------
918+
Action act = () => subject.Should().ContainSubtree(" { items: [ { id: 1 }, { id: 3 } ] } ");
919+
920+
//-----------------------------------------------------------------------------------------------------------
921+
// Assert
922+
//-----------------------------------------------------------------------------------------------------------
923+
act.Should().NotThrow();
924+
}
925+
926+
[Fact]
927+
public void When_array_elements_are_missing_ContainSubtree_should_fail()
928+
{
929+
//-----------------------------------------------------------------------------------------------------------
930+
// Arrange
931+
//-----------------------------------------------------------------------------------------------------------
932+
var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', items: [ { id: 1 }, { id: 3 }, { id: 5 } ] } ");
933+
934+
//-----------------------------------------------------------------------------------------------------------
935+
// Act
936+
//-----------------------------------------------------------------------------------------------------------
937+
Action act = () => subject.Should().ContainSubtree(" { items: [ { id: 1 }, { id: 2 } ] } ");
938+
939+
//-----------------------------------------------------------------------------------------------------------
940+
// Assert
941+
//-----------------------------------------------------------------------------------------------------------
942+
act.Should().Throw<XunitException>();
943+
}
944+
945+
[Fact]
946+
public void When_property_types_dont_match_ContainSubtree_should_fail()
947+
{
948+
//-----------------------------------------------------------------------------------------------------------
949+
// Arrange
950+
//-----------------------------------------------------------------------------------------------------------
951+
var subject = JToken.Parse("{ foo: '1' } ");
952+
953+
//-----------------------------------------------------------------------------------------------------------
954+
// Act
955+
//-----------------------------------------------------------------------------------------------------------
956+
Action act = () => subject.Should().ContainSubtree(" { foo: 1 } ");
957+
958+
//-----------------------------------------------------------------------------------------------------------
959+
// Assert
960+
//-----------------------------------------------------------------------------------------------------------
961+
act.Should().Throw<XunitException>();
962+
}
963+
964+
#endregion
829965
public static string Format(JToken value, bool useLineBreaks = false)
830966
{
831967
return new JTokenFormatter().Format(value, new FormattingContext

0 commit comments

Comments
 (0)