Skip to content

Instantly share code, notes, and snippets.

@chrismckelt
Last active August 29, 2015 14:08
Show Gist options
  • Save chrismckelt/a9d338cd7b3b2745a4e0 to your computer and use it in GitHub Desktop.
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
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