Skip to content

Instantly share code, notes, and snippets.

@mhinze
Created September 30, 2013 18:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mhinze/6767828 to your computer and use it in GitHub Desktop.
Save mhinze/6767828 to your computer and use it in GitHub Desktop.
All properties should match assertion based on Rhino.Mocks AllPropertiesMatchConstraint
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
public static class ShouldExtensions
{
/// <summary>
/// This uses the same logic as AllPropertiesMatchConstraint from Rhino Mocks
/// </summary>
public static void ShouldMatchAllProperties(this object actual, object expected)
{
var constraint = new AllPropertiesMatchConstraint(expected);
if (!constraint.Eval(actual))
{
Assert.Fail(constraint.Message);
}
}
sealed class AllPropertiesMatchConstraint
{
readonly List<object> _checkedObjects;
readonly object _expected;
readonly Stack<string> _properties;
string _message;
/// <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="M:Rhino.Mocks.Constraints.AllPropertiesMatchConstraint.Eval(System.Object)" /> method </param>
public AllPropertiesMatchConstraint(object expected)
{
_expected = expected;
_properties = new Stack<string>();
_checkedObjects = new List<object>();
}
/// <summary>
/// Rhino.Mocks uses this property to generate an error message.
/// </summary>
/// <value> A message telling the tester why the constraint failed. </value>
public string Message
{
get { return _message; }
}
/// <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);
var flag = CheckReferenceType(_expected, obj);
_properties.Pop();
_checkedObjects.Clear();
return flag;
}
/// <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>
bool CheckReferenceType(object expected, object actual)
{
var type1 = expected.GetType();
var type2 = actual.GetType();
if (type1 == type2)
return CheckValue(expected, actual);
_message = string.Format("Expected type '{0}' doesn't match with actual type '{1}'", type1.Name,
type2.Name);
return false;
}
/// <summary />
/// <param name="expected" />
/// <param name="actual" />
/// <returns />
/// <remarks>
/// This is the real heart of the beast.
/// </remarks>
bool CheckValue(object expected, object actual)
{
if (actual == null && expected != null)
{
_message = string.Format("Expected value of {0} is '{1}', actual value is null", BuildPropertyName(),
expected);
return false;
}
if (expected == null)
{
if (actual != null)
{
_message = string.Format("Expected value of {0} is null, actual value is '{1}'",
BuildPropertyName(), actual);
return false;
}
}
else if (expected is IComparable)
{
if (!expected.Equals(actual))
{
_message = string.Format("Expected value of {0} is '{1}', actual value is '{2}'",
BuildPropertyName(), expected, actual);
return false;
}
}
else if (expected is IEnumerable)
{
if (!CheckCollection((IEnumerable) expected, (IEnumerable) actual))
return false;
}
else if (!_checkedObjects.Contains(expected))
{
_checkedObjects.Add(expected);
if (!CheckProperties(expected, actual) || !CheckFields(expected, actual))
return false;
}
return true;
}
/// <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>
bool CheckProperties(object expected, object actual)
{
var type = expected.GetType();
foreach (var propertyInfo in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (propertyInfo.GetIndexParameters().Length == 0)
{
_properties.Push(propertyInfo.Name);
try
{
if (!CheckValue(propertyInfo.GetValue(expected, null), propertyInfo.GetValue(actual, null)))
return false;
}
catch (TargetInvocationException) {}
_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>
bool CheckFields(object expected, object actual)
{
var type = expected.GetType();
_checkedObjects.Add(actual);
foreach (var fieldInfo in type.GetFields(BindingFlags.Instance | BindingFlags.Public))
{
_properties.Push(fieldInfo.Name);
var flag = CheckValue(fieldInfo.GetValue(expected), fieldInfo.GetValue(actual));
_properties.Pop();
if (!flag)
return false;
}
return true;
}
/// <summary>
/// Checks the items of both collections
/// </summary>
/// <param name="expectedCollection"> The expected collection </param>
/// <param name="actualCollection" />
/// <returns> True if both collections contain the same items in the same order. </returns>
bool CheckCollection(IEnumerable expectedCollection, IEnumerable actualCollection)
{
if (expectedCollection != null)
{
var enumerator1 = expectedCollection.GetEnumerator();
var enumerator2 = actualCollection.GetEnumerator();
var flag1 = enumerator1.MoveNext();
var flag2 = enumerator2.MoveNext();
var num1 = 0;
var num2 = 0;
var str = _properties.Pop();
for (; flag1 && flag2; flag2 = enumerator2.MoveNext())
{
var current1 = enumerator1.Current;
var current2 = enumerator2.Current;
_properties.Push(str + string.Format("[{0}]", num1));
++num1;
++num2;
if (!CheckReferenceType(current1, current2))
return false;
_properties.Pop();
flag1 = enumerator1.MoveNext();
}
_properties.Push(str);
if (flag1 & !flag2)
{
do
{
++num1;
} while (enumerator1.MoveNext());
}
if (!flag1 & flag2)
{
do
{
++num2;
} while (enumerator2.MoveNext());
}
if (num1 != num2)
{
_message = string.Format(
"expected number of items in collection {0} is '{1}', actual is '{2}'", BuildPropertyName(),
num1, num2);
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>
string BuildPropertyName()
{
var stringBuilder = new StringBuilder();
foreach (var str in _properties.ToArray())
{
if (stringBuilder.Length > 0)
stringBuilder.Insert(0, '.');
stringBuilder.Insert(0, str);
}
return (stringBuilder).ToString();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment