Last active
August 29, 2015 14:08
-
-
Save chrismckelt/a9d338cd7b3b2745a4e0 to your computer and use it in GitHub Desktop.
NSubstitute Extensions–AllPropertiesMatch -- argument matcher for object to object checking with support for dynamic DTO comparison
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; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
using System.Diagnostics; | |
using System.Dynamic; | |
using System.Linq.Expressions; | |
using System.Reflection; | |
using System.Text; | |
using NSubstitute; | |
using Xunit; | |
// Tests at bottom | |
namespace TestExtensions | |
{ | |
public static class NSubstituteExtensions | |
{ | |
public static Tuple<bool, string> AllPropertiesMatch(this object expected, object actual) | |
{ | |
// if 1 is an expando - make sure both are | |
expected = CheckForExpandoObjects(expected, ref actual); | |
var matcher = new PropertyMatcher(expected); | |
bool result = matcher.Eval(actual); | |
if (!result) Debug.WriteLine(matcher.Message); | |
return new Tuple<bool, string>(result, matcher.Message); | |
} | |
private static object CheckForExpandoObjects(object expected, ref object actual) | |
{ | |
if (expected is ExpandoObject && actual.GetType() != typeof (ExpandoObject)) | |
{ | |
actual = actual.ToDynamic(); | |
} | |
if (actual is ExpandoObject && expected.GetType() != typeof (ExpandoObject)) | |
{ | |
expected = expected.ToDynamic(); | |
} | |
return expected; | |
} | |
public static dynamic ToDynamic(this object value) | |
{ | |
IDictionary<string, object> expando = new ExpandoObject(); | |
foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(value.GetType())) | |
expando.Add(property.Name, property.GetValue(value)); | |
return expando as ExpandoObject; | |
} | |
public static ExpandoObject MiniDto<T>(this T obj, Expression<Func<T, dynamic>> item, | |
params Expression<Func<T, dynamic>>[] items) where T : class | |
{ | |
var eo = new ExpandoObject(); | |
var props = eo as IDictionary<String, object>; | |
if (items == null) | |
{ | |
var temp = new List<Expression<Func<T, dynamic>>>(); | |
temp.Add(item); | |
items = temp.ToArray(); | |
} | |
foreach (var thing in items) | |
{ | |
var body = thing.Body as MemberExpression; | |
if (body == null) | |
{ | |
var ubody = (UnaryExpression) thing.Body; | |
body = ubody.Operand as MemberExpression; | |
} | |
if (body != null) | |
{ | |
var property = body.Member as PropertyInfo; | |
if (property != null) | |
props[property.Name] = obj.GetType().GetProperty(property.Name).GetValue(obj, null); | |
} | |
else | |
{ | |
var ubody = (UnaryExpression) thing.Body; | |
var property = ubody.Operand as MemberExpression; | |
if (property != null) | |
props[property.Member.Name] = obj.GetType() | |
.GetProperty(property.Member.Name) | |
.GetValue(obj, null); | |
} | |
} | |
return eo; | |
} | |
internal class PropertyMatcher | |
{ | |
private readonly object _expected; //object holding the expected property values. | |
private readonly StringBuilder _message = new StringBuilder(); // messages to return | |
private readonly Stack<string> _properties; | |
//used to build the property name like Order.Product.Price for the message. | |
private readonly List<object> _checkedObjects; | |
//every object that is matched goes in this list to prevent recursive loops. | |
/// <summary> | |
/// Initializes a new constraint object. | |
/// </summary> | |
/// <param name="expected"> | |
/// The expected object, The actual object is passed in as a parameter to the <see cref="Eval" /> | |
/// method | |
/// </param> | |
public PropertyMatcher(object expected) | |
{ | |
_expected = expected; | |
_properties = new Stack<string>(); | |
_checkedObjects = new List<object>(); | |
} | |
/// <summary> | |
/// Evaluate this constraint. | |
/// </summary> | |
/// <param name="obj">The actual object that was passed in the method call to the mock.</param> | |
/// <returns>True when the constraint is met, else false.</returns> | |
public bool Eval(object obj) | |
{ | |
_properties.Clear(); | |
_checkedObjects.Clear(); | |
_properties.Push(obj.GetType().Name); | |
bool result = CheckReferenceType(_expected, obj); | |
_properties.Pop(); | |
_checkedObjects.Clear(); | |
return result; | |
} | |
/// <summary> | |
/// Formatted error message. | |
/// </summary> | |
/// <value> | |
/// A message telling the tester why the constraint failed. | |
/// </value> | |
public string Message | |
{ | |
get { return _message.ToString(); } | |
} | |
/// <summary> | |
/// Checks if the properties of the <paramref name="actual" /> object | |
/// are the same as the properies of the <paramref name="expected" /> object. | |
/// </summary> | |
/// <param name="expected">The expected object</param> | |
/// <param name="actual">The actual object</param> | |
/// <returns>True when both objects have the same values, else False.</returns> | |
protected virtual bool CheckReferenceType(object expected, object actual) | |
{ | |
Type tExpected = expected.GetType(); | |
Type tActual = actual.GetType(); | |
if ((tExpected.BaseType != tActual) && (tExpected != tActual)) | |
{ | |
_message.AppendLine(string.Format("Expected type '{0}' doesn't match with actual type '{1}'", | |
tExpected.Name, tActual.Name)); | |
return false; | |
} | |
CheckValue(expected, actual); | |
if (_message.Length > 0) return false; | |
return true; | |
} | |
/// <summary> | |
/// </summary> | |
/// <param name="expected"></param> | |
/// <param name="actual"></param> | |
/// <returns></returns> | |
/// <remarks>This is the real heart of the beast.</remarks> | |
protected virtual void CheckValue(object expected, object actual) | |
{ | |
if (actual == null && expected != null) | |
{ | |
_message.AppendLine(string.Format("Expected value of {0} is '{1}', actual value is null", | |
BuildPropertyName(), expected)); | |
} | |
if (expected == null) | |
{ | |
if (actual != null) | |
{ | |
_message.AppendLine(string.Format("Expected value of {0} is null, actual value is '{1}'", | |
BuildPropertyName(), actual)); | |
} | |
} | |
else | |
{ | |
//if both objects are comparable Equals can be used to determine equality. (value types implement IComparable too when boxed) | |
if (expected is IComparable) | |
{ | |
if (!expected.Equals(actual)) | |
{ | |
if (actual != null) | |
_message.AppendLine( | |
string.Format("Expected value of {0} is '{1}', actual value is '{2}'", | |
BuildPropertyName(), expected.ToString(), actual.ToString())); | |
} | |
} | |
else if (expected is IEnumerable) //if both objects are lists we should tread them as such. | |
{ | |
if (!CheckCollection((IEnumerable) expected, (IEnumerable) actual)) | |
{ | |
} | |
} | |
else if (expected is KeyValuePair<string, object>) | |
{ | |
var k1 = (KeyValuePair<string, object>) expected; | |
if (actual != null) | |
{ | |
var k2 = (KeyValuePair<string, object>) actual; | |
_checkedObjects.Add(expected); | |
if (k1.Value.ToString() != k2.Value.ToString()) | |
{ | |
_message.AppendLine( | |
string.Format( | |
"Expected values for '{0}' do not match. Expected: '{1}' Actual: '{2}'", | |
k1.Key, k1.Value, k2.Value)); | |
} | |
} | |
else | |
{ | |
_message.AppendLine( | |
string.Format("Expected values for '{0}' do not match. Expected: '{1}' Actual: '{2}'", | |
k1.Key, k1.Value, null)); | |
} | |
} | |
else if (!_checkedObjects.Contains(expected)) //prevent endless recursive loops. | |
{ | |
_checkedObjects.Add(expected); | |
if (!CheckProperties(expected, actual)) | |
{ | |
} | |
if (!CheckFields(expected, actual)) | |
{ | |
} | |
} | |
} | |
} | |
/// <summary> | |
/// Used by CheckReferenceType to check all properties of the reference type. | |
/// </summary> | |
/// <param name="expected">The expected object</param> | |
/// <param name="actual">The actual object</param> | |
/// <returns>True when both objects have the same values, else False.</returns> | |
protected virtual bool CheckProperties(object expected, object actual) | |
{ | |
Type tExpected = expected.GetType().BaseType; | |
PropertyInfo[] properties = tExpected.GetProperties(BindingFlags.Instance | BindingFlags.Public); | |
foreach (PropertyInfo property in properties) | |
{ | |
//TODO: deal with indexed properties | |
ParameterInfo[] indexParameters = property.GetIndexParameters(); | |
if (indexParameters.Length == 0) //It's not an indexed property | |
{ | |
_properties.Push(property.Name); | |
try | |
{ | |
object expectedValue = property.GetValue(expected, null); | |
object actualValue = property.GetValue(actual, null); | |
//if (!CheckValue(expectedValue, actualValue)) return false; | |
CheckValue(expectedValue, actualValue); | |
} | |
catch (TargetInvocationException) | |
{ | |
//the inner exception should give you a clou about why we couldn't invoke GetValue... | |
//do nothing | |
} | |
_properties.Pop(); | |
} | |
} | |
return true; | |
} | |
/// <summary> | |
/// Used by CheckReferenceType to check all fields of the reference type. | |
/// </summary> | |
/// <param name="expected">The expected object</param> | |
/// <param name="actual">The actual object</param> | |
/// <returns>True when both objects have the same values, else False.</returns> | |
protected virtual bool CheckFields(object expected, object actual) | |
{ | |
Type tExpected = expected.GetType().BaseType; | |
_checkedObjects.Add(actual); | |
FieldInfo[] fields = tExpected.GetFields(BindingFlags.Instance | BindingFlags.Public); | |
foreach (FieldInfo field in fields) | |
{ | |
_properties.Push(field.Name); | |
object expectedValue = field.GetValue(expected); | |
object actualValue = field.GetValue(actual); | |
CheckValue(expectedValue, actualValue); | |
_properties.Pop(); | |
if (_message.Length > 0) return false; | |
return true; | |
} | |
return true; | |
} | |
/// <summary> | |
/// Checks the items of both collections | |
/// </summary> | |
/// <param name="expectedCollection">The expected collection</param> | |
/// <param name="actualCollection"></param> | |
/// <returns>True if both collections contain the same items in the same order.</returns> | |
private bool CheckCollection(IEnumerable expectedCollection, IEnumerable actualCollection) | |
{ | |
if (expectedCollection != null) //only check the list if there is something in there. | |
{ | |
IEnumerator expectedEnumerator = expectedCollection.GetEnumerator(); | |
IEnumerator actualEnumerator = actualCollection.GetEnumerator(); | |
bool expectedHasMore = expectedEnumerator.MoveNext(); | |
bool actualHasMore = actualEnumerator.MoveNext(); | |
int expectedCount = 0; | |
int actualCount = 0; | |
string name = _properties.Pop(); | |
//pop the propertyname from the stack to be replaced by the same name with an index. | |
while (expectedHasMore && actualHasMore) | |
{ | |
object expectedValue = expectedEnumerator.Current; | |
object actualValue = actualEnumerator.Current; | |
_properties.Push(name + string.Format("[{0}]", expectedCount)); | |
//replace the earlier popped property name | |
expectedCount++; | |
actualCount++; | |
if (!CheckReferenceType(expectedValue, actualValue)) | |
{ | |
return false; | |
} | |
_properties.Pop(); //pop the old indexed property name to make place for a new one | |
expectedHasMore = expectedEnumerator.MoveNext(); | |
actualHasMore = actualEnumerator.MoveNext(); | |
} | |
_properties.Push(name); //push the original property name back on the stack. | |
//examine the expectedMoveNextResult and the actualMoveNextResult to see if one collection was bigger than the other. | |
if (expectedHasMore & !actualHasMore) //actual has less items than expected. | |
{ | |
//find out how much items there are in the expected collection. | |
do expectedCount++; while (expectedEnumerator.MoveNext()); | |
} | |
if (!expectedHasMore & actualHasMore) //actual has more items than expected. | |
{ | |
//find out how much items there are in the actual collection. | |
do actualCount++; while (actualEnumerator.MoveNext()); | |
} | |
if (expectedCount != actualCount) | |
{ | |
_message.AppendLine( | |
string.Format("expected number of items in collection {0} is '{1}', actual is '{2}'", | |
BuildPropertyName(), expectedCount, actualCount)); | |
return false; | |
} | |
} | |
return true; | |
} | |
/// <summary> | |
/// Builds a propertyname from the Stack _properties like 'Order.Product.Price' | |
/// to be used in the error message. | |
/// </summary> | |
/// <returns>A nested property name.</returns> | |
private string BuildPropertyName() | |
{ | |
var result = new StringBuilder(); | |
string[] names = _properties.ToArray(); | |
foreach (string name in names) | |
{ | |
if (result.Length > 0) | |
{ | |
result.Insert(0, '.'); | |
} | |
result.Insert(0, name); | |
} | |
return result.ToString(); | |
} | |
} | |
} | |
public class NSubstituteExtensionsTestFixture | |
{ | |
public class TestObject | |
{ | |
public Guid AggregateId { get; set; } | |
public Guid CausationId { get; set; } | |
public Guid CustomerId { get; set; } | |
} | |
[Fact] | |
public void PropertyMatcher_StandardObjectMatches() | |
{ | |
Guid guid = Guid.NewGuid(); | |
var a = Substitute.For<TestObject>(); | |
a.AggregateId = guid; | |
a.CausationId = guid; | |
var b = new TestObject(); | |
b.AggregateId = guid; | |
b.CausationId = guid; // a.CausationId; | |
// test result + error message | |
Tuple<bool, string> standardMatch = a.AllPropertiesMatch(b); | |
if (!standardMatch.Item1) | |
{ | |
Assert.False(true, "standardMatch -- " + standardMatch.Item2); | |
} | |
else | |
{ | |
Assert.True(true, "standardMatch -- " + standardMatch.Item2); | |
} | |
} | |
[Fact] | |
public void PropertyMatcher_StandardObjectMatches_FailCheck() | |
{ | |
Guid guid = Guid.NewGuid(); | |
var a = Substitute.For<TestObject>(); | |
a.AggregateId = Guid.NewGuid(); | |
a.CausationId = Guid.NewGuid(); | |
var b = new TestObject(); | |
b.AggregateId = guid; | |
b.CausationId = guid; // a.CausationId; | |
// test result + error message | |
Tuple<bool, string> standardMatch = a.AllPropertiesMatch(b); | |
if (!standardMatch.Item1) | |
{ | |
Assert.True(true, "standardMatch -- " + standardMatch.Item2); | |
} | |
else | |
{ | |
Assert.False(true, "standardMatch -- " + standardMatch.Item2); | |
} | |
} | |
[Fact] | |
public void PropertyMatcher_DynamicObjectMatches() | |
{ | |
Guid guid = Guid.NewGuid(); | |
var a = Substitute.For<TestObject>(); | |
a.AggregateId = guid; | |
a.CausationId = guid; | |
var b = new TestObject(); | |
b.AggregateId = guid; | |
b.CausationId = guid; // a.CausationId; | |
// test result + error message | |
Tuple<bool, string> dynamicMatch = | |
a.MiniDto( | |
c => c.AggregateId, | |
d => d.CustomerId, | |
e => e.CausationId) | |
.AllPropertiesMatch( | |
b.MiniDto( | |
f => f.AggregateId, | |
g => g.CustomerId, | |
h => h.CausationId)); | |
if (!dynamicMatch.Item1) | |
{ | |
Assert.False(true, "dynamicMatch -- " + dynamicMatch.Item2); | |
} | |
else | |
{ | |
Assert.True(true, "dynamicMatch -- " + dynamicMatch.Item2); | |
} | |
} | |
[Fact] | |
public void PropertyMatcher_DynamicObjectMatches_FailCheck() | |
{ | |
Guid guid = Guid.NewGuid(); | |
var a = Substitute.For<TestObject>(); | |
a.AggregateId = guid; | |
a.CausationId = guid; | |
var b = new TestObject(); | |
b.AggregateId = Guid.NewGuid(); | |
b.CausationId = Guid.NewGuid(); // a.CausationId; | |
// test result + error message | |
Tuple<bool, string> dynamicMatch = | |
a.MiniDto( | |
c => c.AggregateId, | |
d => d.CustomerId, | |
e => e.CausationId) | |
.AllPropertiesMatch( | |
b.MiniDto( | |
f => f.AggregateId, | |
g => g.CustomerId, | |
h => h.CausationId)); | |
if (!dynamicMatch.Item1) | |
{ | |
Assert.True(true, "dynamicMatch -- " + dynamicMatch.Item2); | |
} | |
else | |
{ | |
Assert.False(true, "dynamicMatch -- " + dynamicMatch.Item2); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment