0

I am trying to initialize objects for tests purposes. After initializing an object, I want to assert that all properties whose underlying type is a value type have a value that is not the default value of the property's type, with certain exceptions. I am not trying to recurse into sub-objects. For this purpose, I have the following:

    public static void NoValueTypedPropertyHasDefaultValue<T>(this T t, params string[] exceptions)
        where T: class
    {
        foreach (var property in typeof(T).GetProperties())
        {
            var propertyType = property.PropertyType;
            if (propertyType.IsValueType && !exceptions.Any(x => x == property.Name))
            {
                var defaultValue = Activator.CreateInstance(propertyType);
                object actualValue = property.GetValue(t);
                Assert.NotEqual(defaultValue, actualValue);
            }
        }
    }

Provided that everything has a parameterless constructor, it works. I'm not thrilled with the way I'm passing in strings for the exceptions. Is there a better way to do that?

William Jockusch
  • 26,513
  • 49
  • 182
  • 323
  • 1
    Define: better. Sounds opinionated. – Jamiec Sep 23 '20 at 17:52
  • I'm not thrilled that the caller could just type in the string, in which case it would not survive a renaming of the property. – William Jockusch Sep 23 '20 at 18:05
  • 1
    I'd probably rename `exceptions` to `propertiesToIngnore`, exceptions implies various runtime error Exceptions to me. Then change them from strings to `PropertyInfo` objects – theKunz Sep 23 '20 at 18:22
  • Hmmm, `typeof(MyObject).GetProperty(nameof(MyObject.PropertyName)))` seems to be the way to create a PropertyInfo object? Not sure I like that. Definitely like propertiesToIgnore though. – William Jockusch Sep 23 '20 at 19:39
  • 1
    You mentioned: "Provided that everything has a parameterless constructor". You don't need to worry - since you're already filtering on value types (`propertyType.IsValueType`, and "not trying to recurse into sub-objects"), `Activator.CreateInstance` will work fine. But note that `string` is not a value type, so you'll skip over any string properties. – Sean Skelly Sep 23 '20 at 22:37
  • There is a better way using expressions so you could do `NoValueTypedPropertyHasDefaultValue(myObject, o => o.Prop1, o => o.Prop2)` etc. So you'll get build failures if a property is renamed. If that looks better for you I'll have a go at an example later – Jamiec Sep 24 '20 at 07:54
  • Far better, yes. – William Jockusch Sep 24 '20 at 13:21

1 Answers1

0

You mentioned in the comments:

I'm not thrilled that the caller could just type in the string, in which case it would not survive a renaming of the property.

Making use of System.Linq.Expressions can give you compile-time safety allowing code such as:

var inst = new MyType { MyInt1 = 1 }; // Set MyInt1 but not MyInt2
inst.NoValueTypedPropertyHasDefaultValue(x => x.MyInt2); // Ignore MyInt2 from validation

The code looks like below (H/T to Retrieving Property name from lambda expression) :

public static class Extensions
{
    public static void NoValueTypedPropertyHasDefaultValue<T>(this T t, params Expression<Func<T,object>>[] propertiesToIgnore)
        where T : class
    {
        var propertyNamesToIgnore = propertiesToIgnore.Select(x => GetMemberInfo(x).Member.Name).ToArray();
        foreach (var property in typeof(T).GetProperties())
        {
            var propertyType = property.PropertyType;
            if (propertyType.IsValueType && !propertyNamesToIgnore.Contains(property.Name))
            {
                var defaultValue = Activator.CreateInstance(propertyType);
                object actualValue = property.GetValue(t);
                Assert.NotEqual(defaultValue, actualValue);
            }
        }
    }

    private static MemberExpression GetMemberInfo(Expression method)
    {
        LambdaExpression lambda = method as LambdaExpression;
        if (lambda == null)
            throw new ArgumentNullException("method");

        MemberExpression memberExpr = null;

        if (lambda.Body.NodeType == ExpressionType.Convert)
        {
            memberExpr =
                ((UnaryExpression)lambda.Body).Operand as MemberExpression;
        }
        else if (lambda.Body.NodeType == ExpressionType.MemberAccess)
        {
            memberExpr = lambda.Body as MemberExpression;
        }

        if (memberExpr == null)
            throw new ArgumentException("method");

        return memberExpr;
    }
}

Live example: https://dotnetfiddle.net/W9Q4gN (Note I have replaced your Assert call with an exception just for testing)


However, if all of that looks a little bit crazy/overkill to you (and to some extent, it really is!!) remember you could have just used nameof with your original scheme. Using my example your code would have become:

var inst = new MyType { MyInt1 = 1 }; // Set MyInt1 but not MyInt2
inst.NoValueTypedPropertyHasDefaultValue(nameof(inst.MyInt2)); // Ignore MyInt2 from validation
Jamiec
  • 133,658
  • 13
  • 134
  • 193