用测试数据填充一个类

4

我正在使用反射机制为给定的C#类填充测试数据

public object CreateObj(Type type)
{
    var obj = Activator.CreateInstance(type);
    var fields = type.GetFields();
    foreach (var field in fields)
    {
        field.SetValue(obj, GetRnd(field.FieldType));
    }

    return obj;
}
< p> GetRnd() 函数根据字段类型设置值:

private object GetRnd(Type type)
{
    if (type == typeof(int))
    {
        return 4;
    }
    else if (type == typeof(string))
    {
        return "text";
    }
    else
    {
        throw new Exception();
    }

}

只要我向CreateObj()传递一个正确的类,这就可以工作。我希望它能够处理基本类型(如字符串、整数等)。目前,当我传递一个简单的“int”时,我会收到一个“SetType(): Cannot set a constant field”异常。

1
看一下AutoFixture。它可以提供随机测试数据和更多功能。(不是我的踩,事实上这是一个有效的问题) - Sriram Sakthivel
2
@SriramSakthivel - AutoFixture非常好用,我经常使用它。 - Ric
我会选择 AutoFac。它是专门为解决这个问题而设计的。它是开源的,你也可以从 nuget 上下载它。如果你想自己解决,那么你将会写很多代码,但却得不到任何回报 ;) - Sriram Sakthivel
我只需要针对由整型和字符串组成的简单结构体/类。为了处理复杂的类,我可以在setField上递归调用CreateObj(),并在基本类型处停止(即最底层)。 - Gianluca Ghettini
1
基本上 AutoFixture 的作用是什么。 - Ric
显示剩余3条评论
3个回答

2
首先,不建议使用随机值创建测试对象进行单元测试。
您的单元测试的目的是查找正在测试的类中的错误。当前版本和未来版本中的错误。您的单元类不是为了查找在使用正常输入时发生的错误。这些错误在代码更改后第一次运行程序时就已经发现了。主要目的是查找在正常运行程序时无法发现的错误。
仅当单元测试在罕见情况下发现错误时,才能对代码产生信心。通常称为边界条件:最低值、最高值、空值、最大数量的值、负值、null 值等。
创建一个充满随机值的类会花费很多精力,而这个测试对象可能会找到与您在一分钟内可以发明的任何非边缘测试对象相同的错误。您必须运行测试一百万次才能使用空数组、负数元素或偶数质数等测试代码。
因此,请不要花费任何精力用随机值输入创建单元测试。
只有少数几种情况下使用正常输入,并尝试找到许多带有边界条件的测试。
但是,创建一个工厂,它将创建任何类型的任何类并填充随机值,可能是一个有趣的练习。
首先,您应该仅初始化可写的属性,因此应检查它们是否可写。使用 System.PropertyInfo.IsWritable。
此外,扩展您的 GetRnd 函数,使其初始化任何基元类型。使用 System.Type.IsPrimitive 和 System.Type.GetTypeCode()。
如果您的可写属性之一是类,则您的类将失败。在这种情况下,可以递归初始化该类。使用 System.Type.IsClass
您还想初始化具有非默认构造函数的类吗?使用 System.Type.GetConstructors 来查看是否有其他可用的构造函数。
如何处理数组?
额外的难度:如果您的类具有只读属性,您可以更改返回的值。
class RandomObjectFactory
{
    // creates an object of type T that has a default constructor
    public T CreateObject<T>() where T: class, new()
    {
        ...

这种方法比带有类型参数的方法更受欢迎,因为使用这种方法时,如果您尝试编写以下内容,编译器会发出警告:

MyClass m = CreateObject<YourClass>();

除此之外,如果MyClass没有默认构造函数,编译器会报错。
同样地,为基本类型创建一个方法是明智的:
public T CreatePrimitive<T> where T: struct, IConvertible
{
    ...

这将避免像以下这样的错误:

(译注:此处原文已翻译,以下为原文)

int i = Create(typeof(Form));

创建对象的代码:
public T CreateObject<T>() where T: class, new()
{
    var obj = Activator.CreateInstance<T>();
    foreach (var property in typeof(T).GetProperties()
        .Where(property => property.CanWrite))
    {
        if (property.PropertyType.IsPrimitive)
        {
            property.SetValue(obj, this.CreatePrimitive 
               (Type.GetTypeCode(property.PropertyType)));
        }
        else if (property.PropertyType.IsClass)
        {
             property.SetValue(obj, this.CreatObject...

这里出现问题了:我们无法传递property.PropertyType。

解决方法是:创建一个私有函数CreateObject,该函数接受一个system.Type并返回一个对象。因为这个函数是私有的,所以没有人会错误地使用它。

private object CreateObject(Type type)
{
    var obj = Activator.CreateInstance(type);
    foreach (var property in typeof(T).GetProperties()
        .Where(property => property.CanWrite))
    {
        if (property.PropertyType.IsPrimitive)
        {
            property.SetValue(obj, this.CreatePrimitive 
               (Type.GetTypeCode(property.PropertyType)));
        }
        else if (property.PropertyType.IsClass)
        {
             property.SetValue(obj, this.CreateObject (property.PropertyType); 
        }
    }
    return obj;
}

private object CreatePrimitive(TypeCode typeCode)
{
    switch (typeCode)
    {
        case TypeCode:Boolean:
            return this.rnd.Next(2) == 0;
        case TypeCode.Byte:
            return (Byte)this.rnd.Next(Byte.MinValue, Byte.MaxValue);
        case TypeCode.DateTime:
            long ticks = (long)((DateTime.MaxValue.Ticks - DateTime.MinValue.Ticks) * rnd.NextDouble() + DateTime.MinValue.Ticks);
            return new DateTime(ticks);
         // etc.
    }
    return obj;
}

创建一个类似的东西来创建结构体或数组。

如果我问“如何做某事”,请不要回答“你应该这样做,而不是那样做”。我并不是在寻求更好的解决方案(这总是存在的),只是想知道如何在给定环境和给定糟糕的东西上解决一个即时问题。此行为被标记为不当。 - Gianluca Ghettini
我们的准则:粗鲁和贬低性语言是不可接受的。你的语气应该与你尊重并希望得到尊重的人面对面交谈时一致。如果你没有时间礼貌地说话,就把它留给有时间的人。 - Harald Coppoolse

1
检查类型是否为值“Type.IsValue”,这意味着传入参数的类型是标量类型。

-1

您没有提出问题,但这是我认为您想知道的:

如何更改此代码以设置常量或只读字段?

您不能。它们是只读的。

如何使其与int和其他类型一起工作?

好的,您可以通过检查只读或常量字段来防止错误:

foreach (var field in fields)
{
    if (!(field.IsInitOnly || field.IsLiteral))
        field.SetValue(obj, GetRnd(field.FieldType));
}

我可以使用这个类来生成随机整数或字符串吗?
不通过更改它的字段。像int和string这样的原始类型是不可变的。你可以“改变”一个变量的值的唯一方法是用新值覆盖它。因此,您需要处理不可变类型的情况:
static Random rand = new Random();

public object CreateObj(Type type)
{

    if(type == typeof(int)) return GetRnd(type);
    if(type == typeof(string)) return GetRnd(type);

    var obj = Activator.CreateInstance(type);
    var fields = type.GetFields();
    foreach (var field in fields)
    {
        field.SetValue(obj, GetRnd(field.FieldType));
    }

    return obj;
}

这个还有什么可以改进的吗?
  • 它只设置字段,而不是属性。如果你有一个包装私有字段的属性类,那么它们将无法被设置。
  • 它不能用于没有公共无参构造函数的类型。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接