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.
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;
}
}
[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());
}
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);
}
}
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;
}
}
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
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?
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
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);
}
}
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);
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With