Skip to content

Commit

Permalink
Make BeEquivalentTo error messages more precise
Browse files Browse the repository at this point in the history
Mention actual type and length differences in message
  • Loading branch information
ronaldkroon committed Feb 10, 2020
1 parent d3aa995 commit f7dfdf9
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 78 deletions.
1 change: 1 addition & 0 deletions FluentAssertions.Json.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
Expand Down
70 changes: 61 additions & 9 deletions Src/FluentAssertions.Json/JTokenDifferentiator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private static Difference FindJArrayDifference(JArray actualArray, JToken expect
{
if (!(expected is JArray expectedArray))
{
return new Difference(DifferenceKind.OtherType, path);
return new Difference(DifferenceKind.OtherType, path, Describe(actualArray.Type), Describe(expected.Type));
}

return CompareItems(actualArray, expectedArray, path);
Expand All @@ -63,7 +63,7 @@ private static Difference CompareItems(JArray actual, JArray expected, JPath pat

if (actualChildren.Count() != expectedChildren.Count())
{
return new Difference(DifferenceKind.DifferentLength, path);
return new Difference(DifferenceKind.DifferentLength, path, actualChildren.Count(), expectedChildren.Count());
}

for (int i = 0; i < actualChildren.Count(); i++)
Expand All @@ -84,7 +84,7 @@ private static Difference FindJObjectDifference(JObject actual, JToken expected,
{
if (!(expected is JObject expectedObject))
{
return new Difference(DifferenceKind.OtherType, path);
return new Difference(DifferenceKind.OtherType, path, Describe(actual.Type), Describe(expected.Type));
}

return CompareProperties(actual?.Properties(), expectedObject.Properties(), path);
Expand Down Expand Up @@ -131,7 +131,7 @@ private static Difference FindJPropertyDifference(JProperty actualProperty, JTok
{
if (!(expected is JProperty expectedProperty))
{
return new Difference(DifferenceKind.OtherType, path);
return new Difference(DifferenceKind.OtherType, path, Describe(actualProperty.Type), Describe(expected.Type));
}

if (actualProperty.Name != expectedProperty.Name)
Expand All @@ -146,7 +146,7 @@ private static Difference FindValueDifference(JValue actualValue, JToken expecte
{
if (!(expected is JValue expectedValue))
{
return new Difference(DifferenceKind.OtherType, path);
return new Difference(DifferenceKind.OtherType, path, Describe(actualValue.Type), Describe(expected.Type));
}

return CompareValues(actualValue, expectedValue, path);
Expand All @@ -156,7 +156,7 @@ private static Difference CompareValues(JValue actual, JValue expected, JPath pa
{
if (actual.Type != expected.Type)
{
return new Difference(DifferenceKind.OtherType, path);
return new Difference(DifferenceKind.OtherType, path, Describe(actual.Type), Describe(expected.Type));
}

if (!actual.Equals(expected))
Expand All @@ -166,19 +166,71 @@ private static Difference CompareValues(JValue actual, JValue expected, JPath pa

return null;
}

private static string Describe(JTokenType jTokenType)
{
switch (jTokenType)
{
case JTokenType.None:
return "type none";
case JTokenType.Object:
return "an object";
case JTokenType.Array:
return "an array";
case JTokenType.Constructor:
return "a constructor";
case JTokenType.Property:
return "a property";
case JTokenType.Comment:
return "a comment";
case JTokenType.Integer:
return "an integer";
case JTokenType.Float:
return "a float";
case JTokenType.String:
return "a string";
case JTokenType.Boolean:
return "a boolean";
case JTokenType.Null:
return "type null";
case JTokenType.Undefined:
return "type undefined";
case JTokenType.Date:
return "a date";
case JTokenType.Raw:
return "type raw";
case JTokenType.Bytes:
return "type bytes";
case JTokenType.Guid:
return "a GUID";
case JTokenType.Uri:
return "a URI";
case JTokenType.TimeSpan:
return "a timespan";
default:
throw new ArgumentOutOfRangeException(nameof(jTokenType), jTokenType, null);
}
}
}

internal class Difference
{
public Difference(DifferenceKind kind, JPath path, object actual, object expected) : this(kind, path)
{
Actual = actual;
Expected = expected;
}

public Difference(DifferenceKind kind, JPath path)
{
Kind = kind;
Path = path;
}

private DifferenceKind Kind { get; }

private JPath Path { get; }
private object Actual { get; }
private object Expected { get; }

public override string ToString()
{
Expand All @@ -189,13 +241,13 @@ public override string ToString()
case DifferenceKind.ExpectedIsNull:
return "is not null";
case DifferenceKind.OtherType:
return $"has a different type at {Path}";
return $"has {Actual} instead of {Expected} at {Path}";
case DifferenceKind.OtherName:
return $"has a different name at {Path}";
case DifferenceKind.OtherValue:
return $"has a different value at {Path}";
case DifferenceKind.DifferentLength:
return $"has a different length at {Path}";
return $"has {Actual} elements instead of {Expected} at {Path}";
case DifferenceKind.ActualMissesProperty:
return $"misses property {Path}";
case DifferenceKind.ExpectedMissesProperty:
Expand Down
149 changes: 80 additions & 69 deletions Tests/FluentAssertions.Json.Shared.Specs/JTokenAssertionsSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,103 +65,114 @@ public void When_both_objects_are_the_same_or_equal_BeEquivalentTo_should_succee
a.Should().BeEquivalentTo(b);
}

[Fact]
public void When_objects_differ_BeEquivalentTo_should_fail()
public static IEnumerable<object[]> FailingBeEquivalentCases
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
var testCases = new []
get
{
Tuple.Create(
(string)null,
yield return new object[]
{
null,
"{ id: 2 }",
"is null")
,
Tuple.Create(
"is null"
};
yield return new object[]
{
"{ id: 1 }",
(string)null,
"is not null")
,
Tuple.Create(
null,
"is not null"
};
yield return new object[]
{
"{ items: [] }",
"{ items: 2 }",
"has a different type at $.items")
,
Tuple.Create(
"has an array instead of an integer at $.items"
};
yield return new object[]
{
"{ items: [ \"fork\", \"knife\" , \"spoon\" ]}",
"{ items: [ \"fork\", \"knife\" ]}",
"has a different length at $.items")
,
Tuple.Create(
"has 3 elements instead of 2 at $.items"
};
yield return new object[]
{
"{ items: [ \"fork\", \"knife\" ]}",
"{ items: [ \"fork\", \"knife\" , \"spoon\" ]}",
"has a different length at $.items")
,
Tuple.Create(
"has 2 elements instead of 3 at $.items"
};
yield return new object[]
{
"{ items: [ \"fork\", \"knife\" , \"spoon\" ]}",
"{ items: [ \"fork\", \"spoon\", \"knife\" ]}",
"has a different value at $.items[1]")
,
Tuple.Create(
"has a different value at $.items[1]"
};
yield return new object[]
{
"{ tree: { } }",
"{ tree: \"oak\" }",
"has a different type at $.tree")
,
Tuple.Create(
"has an object instead of a string at $.tree"
};
yield return new object[]
{
"{ tree: { leaves: 10} }",
"{ tree: { branches: 5, leaves: 10 } }",
"misses property $.tree.branches")
,
Tuple.Create(
"misses property $.tree.branches"
};
yield return new object[]
{
"{ tree: { branches: 5, leaves: 10 } }",
"{ tree: { leaves: 10} }",
"has extra property $.tree.branches")
,
Tuple.Create(
"has extra property $.tree.branches"
};
yield return new object[]
{
"{ tree: { leaves: 5 } }",
"{ tree: { leaves: 10} }",
"has a different value at $.tree.leaves")
,
Tuple.Create(
"has a different value at $.tree.leaves"
};
yield return new object[]
{
"{ eyes: \"blue\" }",
"{ eyes: [] }",
"has a different type at $.eyes")
,
Tuple.Create(
"has a string instead of an array at $.eyes"
};
yield return new object[]
{
"{ eyes: \"blue\" }",
"{ eyes: 2 }",
"has a different type at $.eyes")
,
Tuple.Create(
"has a string instead of an integer at $.eyes"
};
yield return new object[]
{
"{ id: 1 }",
"{ id: 2 }",
"has a different value at $.id")
};

foreach (var testCase in testCases)
{
string actualJson = testCase.Item1;
string expectedJson = testCase.Item2;
string expectedDifference = testCase.Item3;
"has a different value at $.id"
};
}
}

var actual = (actualJson != null) ? JToken.Parse(actualJson) : null;
var expected = (expectedJson != null) ? JToken.Parse(expectedJson) : null;
[Theory, MemberData(nameof(FailingBeEquivalentCases))]
public void When_objects_are_not_equivalent_it_should_throw(string actualJson, string expectedJson,
string expectedDifference)
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
var actual = (actualJson != null) ? JToken.Parse(actualJson) : null;
var expected = (expectedJson != null) ? JToken.Parse(expectedJson) : null;

var expectedMessage =
$"JSON document {expectedDifference}." +
$"Actual document" +
$"{Format(actual, true)}" +
$"was expected to be equivalent to" +
$"{Format(expected, true)}.";
var expectedMessage =
$"JSON document {expectedDifference}." +
$"Actual document" +
$"{Format(actual, true)}" +
$"was expected to be equivalent to" +
$"{Format(expected, true)}.";

//-----------------------------------------------------------------------------------------------------------
// Act & Assert
//-----------------------------------------------------------------------------------------------------------
actual.Should().Invoking(x => x.BeEquivalentTo(expected))
.Should().Throw<XunitException>()
.WithMessage(expectedMessage);
}
//-----------------------------------------------------------------------------------------------------------
// Act & Assert
//-----------------------------------------------------------------------------------------------------------
actual.Should().Invoking(x => x.BeEquivalentTo(expected))
.Should().Throw<XunitException>()
.WithMessage(expectedMessage);
}

[Fact]
Expand All @@ -175,7 +186,7 @@ public void When_properties_differ_BeEquivalentTo_should_fail()
Tuple.Create<JToken, JToken, string>(
new JProperty("eyes", "blue"),
new JArray(),
"has a different type at $")
"has a property instead of an array at $")
,
Tuple.Create<JToken, JToken, string>(
new JProperty("eyes", "blue"),
Expand Down

0 comments on commit f7dfdf9

Please sign in to comment.