Gianluca Ghettini
Gianluca Ghettini

Reputation: 11658

Fill a class with test data

I'm using reflection to fill a given C# class with test data

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

The GetRnd() function set the value according to the type of the field:

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

}

This works as long as I pass to CreateObj() a proper class. I'd like to make it work even with basic types (string, int and so on..) By now I get a "SetType(): Cannot set a constant field" exception when I pass a simple "int".

Upvotes: 4

Views: 3337

Answers (3)

Harald Coppoolse
Harald Coppoolse

Reputation: 30512

First of all: for a unit test it is not advised to create test objects with random values.

The purpose of your unit test is to find errors in the class you are testing. Errors in the current version and in future versions. Your unit class is not created to find the errors that occur when using normal input. Those errors are already found the first time you run the program after the code changes. The main purpose is to find errors that you wouldn't find when running the program normally.

Your unit test would only give confidence in your code if it will find errors in rare cases.

Usually they are called edge conditions: the lowest value, the highest value, an empty value, the maximum number of values, negative values, null values, etc.

Creating a class filled with random values is putting a lot of effort in creating code that creates a test object that will probably find the same errors as any non-edge test object that you can invent within a minute. You would have to run your test a million times before you test your code with an empty array, or a negative number of elements, or an even primary number, etc.

Therefore don't put any effort in creating a unit test with random value input.

Only have a few cases with normal input and try to find a lot of tests with edge conditions.

However, creating a factory that will create any class of any type filled with random values might be an interesting exercise.

First of all you should only initialize the properties that are writable, therefore you should check if they are. Use System.PropertyInfo.IsWritable.

Besides, extend your GetRnd function such that it will initialize any primitive types Use System.Type.IsPrimitive and System.Type.GetTypeCode()

Your class will fail if one of your writable properties is a class. In that case you can recursively initialize that class. Use System.Type.IsClass

Do you also want to initialize classes with non-default constructors? Use System.Type.GetConstructors to see if it has and to check what other constructors are available.

How about an array?

Extra difficulty: if your class has a read-only property where you can change the returned values.

class RandomObjectFactory
{
    // creates an object of type T that has a default constructor
    public T CreateObject<T>() where T: class, new()
    {
        ...

This method is preferred above the method with a Type parameter, because with this method the compiler complains if you try to write:

MyClass m = CreateObject<YourClass>();

Besides the compiler would complain if MyClass didn't have a default constructor.

Similarly it is wise to create a method for primitive types:

public T CreatePrimitive<T> where T: struct, IConvertible
{
    ...

This would prevent errors like:

int i = Create(typeof(Form));

The code to create the object:

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...

Here we have problems: we can't pass property.PropertyType.

To solve this: make a private function CreateObject that takes a system.Type and returns an object. Because this one is private, no one can make erroneous use of it.

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

Invent something similar to create a struct or an array.

Upvotes: 2

D Stanley
D Stanley

Reputation: 152624

You didn't ask a question, but here's what I think you want to know:

How can I change this code to set constant or read-only fields?

You can't. They are read-only.

How can I get this to work with int and other types

Well, You can prevent errors by checking for read-only or constant fields:

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

Can I use this class to generate a random integer or string?

Not by changing it's fields. Primitive types like int and string are immutable. The only way you can "change" a variable's value is to overwrite it with a new value. So you'll need to handle cases for immutable types:

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

How else can this be improved?

  • It only sets fields, not properties. If you have a class with properties that wrap private fields then they would not be set
  • It won't work for types that don't have a public parameterless constructor

Upvotes: -1

Mickael Thumerel
Mickael Thumerel

Reputation: 536

Check on the type if it is a value 'Type.IsValue' that means that the type pass in parameter is a scalar type.

Upvotes: 1

Related Questions