简体   繁体   English

如何在C#中使用反射设置私有惰性<T>以进行测试?

[英]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. 对于使用测试特定方案,我曾经使用私有setter在这些属性中写入数据。

However, because the system was getting slow, and was loading unnessesary things, we changed certain things to lazy loading, using the Lazy class. 但是,由于系统变慢,并且正在加载无关紧要的东西,我们使用Lazy类将某些内容更改为延迟加载。 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. 上面示例中使用的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);
    }
}

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 因为我们不想更改所有测试的代码,所以最好的选择似乎是改变中间人: ReflectionHelper

This is the current state of 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. 我遇到的第一个问题是,我无法在运行时创建未知类型的委托,所以我尝试通过尝试设置Lazy<T>的内部值来解决这个问题。

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>的内部字段不是<T> ,而实际上是Lazy<T>.Boxed Lazy<T>.Boxed is an internal class of lazy, so I'd have to instantiate that somehow... Lazy<T>.Boxed是一个懒惰的内部类,所以我必须以某种方式实例化...

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'. 我意识到也许我正在从错误的方向接近这个问题,因为解决方案变得越来越复杂,我怀疑很多人会理解'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? 我可以在ReflectionHelper解决这个问题,还是我必须通过每个单元测试并修改它们?

Edit after getting the answer 得到答案后编辑

I got a answer from dasblinkenlight to make SetProperty generic. 我从dasblinkenlight得到了一个答案,使SetProperty变得通用。 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. 将变量设置为null不再有效而不显式赋予它类型。

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

has to become 必须成为

ReflectionHelper.SetProperty<object>(instance, "parameter", null);

Try making SetProperty a generic method: 尝试使SetProperty成为通用方法:

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

This should let you capture the type of value . 这应该可以捕获value的类型。 With T in place, you could construct Lazy<T> object in the regular C# syntax, rather than going through reflection: 使用T ,您可以在常规C#语法中构造Lazy<T>对象,而不是通过反射:

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

Now you can write the lazyValue into the property/field with the setValue call. 现在,您可以使用setValue调用将lazyValue写入属性/字段。

This should be sufficient for many, if not all, of your unit tests. 对于许多(如果不是全部)单元测试,这应该足够了。

To make your classes unit-testable, and to promote the separation of concerns, consider using Dependency Injection: 要使您的类可单元测试,并促进关注点分离,请考虑使用依赖注入:

What you should have: 你应该拥有什么:

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;
    }
}

What the test should look like: 测试应该是什么样子:

[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());
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM