Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to set a private lazy<T> with reflection for testing purposes in C#?

The problem description

We have a pretty big system, that used to eager load data into properies with private setters. For using testing specific scenarios, I used to write data in those properties with private setters.

However, because the system was getting slow, and was loading unnessesary things, we changed certain things to lazy loading, using the Lazy class. However, now I'm no longer able to write data into those properties, so a lot of unit tests won't run anymore.

What we used to have

The object to test:

public class ComplexClass
{
    public DateTime Date { get; private set; }

    public ComplexClass()
    {
        // Sample data, eager loading data into variable
        Date = DateTime.Now;
    }
    public string GetDay()
    {
        if (Date.Day == 1 && Date.Month == 1)
        {
            return "New year!";
        }
        return string.Empty;
    }
}

How the tests look like:

[Test]
public void TestNewyear()
{
    var complexClass = new ComplexClass();
    var newYear = new DateTime(2014, 1, 1);
    ReflectionHelper.SetProperty(complexClass, "Date", newYear);

    Assert.AreEqual("New year!", complexClass.GetDay());
}

The implementation of the ReflectionHelper used in above sample.

public static class ReflectionHelper
{
    public static void SetProperty(object instance, string properyName, object value)
    {
        var type = instance.GetType();

        var propertyInfo = type.GetProperty(properyName);
        propertyInfo.SetValue(instance, Convert.ChangeType(value, propertyInfo.PropertyType), null);
    }
}

What we have now

The object to test:

public class ComplexClass
{
    private readonly Lazy<DateTime> _date;

    public DateTime Date
    {
        get
        {
            return _date.Value;
        }
    }

    public ComplexClass()
    {
        // Sample data, lazy loading data into variable
        _date = new Lazy<DateTime>(() => DateTime.Now);
    }
    public string GetDay()
    {
        if (Date.Day == 1 && Date.Month == 1)
        {
            return "New year!";
        }
        return string.Empty;
    }
}

Attempt to solve it

Now keep in mind, this is only one sample. The changes to code from eager loading to lazy loading is changed on a lot of different places. Because we don't want to change the code for all tests, the best option seemed to be to change the middleman: the ReflectionHelper

This is the current state of ReflectionHelper

Btw, I would like to apologize in advance for this weird piece of code

public static class ReflectionHelper
{
    public static void SetProperty(object instance, string properyName, object value)
    {
        var type = instance.GetType();

        try
        {
            var propertyInfo = type.GetProperty(properyName);
            propertyInfo.SetValue(instance, Convert.ChangeType(value, propertyInfo.PropertyType), null);
        }
        catch (ArgumentException e)
        {
            if (e.Message == "Property set method not found.")
            {
                // it does not have a setter. Maybe it has a backing field
                var fieldName = PropertyToField(properyName);
                var field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);

                // Create a new lazy at runtime, of the type value.GetType(), for comparing reasons
                var lazyGeneric = typeof(Lazy<>);
                var lazyGenericOfType = lazyGeneric.MakeGenericType(value.GetType());
                
                // If the field is indeed a lazy, we can attempt to set the lazy
                if (field.FieldType == lazyGenericOfType)
                {
                    var lazyInstance = Activator.CreateInstance(lazyGenericOfType);
                    var lazyValuefield = lazyGenericOfType.GetField("m_boxed", BindingFlags.NonPublic | BindingFlags.Instance);
                    lazyValuefield.SetValue(lazyInstance, Convert.ChangeType(value, lazyValuefield.FieldType));

                    field.SetValue(instance, Convert.ChangeType(lazyInstance, lazyValuefield.FieldType));
                }

                field.SetValue(instance, Convert.ChangeType(value, field.FieldType));
            }
        }
    }

    private static string PropertyToField(string propertyName)
    {
        return "_" + Char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1);
    }
}

The first problem I came across attempting to do this, is that I was unable to create a delegate at runtime of an unknown type, so I tried to get around that by trying to set the internal values of the Lazy<T> instead.

After setting the internal values of the lazy, I could see it was indeed set. However the problem I ran into doing that, was that I found out the internal field of a Lazy<T> is not a <T>, but actually a Lazy<T>.Boxed. Lazy<T>.Boxed is an internal class of lazy, so I'd have to instantiate that somehow...

I realized that maybe I'm approaching this problem from the wrong direction, since the solution is getting exponentially more complex, and I doubt many people will understand the weird metaprogramming of the 'ReflectionHelper'.

What would be the best approach in solving this? Can I solve this in the ReflectionHelper or will I have to go through every unittest and modify those?

Edit after getting the answer

I got a answer from dasblinkenlight to make SetProperty generic. I changed to code, and this is the end result, in case someone else needs it

The solution

public static class ReflectionHelper
{
    public static void SetProperty<T>(object instance, string properyName, T value)
    {
        var type = instance.GetType();

        var propertyInfo = type.GetProperty(properyName);
        var accessors = propertyInfo.GetAccessors(true);

        // There is a setter, lets use that
        if (accessors.Any(x => x.Name.StartsWith("set_")))
        {
            propertyInfo.SetValue(instance, Convert.ChangeType(value, propertyInfo.PropertyType), null);
        }
        else
        {
            // Try to find the backing field
            var fieldName = PropertyToField(properyName);
            var fieldInfo = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);

            // Cant find a field
            if (fieldInfo == null)
            {
                throw new ArgumentException("Cannot find anything to set.");
            }

            // Its a normal backing field
            if (fieldInfo.FieldType == typeof(T))
            {
                throw new NotImplementedException();
            } 
            
            // if its a field of type lazy
            if (fieldInfo.FieldType == typeof(Lazy<T>))
            {
                var lazyValue = new Lazy<T>(() => value);
                fieldInfo.SetValue(instance, lazyValue);
            }
            else
            {
                throw new NotImplementedException();
            }
        }
    }

    private static string PropertyToField(string propertyName)
    {
        return "_" + Char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1);
    }
}

Breaking changes of this

Setting variables to null no longer work without explicitly giving it a type.

ReflectionHelper.SetProperty(instance, "parameter", null);

has to become

ReflectionHelper.SetProperty<object>(instance, "parameter", null);
like image 288
Ron Sijm Avatar asked Mar 13 '13 11:03

Ron Sijm


1 Answers

Try making SetProperty a generic method:

public static void SetProperty<T>(object instance, string properyName, T value)

This should let you capture the type of value. With T in place, you could construct Lazy<T> object in the regular C# syntax, rather than going through reflection:

...
Lazy<T> lazyValue = new Lazy<T>(() => value);
...

Now you can write the lazyValue into the property/field with the setValue call.

This should be sufficient for many, if not all, of your unit tests.

like image 114
Sergey Kalinichenko Avatar answered Oct 10 '22 17:10

Sergey Kalinichenko