Monday, November 26, 2007

C# unit testing helper

As I am writing some unit tests for a C# application I found myself filling the attributes of the objects with meaningful values. With objects having 26 attributes this can be a very frustrating task and especially if you have to do it twice because you want to test the "add" and the "update" method. The other problem that I had was that I need to compare the values of all attributes of two "user" objects which again is a long task with many attributes. I decided therefore to take advantage of Reflection and letting the computer to do this nasty work.

Possible Solution:
I created a class called "TestHelper" which contains static methods that do exactly this work for me :-). One method fills the public, non inherited properties with random values; except for properties which name ends in "id" and properties which name is "pk". This behaviour is adjusted to my needs but can easyily be changed. The other method takes two objects as input and compares each value of their properties if the objects are of the same type; it returns true if the two objects are "equal". For the compare operations I took some code from Steve Lautenschlager (his code can be found here) and Jonas John (his code can be found here). I needed to mix this two versions because in Johns "RandomHelper" there was missing a method to generate random DateTime objects. The random fill method fills the properties according to their type as follows:
  • DateTime: it puts a random date between now and 3000-01-01
  • String: it puts the name of the property plus a random string of uppercase letters of lenght 50
  • Int, long, Int32, Int64: if they are not ids or pks a random number between 0 and 999999 is set
  • Bool: a random boolean value is created
This behaviour can be easily changed to match your specific needs!

The compare method has some small disadvantage when working with DateTime objects and databases. My problem was that if in the database there was only Time datatype and it was then stored inside a DateTime object it has differed in the last 5 or 6 numbers of the Ticks number. This forced me to make this additional check but it can easily be removed and adopted to other needs.

Here is the code of the TestHelper and the RandomHelper classes:
public class TestHelper
{
/* This method can be used to fill all public properties of an object with random values depending on their type.
CAUTION: it does not fill attributes that end with 'ID' or attributes which are called 'pk'. They have to be filled manually.*/
public static object FillAttributesWithRandomValues(object obj)
{
Type type = obj.GetType();
PropertyInfo[] infos = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);

foreach (PropertyInfo info in infos)
{
Type infoType = info.PropertyType;
if (infoType.Equals(typeof(DateTime)))
info.SetValue(obj, RandomHelper.RandomDateTime(DateTime.Now, new DateTime(3000, 01, 01)), null);
else if (infoType.Equals(typeof(String)))
info.SetValue(obj, info.Name + " " + RandomHelper.RandomString(50, false), null);
else if ((infoType.Equals(typeof(long)) || infoType.Equals(typeof(Int32)) || infoType.Equals(typeof(Int64)) || infoType.Equals(typeof(int))) && !info.Name.ToLower().EndsWith("id") && !info.Name.ToLower().Equals("pk"))
info.SetValue(obj, RandomHelper.RandomNumber(0, 999999), null);
else if (infoType.Equals(typeof(bool)))
info.SetValue(obj, RandomHelper.RandomBool(), null);
else if (infoType.Equals(typeof(Color)))
info.SetValue(obj, RandomHelper.RandomColor(), null);
}
return obj;
}

/* This method takes as input two objects and compares the properties of them.
It returns true if all the public properties of the objects (not inherited ones) are equal.*/
public static bool CompareObjectAttributes(object firstObject, object secondObject)
{
Type t1 = firstObject.GetType();
Type t2 = secondObject.GetType();
/*the two objects must have the same type*/
if (!t1.Equals(t2)) return false;

PropertyInfo[] infos1 =
t1.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
PropertyInfo[] infos2 =
t2.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
for (int i = 0; i < infos1.Length; i++)
{
/*this if is needed because if it is a datetime it should compare only the date and the time and not the ticks*/
if (infos1[i].PropertyType.Equals(typeof(DateTime)))
{
DateTime firstDate = (DateTime)infos1[i].GetValue(firstObject, null);
DateTime secondDate = (DateTime)infos2[i].GetValue(secondObject, null);
/*if the datatype in the database was date then only compare the date part of the datetime object*/
if (firstDate.ToString().Contains("00:00:00") || secondDate.ToString().Contains("00:00:00"))
{
if (!firstDate.Date.ToString().Equals(secondDate.Date.ToString())) return false;
}
/*otherwise compare the string representation of the two datetime objects because the ticks may differ*/
else
{
if (!firstDate.ToString().Equals(secondDate.ToString()))
return false;
}
}
else
{
/*if one property value differs return false*/
if (!(infos1[i].GetValue(firstObject, null)).Equals(infos2[i].GetValue(secondObject, null)))
return false;
}
}

/*when everything went fine the objects have the same values for their properties*/
return true;
}


/* Helper class for generating random values*/
public static class RandomHelper
{
private static Random randomSeed = new Random();

/* Generates a random string with the given length*/
public static string RandomString(int size, bool lowerCase)
{
/* StringBuilder is faster than using strings (+=)*/
StringBuilder RandStr = new StringBuilder(size);

/* Ascii start position (65 = A / 97 = a)*/
int Start = (lowerCase) ? 97 : 65;

/* Add random chars*/
for (int i = 0; i < size; i++)
RandStr.Append((char)(26 * randomSeed.NextDouble() + Start));

return RandStr.ToString();
}

/* Returns a random number.*/
public static int RandomNumber(int Minimal, int Maximal)
{
return randomSeed.Next(Minimal, Maximal);
}

/* Returns a random boolean value*/
public static bool RandomBool()
{
return (randomSeed.NextDouble() > 0.5);
}

/* Returns a random color*/
public static System.Drawing.Color RandomColor()
{
return System.Drawing.Color.FromArgb(
randomSeed.Next(256),
randomSeed.Next(256),
randomSeed.Next(256)
);
}

/* Returns DateTime in the range [min, max)*/
public static DateTime RandomDateTime(DateTime min, DateTime max)
{
if (max <= min)
{
string message = "Max must be greater than min.";
throw new ArgumentException(message);
}
long minTicks = min.Ticks;
long maxTicks = max.Ticks;
double rn = (Convert.ToDouble(maxTicks)
- Convert.ToDouble(minTicks)) * randomSeed.NextDouble()
+ Convert.ToDouble(minTicks);
return new DateTime(Convert.ToInt64(rn));
}
}
}


UPDATE:
This is some very simple code on how to use the UnitTestHelper. It is a very simple Console application. To use it in your unit tests adapt it to Your needs since the creation of Unit Tests is not the topic of this post.
class Program
{
static void Main(string[] args)
{
Person person = new Person();
TestHelper.FillAttributesWithRandomValues(person);

Console.WriteLine("Object with random values:");
Console.WriteLine(string.Format("name = {0}", person.Name));
Console.WriteLine(string.Format("age = {0}", person.Age));
Console.WriteLine(string.Format("tshirtcolor = {0}", person.TShirtColor));
Console.WriteLine(string.Format("birthdate = {0}", person.BirthDate));

Person p1 = new Person
{
Name = "John",
Age = 30,
BirthDate = new DateTime(2000, 10, 19),
TShirtColor = Color.Blue
};

Person p2 = new Person
{
Name = "John",
Age = 30,
BirthDate = new DateTime(2000, 10, 19),
TShirtColor = Color.Blue
};

Person p3 = new Person
{
Name = "Obama",
Age = 30,
BirthDate = new DateTime(2000, 10, 19),
TShirtColor = Color.Blue
};

Console.WriteLine("\nPerson 1 equals Person 2 = " + TestHelper.CompareObjectAttributes(p1, p2));
Console.WriteLine("\nPerson 2 equals Person 3 = " + TestHelper.CompareObjectAttributes(p2, p3));
Console.ReadLine();
}
}


This prints out the following (note that the values may differ on your machine since it are random values):
Object with random values:
name = Name BLHTVGYVHYZVQUJTIPXAWHKMCQLGGYHZMGEBZCXGBDYJCZBMGR
age = 32895
tshirtcolor = Color [A=255, R=32, G=148, B=35]
birthdate = 17.07.2065 15:52:02

Person 1 equals Person 2 = True

Person 2 equals Person 3 = False

4 comments:

Kiquenet said...

Please, any sample code of TestHelper ???

thanks !!!

Manfred said...

Hello Kiquenet,
I posted now some sample code. Copy it to a console application and run it...

www.oracledba.in said...

i like this

Anonymous said...

I am using the newest TV on my desktop at home and my laptop, both with xp. All has been fine for a couple of weeks until this morning. I accessed the desktop fine for about a minute then TV on my end, the laptop, abruptly closed. Now when I try to connect I get Protocol Negotiation Failed error.