12

问题描述

我们有一个相当大的系统,它曾经使用私有设置器将数据急切地加载到属性中。为了使用测试特定场景,我曾经使用私有设置器在这些属性中写入数据。

但是,由于系统变得越来越慢,并且正在加载不必要的东西,我们将某些东西更改为延迟加载,使用 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();

        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的内部值之后,我可以看到它确实被设置了。但是我遇到的问题是,我发现 a 的内部字段Lazy<T>不是 a <T>,而是实际上 a Lazy<T>.BoxedLazy<T>.Boxed是一个懒惰的内部类,所以我必须以某种方式实例化它......

我意识到也许我从错误的方向来解决这个问题,因为解决方案变得越来越复杂,我怀疑很多人会理解“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);
4

2 回答 2

4

尝试制作SetProperty一个通用方法:

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

这应该让您捕获value. 使用T到位后,您可以Lazy<T>使用常规 C# 语法构造对象,而不是通过反射:

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

现在您可以通过调用将其写入lazyValue属性/字段setValue

对于许多(如果不是全部)单元测试来说,这应该足够了。

于 2013-03-13T11:28:56.630 回答
0

为了使您的类可单元测试,并促进关注点分离,请考虑使用依赖注入:

你应该拥有的:

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());
}
于 2013-03-13T18:10:34.197 回答