Last active
November 25, 2016 07:37
-
-
Save JulianMay/eeb533d075d5d2c7f3e4cc7746a9a5da to your computer and use it in GitHub Desktop.
"By example" assertions of json-documents
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using Newtonsoft.Json; | |
using System.Text; | |
namespace ProjectName.Tests.System.json | |
{ | |
public class JsonComparer | |
{ | |
private const string SuperSecretWildcard = "@£$DONTeverUSEthisINtests_WILDCARD$£@"; | |
private const string SuperSecretAnyString = "@£$DONTeverUSEthisINtests_STRING$£@"; | |
private const string SuperSecretAnyNumber = "@£$DONTeverUSEthisINtests_NUMBER$£@"; | |
private const string WildcardType = "#Wildcard#"; | |
private const string StringType = "#String#"; | |
private const string NumberType = "#Number#"; | |
public class Result | |
{ | |
public Difference Report; | |
public string ExpectedPretty; | |
public string ActualPretty; | |
} | |
public class Difference | |
{ | |
public string ObjectName; | |
public string DescriptionOfDifference; | |
public List<Difference> UnderLyingDifferences = new List<Difference>(); | |
public bool HasUnexpectedDifferences | |
{ get | |
{ | |
return !string.IsNullOrEmpty(DescriptionOfDifference) || UnderLyingDifferences.Any(x=>x.HasUnexpectedDifferences); | |
} | |
} | |
public virtual string Report(IEnumerable<string> path) | |
{ | |
if (!HasUnexpectedDifferences) | |
return string.Empty; | |
var b = new StringBuilder(); | |
if (!string.IsNullOrEmpty(DescriptionOfDifference)) | |
b.AppendLine(string.Format("at '{0}', Property '{1}': {2}", | |
string.Join("->", path), ObjectName, DescriptionOfDifference)); | |
if (UnderLyingDifferences.Any()) | |
{ | |
foreach(var d in UnderLyingDifferences) | |
{ | |
var sub = d.Report(path.Concat(new[] { ObjectName })); | |
if (!string.IsNullOrWhiteSpace(sub)) | |
b.AppendLine(sub); | |
} | |
} | |
return b.ToString(); | |
} | |
} | |
public class NoDifference : Difference | |
{ | |
public override string Report(IEnumerable<string> path) | |
{ | |
return string.Empty; | |
} | |
} | |
public Result JsonDifferenceReport(string objectName,string expected,string actual, bool allowUnexpectedProperties, bool allowUndefinedArrayMembers) | |
{ | |
if (String.IsNullOrEmpty(objectName)) | |
throw new ArgumentNullException("objectName"); | |
if (null == expected) | |
throw new ArgumentNullException("first"); | |
if (null == actual) | |
throw new ArgumentNullException("second"); | |
var expectedNode = Deserialize(expected); | |
var actualNode = Deserialize(actual); | |
return new Result | |
{ | |
Report = DocumentDifference(expectedNode, actualNode, allowUnexpectedProperties, "\"root\"", allowUndefinedArrayMembers), | |
ActualPretty = Prettify(actual), | |
ExpectedPretty = Prettify(SanitizeWildcards( expected)) | |
}; | |
} | |
private bool IsNull(dynamic value) | |
{ | |
return value == null; | |
} | |
private Difference ValueDifference(dynamic expectedNode, dynamic actualNode, string objectName) | |
{ | |
//both null means same | |
if (IsNull(expectedNode) && IsNull(actualNode)) | |
return new NoDifference(); | |
//if expectation is "wildcard", it has no relevance if different, so we return false | |
if (Convert.ToString(expectedNode).Equals(SuperSecretWildcard)) | |
return new NoDifference(); | |
//both not null means we can compare with equals | |
string eValue = AsString(expectedNode); | |
string aValue = AsString(actualNode); | |
if (eValue.Equals(aValue)) | |
return new NoDifference(); | |
return new Difference | |
{ | |
ObjectName = objectName, | |
DescriptionOfDifference = string.Format("Expected value '{0}' but was '{1}'", eValue, aValue) | |
}; | |
} | |
private Difference ArrayDifference(Newtonsoft.Json.Linq.JArray expectedNode, Newtonsoft.Json.Linq.JArray actualNode, bool allowUnexpectedProperties, string objectName, bool allowUndefinedArrayMembers) | |
{ | |
string immediateDiff = string.Empty; | |
if(!allowUndefinedArrayMembers && (expectedNode.Count != actualNode.Count)) | |
{ | |
return new Difference | |
{ | |
ObjectName = objectName, | |
DescriptionOfDifference = string.Format("Array expected to have {0} members, but has {1} members", expectedNode.Count, actualNode.Count) | |
}; | |
} | |
var diffs = new List<Difference>(); | |
var eLooper = expectedNode.Children().GetEnumerator(); | |
var aLooper = actualNode.Children().GetEnumerator(); ; | |
int idx = 0; | |
while (eLooper.MoveNext()) | |
{ | |
aLooper.MoveNext(); | |
var e = eLooper.Current; | |
var a = aLooper.Current; | |
var eObj = e as Newtonsoft.Json.Linq.JObject; | |
if (eObj != null) | |
{ | |
diffs.Add(ObjectDifference(eObj, a as Newtonsoft.Json.Linq.JObject, allowUnexpectedProperties, string.Format("{0}[{1}]", objectName, idx), allowUndefinedArrayMembers)); | |
} | |
else | |
{ | |
diffs.Add(ValueDifference(e, a, string.Format("{0}[{1}]", objectName, idx))); | |
} | |
idx++; | |
} | |
return new Difference | |
{ | |
ObjectName = objectName, | |
UnderLyingDifferences = diffs | |
}; | |
} | |
private Difference ObjectDifference(Newtonsoft.Json.Linq.JObject expectedNode, Newtonsoft.Json.Linq.JObject actualNode, bool allowUnexpectedProperties, string objectName, bool allowUndefinedArrayMembers) | |
{ | |
//Use dictionary-trick | |
var jsonExpected = expectedNode.ToString(Formatting.None); | |
var jsonActial = actualNode.ToString(Formatting.None); | |
return DocumentDifference(Deserialize(jsonExpected), Deserialize(jsonActial), allowUnexpectedProperties, objectName, allowUndefinedArrayMembers); | |
} | |
private Difference DocumentDifference(Dictionary<string, dynamic> expectedNodes, Dictionary<string, dynamic> actualNodes, bool allowUnexpectedProperties, string objectName, bool allowUndefinedArrayMembers) | |
{ | |
List<String> allKeys = new List<String>(); | |
foreach (String key in expectedNodes.Keys) | |
if (!allKeys.Any(X => X.Equals(key))) allKeys.Add(key); | |
foreach (String key in actualNodes.Keys) | |
if (!allKeys.Any(X => X.Equals(key))) allKeys.Add(key); | |
List<Difference> differences = new List<Difference>(); | |
string missing = string.Empty; | |
foreach (String key in allKeys) | |
{ | |
if (!actualNodes.ContainsKey(key)) | |
{ | |
missing += string.Format("expected property missing: \"{0}\",\r\n", key); | |
continue; | |
} | |
if (!allowUnexpectedProperties && !expectedNodes.ContainsKey(key)) | |
{ | |
missing += string.Format("unexpected property in actual: \"{0}\":{1}\r\n", key, AsString(actualNodes[key])); | |
continue; | |
} | |
if (!expectedNodes.ContainsKey(key) && allowUnexpectedProperties) | |
{ continue; } | |
//Type match might not be necessary if it's just *somestring* | |
var eNode = expectedNodes[key]; | |
if (eNode!= null && eNode.Equals(SuperSecretWildcard)) | |
return new NoDifference(); | |
var eType = GetTypeOfDynamic(eNode); | |
var aNode = actualNodes[key]; | |
var aType = GetTypeOfDynamic(aNode); | |
if (NodeTypesDifferUnexpectedly(eType, aType)) | |
{ | |
missing += string.Format("property type mismatch: Expected '{0}' to be of type '{1}' but was of type '{2}',\r\n", key, eType, aType); | |
continue; | |
} | |
//////Value comparison might not be relevant if types match and value is just *some___* | |
if (!RequiresValueComparison(eNode)) | |
continue; | |
//////Compare, based on type (Array, Object or value) | |
var eArray = eNode as Newtonsoft.Json.Linq.JArray; | |
if(eArray != null) | |
{ | |
differences.Add(ArrayDifference(eArray, aNode as Newtonsoft.Json.Linq.JArray,allowUnexpectedProperties, key, allowUndefinedArrayMembers)); | |
continue; | |
} | |
var eObj = eNode as Newtonsoft.Json.Linq.JObject; | |
if (eObj != null) | |
{ | |
differences.Add(ObjectDifference(eObj, aNode as Newtonsoft.Json.Linq.JObject, allowUnexpectedProperties, key, allowUndefinedArrayMembers)); | |
continue; | |
} | |
differences.Add(ValueDifference(eNode, aNode, key)); | |
} | |
return new Difference | |
{ | |
ObjectName = objectName, | |
DescriptionOfDifference = missing, | |
UnderLyingDifferences = differences | |
}; | |
} | |
private bool NodeTypesDifferUnexpectedly(string expectedType, string actualType) | |
{ | |
if (expectedType == SuperSecretWildcard) | |
return false; | |
return !(expectedType.Equals(actualType)); | |
} | |
private bool RequiresValueComparison(dynamic node) | |
{ | |
if (node == null) | |
return true; //expected is blank - that can only mean that it is expected to be blank | |
bool isAnyString = node.Equals(SuperSecretAnyString); | |
bool isAnyNumber = node.Equals(SuperSecretAnyNumber); | |
return !isAnyNumber && !isAnyString; | |
} | |
private string GetTypeOfDynamic(dynamic d) | |
{ | |
if (d == null) | |
return "null"; | |
if(d as string != null) | |
{ | |
if (d.Equals(SuperSecretWildcard)) | |
return WildcardType; | |
if (d.Equals(SuperSecretAnyNumber)) | |
return NumberType; | |
//SuperSecretAnyString & all other strings | |
return StringType; | |
} | |
if(IsNumeric(d)) | |
{ | |
return NumberType; | |
} | |
return d.GetType().Name; | |
} | |
private string AsString(dynamic value) | |
{ | |
if (value == null) | |
return "null"; | |
if(value is string) | |
{ | |
return string.Format("\"{0}\"", value); | |
} | |
return value.ToString(); | |
} | |
private Dictionary<string, dynamic> Deserialize(string json) | |
{ | |
var withWildcardReplaced = SanitizeWildcards(json); | |
var readyJson = WithToplevelAsDictionarizable(withWildcardReplaced); | |
//Started breaking once top level was array expresseion... | |
return JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(readyJson); | |
} | |
private string WithToplevelAsDictionarizable(string json) | |
{ | |
if (json.Trim().StartsWith("[")) | |
return string.Format(@"{{ ""TopLevelArray"" : {0} }}", json); | |
return json; | |
} | |
private string Prettify(string json) | |
{ | |
var x = JsonConvert.DeserializeObject(json); | |
return JsonConvert.SerializeObject(x, Formatting.Indented); | |
} | |
private string SanitizeWildcards(string json) | |
{ | |
return json | |
.Replace("*somestring*", string.Format("\"{0}\"", SuperSecretWildcard)) | |
.Replace("*somestring*", string.Format("\"{0}\"", SuperSecretAnyString)) | |
.Replace("*somenumber*", string.Format("\"{0}\"", SuperSecretAnyNumber)); | |
} | |
public static bool IsNumeric(object expression) | |
{ | |
if (expression == null || expression is DateTime) | |
return false; | |
if (expression is Int16 || expression is Int32 || expression is Int64 || expression is Decimal || expression is Single || expression is Double) | |
return true; | |
try | |
{ | |
if (expression is string) | |
Double.Parse(expression as string); | |
else | |
Double.Parse(expression.ToString()); | |
return true; | |
} | |
catch { } // just dismiss errors but return false | |
return false; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment