Skip to content

Instantly share code, notes, and snippets.

@quexy
Last active August 29, 2015 14:01
Show Gist options
  • Save quexy/98c4ef7ec6bf693031c8 to your computer and use it in GitHub Desktop.
Save quexy/98c4ef7ec6bf693031c8 to your computer and use it in GitHub Desktop.
Object verifier for SpecFlow
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions;
namespace TechTalk.SpecFlow.ObjectVerification
{
public static class ObjectVerifierExtensions
{
/// <summary>
/// Specifies the aliases in the verification data for the given property of the object. There can be more than one alias for any property, but not vice versa.
/// </summary>
public static IConfiguredObjectVerifier<TObject> WithPropertyAlias<TObject, TProperty>(this IConfiguredObjectVerifier<TObject> objectVerifier, Expression<Func<TObject, TProperty>> selector, params string[] aliases)
{
foreach (var alias in aliases)
objectVerifier.WithPropertyAlias(selector, alias);
return objectVerifier;
}
/// <summary>
/// Specifies the equality comparer to use when verifying the given field mapped to the verified object (comparison priority: table field, object property, target value, default)
/// </summary>
public static IConfiguredObjectVerifier<TObject> WithFieldEqualityComparer<TObject, TValue>(this IConfiguredObjectVerifier<TObject> objectVerifier, string name, IEqualityComparer<TValue> comparer)
{
objectVerifier.WithFieldEqualityComparer<TValue>(name, (a, b) => comparer.Equals(a, b));
return objectVerifier;
}
/// <summary>
/// Specifies the equality comparer to use when verifying the given property of the verified object (comparison priority: table field, object property, target value, default)
/// </summary>
public static IConfiguredObjectVerifier<TObject> WithPropertyEqualityComparer<TObject, TValue>(this IConfiguredObjectVerifier<TObject> objectVerifier, Expression<Func<TObject, TValue>> selector, IEqualityComparer<TValue> comparer)
{
objectVerifier.WithPropertyEqualityComparer<TValue>(selector, (a, b) => comparer.Equals(a, b));
return objectVerifier;
}
/// <summary>
/// Specifies the equality comparer to use when verifying properties of the given type (comparison priority: table field, object property, target value, default)
/// </summary>
public static IConfiguredObjectVerifier<TObject> WithTypeEqualityComparer<TObject, TValue>(this IConfiguredObjectVerifier<TObject> objectVerifier, IEqualityComparer<TValue> comparer)
{
objectVerifier.WithTypeEqualityComparer<TValue>((a, b) => comparer.Equals(a, b));
return objectVerifier;
}
/// <summary>
/// Returns a converter function for the given type. This method is the default converter provider.
/// </summary>
public static Func<string, object> GetDefaultConverter(Type type)
{
if (type == typeof(string))
return s => s;
if (type.IsGenericType && type.Name == "Nullable`1")
return val =>
{
if (string.IsNullOrEmpty(val)) return null;
else return GetDefaultConverter(type.GetGenericArguments()[0])(val);
};
if (type.IsEnum)
return value => Enum.Parse(type, value);
return value => TypeDescriptor.GetConverter(type).ConvertFromString(value);
}
}
public static class ExpressionExtensions
{
/// <summary>
/// Returns the property name from a property selector expression
/// </summary>
public static string GetPropertyName<TObject, TValue>(this Expression<Func<TObject, TValue>> property)
{
var exp = (LambdaExpression)property;
if (exp.Body.NodeType == ExpressionType.Parameter)
return "item";
var mExp = (exp.Body.NodeType == ExpressionType.MemberAccess) ?
(MemberExpression)exp.Body :
(MemberExpression)((UnaryExpression)exp.Body).Operand;
return mExp.Member.Name;
}
}
public interface IObjectVerifier<TObject>
{
/// <summary>
/// Allows customizing the object verification
/// </summary>
IConfiguredObjectVerifier<TObject> WithConfiguration();
/// <summary>
/// Verifies the object with a vertical property-value collection
/// </summary>
string[] Verify(IEnumerable<IDictionary<string, string>> expected, TObject actual);
}
public interface IConfiguredObjectVerifier<TObject> : IObjectVerifier<TObject>
{
/// <summary>
/// Specifies an alias in the verification data for the given property of the object. There can be more than one alias for any property, but not vice versa.
/// </summary>
IConfiguredObjectVerifier<TObject> WithPropertyAlias<TProperty>(Expression<Func<TObject, TProperty>> selector, string alias);
/// <summary>
/// Specifies the converter to use for the given verification data field (converter priority: table field, object property, target value, default)
/// </summary>
IConfiguredObjectVerifier<TObject> WithFieldValueConverter<TProperty>(string name, Func<string, TProperty> converter);
/// <summary>
/// Specifies the converter to use for the given property of the verified object (converter priority: table field, object property, target value, default)
/// </summary>
IConfiguredObjectVerifier<TObject> WithPropertyValueConverter<TProperty>(Expression<Func<TObject, TProperty>> selector, Func<string, TProperty> converter);
/// <summary>
/// Specifies a conversion method to use when converting to the given value (converter priority: table field, object property, target value, default)
/// </summary>
IConfiguredObjectVerifier<TObject> WithTypeValueConverter<TValue>(Func<string, TValue> converter);
/// <summary>
/// Specifies the provider method to use to obtain the default value converter for any given type (converter priority: table field, object property, target value, default)
/// </summary>
IConfiguredObjectVerifier<TObject> WithDefaultConverter(Func<Type, Func<string, object>> converterProvider);
/// <summary>
/// Specifies the equality comparison method to use when verifying the given field mapped to the verified object (comparison priority: table field, object property, target value, default)
/// </summary>
IConfiguredObjectVerifier<TObject> WithFieldEqualityComparer<TValue>(string name, Func<TValue, TValue, bool> comparer);
/// <summary>
/// Specifies the equality comparison method to use when verifying the given property of the verified object (comparison priority: table field, object property, target value, default)
/// </summary>
IConfiguredObjectVerifier<TObject> WithPropertyEqualityComparer<TValue>(Expression<Func<TObject, TValue>> selector, Func<TValue, TValue, bool> comparer);
/// <summary>
/// Specifies the equality comparison method to use when verifying properties of the given type (comparison priority: table field, object property, target value, default)
/// </summary>
IConfiguredObjectVerifier<TObject> WithTypeEqualityComparer<TValue>(Func<TValue, TValue, bool> comparer);
}
public static class ObjectVerifier
{
/// <summary>
/// Creates an object verifier for a vertical property-value collection with the specified field keys.
/// </summary>
/// <param name="keyField">field for property names</param>
/// <param name="valueField">field for property values</param>
public static IObjectVerifier<TObject> For<TObject>(string keyField, string valueField)
{
return new ObjectVerifier<TObject>(keyField, valueField);
}
}
internal sealed class ObjectVerifier<TObject> : IConfiguredObjectVerifier<TObject>
{
private readonly string keyField;
private readonly string valueField;
internal ObjectVerifier(string keyField, string valueField)
{
this.keyField = keyField;
this.valueField = valueField;
}
public string[] Verify(IEnumerable<IDictionary<string, string>> expectedObject, TObject actualObject)
{
var errors = new List<string>();
foreach (var item in expectedObject)
{
var fieldName = item[keyField];
var propertyName = fieldName;
if (propertyAliases.ContainsKey(propertyName))
propertyName = propertyAliases[propertyName];
var property = typeof(TObject).GetProperty(propertyName);
if (property == null)
throw new InvalidOperationException(string.Format("Invalid property name '{0}'", propertyName));
Func<string, object> converter = null;
if (converter == null) fieldValueConverters.TryGetValue(fieldName, out converter);
if (converter == null) propertyValueConverters.TryGetValue(propertyName, out converter);
if (converter == null) valueConverters.TryGetValue(property.PropertyType, out converter);
if (converter == null) converter = defaultConverterProvider(property.PropertyType);
Func<object, object, bool> comparer = null;
if (comparer == null) fieldComparers.TryGetValue(fieldName, out comparer);
if (comparer == null) propertyComparers.TryGetValue(propertyName, out comparer);
if (comparer == null) equalityComparers.TryGetValue(property.PropertyType, out comparer);
if (comparer == null) comparer = (exp, act) => object.Equals(exp, act);
var expectedValue = converter(item[valueField]);
var actualValue = property.GetValue(actualObject);
if (!comparer(expectedValue, actualValue))
errors.Add(string.Format("The values for {0} do not match (expected:<{2}>, actual:<{3}>)",
fieldName, propertyName, expectedValue, actualValue));
}
return errors.ToArray();
}
public IConfiguredObjectVerifier<TObject> WithConfiguration()
{
return this;
}
private Func<Type, Func<string, object>> defaultConverterProvider = ObjectVerifierExtensions.GetDefaultConverter;
public IConfiguredObjectVerifier<TObject> WithDefaultConverter(Func<Type, Func<string, object>> converterProvider)
{
defaultConverterProvider = converterProvider;
return this;
}
private readonly Dictionary<string, string> propertyAliases = new Dictionary<string, string>();
public IConfiguredObjectVerifier<TObject> WithPropertyAlias<TProperty>(Expression<Func<TObject, TProperty>> selector, string alias)
{
propertyAliases.Add(alias, selector.GetPropertyName());
return this;
}
private readonly Dictionary<string, Func<string, object>> fieldValueConverters = new Dictionary<string, Func<string, object>>();
public IConfiguredObjectVerifier<TObject> WithFieldValueConverter<TProperty>(string name, Func<string, TProperty> converter)
{
fieldValueConverters.Add(name, v => converter(v));
return this;
}
private readonly Dictionary<string, Func<string, object>> propertyValueConverters = new Dictionary<string, Func<string, object>>();
public IConfiguredObjectVerifier<TObject> WithPropertyValueConverter<TProperty>(Expression<Func<TObject, TProperty>> selector, Func<string, TProperty> converter)
{
propertyValueConverters.Add(selector.GetPropertyName(), v => converter(v));
return this;
}
private readonly Dictionary<Type, Func<string, object>> valueConverters = new Dictionary<Type, Func<string, object>>();
public IConfiguredObjectVerifier<TObject> WithTypeValueConverter<TValue>(Func<string, TValue> converter)
{
valueConverters.Add(typeof(TValue), v => converter(v));
return this;
}
private readonly Dictionary<string, Func<object, object, bool>> fieldComparers = new Dictionary<string, Func<object, object, bool>>();
public IConfiguredObjectVerifier<TObject> WithFieldEqualityComparer<TValue>(string name, Func<TValue, TValue, bool> comparer)
{
fieldComparers.Add(name, (a, b) => comparer((TValue)a, (TValue)b));
return this;
}
private readonly Dictionary<string, Func<object, object, bool>> propertyComparers = new Dictionary<string, Func<object, object, bool>>();
public IConfiguredObjectVerifier<TObject> WithPropertyEqualityComparer<TValue>(Expression<Func<TObject, TValue>> selector, Func<TValue, TValue, bool> comparer)
{
propertyComparers.Add(selector.GetPropertyName(), (a, b) => comparer((TValue)a, (TValue)b));
return this;
}
private readonly Dictionary<Type, Func<object, object, bool>> equalityComparers = new Dictionary<Type, Func<object, object, bool>>();
public IConfiguredObjectVerifier<TObject> WithTypeEqualityComparer<TValue>(Func<TValue, TValue, bool> comparer)
{
equalityComparers.Add(typeof(TValue), (a, b) => comparer((TValue)a, (TValue)b));
return this;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment