Created
September 30, 2013 18:15
-
-
Save mhinze/6767828 to your computer and use it in GitHub Desktop.
All properties should match assertion based on Rhino.Mocks AllPropertiesMatchConstraint
This file contains hidden or 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.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