问题描述
我们有一个相当大的系统,过去常常用私有setter将数据加载到属性中。为了使用测试特定的场景,我曾经使用私有setter在这些属性中写入数据。
然而,由于系统变得越来越慢,并且正在加载不必要的东西,我们使用lazy类将某些东西更改为延迟加载。但是,现在我不能再向这些属性中写入数据,因此许多单元测试将不再运行。
我们以前有
测试对象:
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());
}
上面示例中使用的ReflectionHelper的实现。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;
}
}
尝试解决
现在请记住,这只是一个样本。代码从急于加载到惰性加载的变化发生在很多不同的地方。因为我们不想更改所有测试的代码,所以最好的选择似乎是更改中间人:ReflectionHelper
这是ReflectionHelper
的当前状态
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;
}
}
ReflectionHelper
顺便说一句,我想提前为这段奇怪的代码道歉
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);
}
}
我遇到的第一个问题是,我无法在运行时创建一个未知类型的委托,所以我试图通过设置Lazy<T>
的内部值来解决这个问题。
设置lazy的内部值后,我可以看到它确实被设置了。但是我遇到的问题是,我发现Lazy<T>
的内部字段不是<T>
,而是Lazy<T>.Boxed
。Lazy<T>.Boxed
是lazy的内部类,所以我必须以某种方式实例化它…
我意识到也许我从错误的方向接近这个问题,因为解决方案正变得指数级复杂,我怀疑很多人会理解'ReflectionHelper'的奇怪的元编程。
解决这个问题的最佳方法是什么?我能在ReflectionHelper
中解决这个问题吗?还是我必须遍历每个单元测试并修改它们?
得到答案后编辑
我从dasblinkenlight得到了一个答案,使SetProperty通用。我更改为代码,这是最终结果,以防其他人需要它
<标题> 的解决方案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);
}
}
打破这个
将变量设置为null不再工作,除非显式地给它一个类型。
ReflectionHelper.SetProperty(instance, "parameter", null);
必须变成
ReflectionHelper.SetProperty<object>(instance, "parameter", null);
标题>尝试将SetProperty
设置为泛型方法:
public static void SetProperty<T>(object instance, string properyName, T value)
这会让您捕获value
的类型。有了T
,您就可以用常规c#语法构造Lazy<T>
对象,而不是通过反射:
...
Lazy<T> lazyValue = new Lazy<T>(() => value);
...
现在你可以用setValue
调用将lazyValue
写入属性/字段。
这对于许多单元测试来说应该足够了,如果不是全部的话。
为了使你的类是可单元测试的,并且为了促进关注点的分离,考虑使用依赖注入:
你应该拥有什么:
public class ComplexClass
{
private readonly Lazy<DateTime> _date;
public DateTime Date
{
get
{
return _date.Value;
}
}
public ComplexClass(Lazy<DateTime> date)
{
// Allow your DI framework to determine where dates come from.
// This separates the concern of date resolution from this class,
// whose responsibility is mostly around determining information
// based on this date.
_date = date;
}
public string GetDay()
{
if (Date.Day == 1 && Date.Month == 1)
{
return "New year!";
}
return string.Empty;
}
}
测试应该是什么样的:
[Test]
public void TestNewyear()
{
var newYear = new DateTime(2014, 1, 1);
var complexClass = new ComplexClass(new Lazy<DateTime>(() => newYear));
Assert.AreEqual("New year!", complexClass.GetDay());
}