Visual Studio .NET 2005 - Unit Tests

By Robbe D. Morris

Printer Friendly Version

Robbe Morris
Robbe & Melisa Morris
Unit tests... The necessary evil.  I'm not a huge fan of Test Driven Development because it really doesn't work well unless you have a huge percentage of the application requirements dictated to you early on in the process.  I rarely have that luxury.  To tell you the truth, I've also never been a huge fan of writing unit tests at all largely because the expectation of others is that the unit test for a method should be stand alone or autonomous from other methods.  What we wind up with is a bunch of test code that does a half ass job of testing our code in isolation rather than accurately testing whether the application or section of the application works.  This is very much like testing whether a drug will cure my allergies without bothering to test if it will also kill me in the process.
Writing useful unit tests takes a lot of time and a lot of thought.  Not only do you need to write tests to see if it works as expected, you need to write related tests to see if it fails properly when called improperly or improper data is passed in.  You shouldn't expect a unit test to catch everything.  However, it should catch most things.  These tests serve as a great way to perform a large scale mass test of your application prior to deploying each new release to quality assurance testing.
I've been working on a fairly indepth research application framework recently and finally decided that serious consideration to writing more effective unit tests in a shorter period of time was long overdue.  So, I've thrown together a few ideas that seem to work relatively well and opted to share them with you here.


1. Perform these preparation steps prior to writing your first unit test.
a. Load at least one test record in every single table in the database.  Make sure that each record is populated in such a way to guarantee an accurate test sample of a properly populated record.
b. Where applicable, create a test only table that stores keys of records you don't want altered or deleted during testing.
c. Write a stored procedure that will delete all records not identified in b.  This will get called at the beginning of the test cycle and optionally at the end.  Manually verify this works properly.
d. Create a reusable class for copying class properties from one class to another regardless of whether the two classes are of the same type.  See code sample using reflection below.  Remember, we aren't terribly concerned about the speed performance of these types of tests.  You'll find this to be a bit of a time saver when moking up and altering test data for similar classes.
e. Use custom attributes on your class properties of data object classes (see the ADO.NET Code Generator link below for a sample) to flag whether the database will accept null values for that particular property.  This will enable the class to optionally self validate itself (see ADO.NET Code Generator CustomAttributes.cs class for a code sample) versus having to write test code or business layer validation code for each and every class.  This can be a huge time saver.
f. I use custom attributes to dynamically populate class properties from DataTables at runtime.  So, I never hard code class property = DataTable.Column value.  If the column was selected in the stored procedue, it will get loaded to the class.  My ADO.NET code generator will show you how to do this.
g. Create a separate solution that has references to all necessary assemblies and your test project.  Adding a test project to your normal development solution will create issues during actual development along with slowing up compilation.
h. In your test project, create a class with static variables for every applicable primary key for applicable tables.  Also create static variables for keys of existing test records.  You can optionally load these pre-existing keys from the config file.  In step 3 below, we'll query these existing records to start with.

2. Determine/change the order of unit tests being called.

a. Tests within the same test class are called in the order in which they exist in the code.  Visual Studio .NET 2005 does a decent job of adding new tests when they are created below existing test.  However, if you copy and paste code above another method, Visual Studio .NET 2005 does not automically update the test order.  You have to either unload and reload the project or close and reopen Visual Studio.
b. Test classes are run in alphabetical order by classname.  So, name your test class files accordingly.  I've found using a class that starts with a letter and then a number of which test class I want called first works out ok (i.e. a0001MyClass.cs runs before a0002Class2.cs).
c. Create a test class called aaStartUp.cs and name it (as mentioned above) to show up first in the Test View.  Put code you want run (like a stored procedure to clean up from previous tests) at the start of the entire test process in the MyClassInitialize event.
d. Create a test class called zzEndTests.cs and name it (as mentioned above) to show up last in the Test View.  Put code you want run (like a stored procedure to clean up from previous tests) at the end of the entire test process in the MyClassInitialize event.  Steps c and d allow you to more easily optionally run pre and post test clean up methods.
Note to Bill and Steve(and their developers of course), my needing to manage my file names in steps c and d like this isn't "microsoft innovation".

3. Perform these test steps to confirm that your code to move data in and out of the database works if called properly.

a. Retrieve an existing test record.  Confirm it is successful and thus unit test the retrieval process by running the class self validation process.  This won't confirm 100% that every column you wanted selected was but it will definitely get those that aren't nullable.  During the testing phase, you may want to consider going against best practices in your stored procedures and use select * from tablename instead of manually typing out the columns.  Just be sure that you go back and specify the columns prior to release and rerun your tests.  You are the best judge of whether this is prudent when testing your application.
b. Use 1.d to copy the properties of that class to a brand new instance of that same class.  Alter a few properties and along with any property key values needed to trigger an insert.
c. Create another instance of your class by running a query to retrieve the record you've just inserted.  Compare the two classes.  If they are the same, you've validated your insert test.
d. Alter the properties of the newly retrieved instance and save it to the database initiating a database update.  Retrieve that record from the database again.  Compare it to the previous instance of the class.  If they are different, then your database update unit test has been confirmed as successful.
e. If applicable, process the Delete method for the class and then attempt to retrieve that record.  If it can't be retrieved, you've confirmed your delete has worked.  If the delete is supposed to cascade across multiple tables, you'll want to query each of these tables to make sure ALL of the corresponding records were actually deleted.
f. Repeat steps a through c.  Upon completion set the static variable associated with this new record's key for use with other unit tests that might depend on this record's existance.  This is one of many ways for you to run unit tests that are dependent upon the existence of test records from other tables.
I've found that if you bend the naming convention rules in your test classes to use more generic variable names, you can almost template steps a through e and copy/paste and then just modify class names.  This is especially true if your naming convention for common business layer methods is similar.  So, while these steps seem like a lot of work, you are only doing it for one class not 50.  Be careful you don't bend the rules so much that supporting the code in your unit tests becomes difficult for someone with an IQ above remove temperature.
I think if you review what has been done in steps a through e, you can have a high degree of confidence that between your quality coding techniques, a solid set of real world unit tests, and other developers programming against your code, you've drastically reduced the likelihood of data access bugs even making it to quality assurance testing.

4. Write Failure Tests For Data Access

a. I'd focus largely on database manipulation methods.  Failure to isolate problems here create big problems as the development cycle nears your first production build.  While database retrieval queries can also create problems, they are typically much easier to resolve and be identified by user interface developers.  If you've use a something like the code generator mentioned above, you should be able to limit your vulnerability to data retrieval failures.
b. Purposely fail to populate any required field.  Then, run your record Save test.  If it does not save the record, then have your test Assert.IsTrue(true) because this test SHOULD fail and you don't want your unit test output to reflect a fail in this case.

5. Repeat Steps 3 and 4 For Business Logic Only Tests

a. Now, that you almost entirely ruled out database access/manipulations as potential failure points for your unit tests, proceed with writing unit tests that process business rules not necessarily associated with database access.
It is common practice for you to write your own test code (assuming you don't have a group of designated test code writers).  You may want to consider having your UI developers write many of these tests.  You already know what you think should and shouldn't work.  These folks will tend to write more elaborate tests to see just how much validation in the UI layer needs to be written as well as what will your business/database layer communicate back to them on success or failure.  You may both decide to make changes early on that can drastically improve your productivity.
 
Below is some of the sample code for managing properties in your unit tests:
 
PropertyHandler.cs
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Diagnostics;

namespace MyApplication
{
    public class PropertyHandler
    {
        #region Set Properties
        public static void SetProperties(PropertyInfo[] fromFields,
                                         PropertyInfo[] toFields,
                                         object fromRecord,
                                         object toRecord)
        {
            PropertyInfo fromField = null;
            PropertyInfo toField = null;

            try
            {

                if (fromFields == null)
                {
                   return;
                }
                if (toFields == null)
                {
                   return;
                }

                for (int f = 0; f < fromFields.Length; f++)
                {

                    fromField = (PropertyInfo)fromFields[f];

                    for (int t = 0; t < toFields.Length; t++)
                    {

                        toField = (PropertyInfo)toFields[t];

                        if (fromField.Name != toField.Name)
                        {
                            continue;
                        }

                        toField.SetValue(toRecord,
                                         fromField.GetValue(fromRecord, null),
                                         null);
                        break;

                    }

                }
 
            }
            catch (Exception)
            {
                throw;
            }
        }
        #endregion

        #region Set Properties
        public static void SetProperties(PropertyInfo[] fromFields,
                                         object fromRecord,
                                         object toRecord)
        {
            PropertyInfo fromField = null;
         
            try
            {

                if (fromFields == null)
                {
                    return;
                }

                for (int f = 0; f < fromFields.Length; f++)
                {

                    fromField = (PropertyInfo)fromFields[f];

                    fromField.SetValue(toRecord,
                                       fromField.GetValue(fromRecord, null),
                                       null);
                }

            }
            catch (Exception)
            {
                throw;
            }
        }
        #endregion
  
        
    }
} 
PropertyHandler Sample For Identical Classes

MyClass record = new MyClass();
MyClass newRecord = new MyClass();
PropertyInfo[] fromFields = null;

fromFields = typeof(MyClass).GetProperties();

PropertyHandler.SetProperties(fromFields, record, newRecord);
 
PropertyHandler Sample For Similar Classes
 
MyClass record = new MyClass();
MyOtherClass newRecord = new MyOtherClass();
PropertyInfo[] fromFields = null;
PropertyInfo[] toFields = null;

fromFields = typeof(MyClass).GetProperties();
toFields = typeof(MyOtherClass).GetProperties();

PropertyHandler.SetProperties(fromFields,toFields,record, newRecord);
 
Self Validation Methods

// Some of the code below relies on runtime reflection.  Certain aspects
// of reflection are detrimental to performance.  Where possible, you
// can create static instances of the reflection results.  It will give
// the power without the overhead of using reflection.

 // Here is a sample business class layer self validation method.

 
 private bool ValidateSave(CellAlignment record)
 {
 
   object[,] properties = null;
   List<string> returnMessages = new List<string>();

   try
   {

     // Use the .GetFields() method mentioned later in this sample.

     properties = this.GetFields(this.GetType());

     if (!record.ValidateRequiredProperties(properties,
                                            returnMessages))
     {
        for (int i = 0; i < returnMessages.Count; i++)
        {
           Debug.WriteLine(returnMessages[i]);
        }
        return false;
     }

   }
   catch (Exception) { throw;}
   return true;
 
 }
 



 // This code was extracted from the ADO.NET Code Generator mentioned
 // above.

 // Here is a sample property to demonstrate how to use the
 // ColumnAttributes class.  It sets this as being required
 // and tells the validator that it is an int data type.

 private int cellAlignmentID = 0;
 
 [ColumnAttributes("CellAlignmentID",true,"int")]
 public int CellAlignmentID
 {
   get 
   {
      return cellAlignmentID;
   }
   set 
   { 
      if (value != cellAlignmentID)
      { 
        cellAlignmentID = valuue;
      }
   }
 }





// The ColumnAttributes class itself.

 
[AttributeUsage(AttributeTargets.Property,AllowMultiple = true)] 
 public sealed class ColumnAttributes : System.Attribute  
 { 
 
   private string columnName;
   private bool isRequired;
   private string propertyType;
 
   public string ColumnName
   {
     get { return columnName; } 
   }
 
   public bool IsRequired
   {
     get { return isRequired; } 
   }
 
   public string PropertyType
   {
      get { return propertyType; } 
   }
 
   public ColumnAttributes(string columnNameValue,
                           bool isRequiredValue,
                           string propertyTypeValue)
   { 
      columnName = columnNameValue; 
      isRequired = isRequiredValue; 
      propertyType = propertyTypeValue; 
   } 
 
 } 




// Here is a method we can use to get an array of
// PropertyInfo objects as well as custom attributes.
// Make sure every class has this in method available
// to run on itself.  The ADO.NET Code Generator has
// this apart of the CustomAttributes.cs that all
// DataObjects inherit.

 
 public object[,] GetFields(Type t) 
 { 
 
   PropertyInfo[] fields = t.GetProperties(); 
   PropertyInfo field; 
   Attribute[] attributes; 
   object[,]  structureInfo = new object[fields.Length,2];  
 
   try 
   { 
      for(int i =0;i<fields.Length;i++) 
      {       
        field = fields[i];
        attributes =  Attribute.GetCustomAttributes(field,
                                           typeof(DataObjects.Tables.ColumnAttributes),
                                           false); 
        structureInfo[i,0] = field; 
        structureInfo[i,1] = attributes; 
      } 
   } 
   catch (Exception) { throw; } 
   return structureInfo; 
 } 
  
 
 public bool ValidateRequiredProperties(object[,] properties,
                                        List<string> returnMessages)
 {
    bool returnValue = true;
    PropertyInfo property;
    Attribute[] attributes;
    ColumnAttributes columnAttribute = null;

    try
    {


      if (properties == null)
      {
        throw new Exception("Please pass in the results of this.GetFields().");
      }
 
      if (returnMessages != null) 
      { 
         returnMessages.Clear(); 
      } 
 
      for (int i = 0; i <= properties.GetUpperBound(0); i++) 
      { 
 
          property = (PropertyInfo)properties[i, 0]; 
          attributes = (Attribute[])properties[i, 1]; 

          foreach (Attribute attribute in attributes) 
          { 

            columnAttribute = (ColumnAttributes)attribute; 

            if (!columnAttribute.IsRequired) 
            { 
              continue; 
            } 

            //    Debug.WriteLine(columnAttribute.ColumnName); 
           //     Debug.WriteLine(property.GetValue(this,null)); 

           if (!this.ValidateRequiredPropertyInfo(columnAttribute, 
                                                  property)) 
           { 

              returnValue = false; 

              if (returnMessages != null) 
              { 
                 returnMessages.Add(columnAttribute.ColumnName + " is required.");
              } 
           } 

         } 

     } 
     } 
     catch (Exception) { throw;}
     return returnValue;
   }
   
 
     public bool ValidateRequiredProperties(object[,] properties) 
     { 
        List<string> returnMessages = null; 

        try
        {
           return ValidateRequiredProperties(properties,
                                             returnMessages); 
        } 
        catch (Exception) { throw; } 
     } 
 
     public bool ValidateRequiredPropertyInfo(ColumnAttributes columnAttribute, 
                                              PropertyInfo property) 
     { 
        try 
        { 
          switch (columnAttribute.PropertyType.ToLower()) 
          { 

              case "int": 

                  if ((int)property.GetValue(this, 
                                        null) == 0) 
                  { 
                      return false; 
                  } 
                  
                  break; 

              case "int16": 

                  if ((Int16)property.GetValue(this, 
                                        null) == 0) 
                  { 
                      return false; 
                  } 
                  
                  break; 

              case "int32": 

                  if ((Int32)property.GetValue(this, 
                                        null) == 0) 
                  { 
                      return false; 
                  } 
                  
                  break; 

              case "int64": 

                  if ((Int64)property.GetValue(this, 
                                        null) == 0) 
                  { 
                      return false; 
                  } 
                  
                  break; 

              case "uint": 

                  if ((uint)property.GetValue(this, 
                                        null) == 0) 
                  { 
                      return false; 
                  } 
                  
                  break; 

              case "uint16": 

                  if ((UInt16)property.GetValue(this, 
                                        null) == 0) 
                  { 
                      return false; 
                  } 
                  
                  break; 

              case "uint32": 

                  if ((UInt32)property.GetValue(this, 
                                        null) == 0) 
                  { 
                      return false; 
                  } 
                  
                  break; 

              case "uint64": 

                  if ((UInt64)property.GetValue(this, 
                                        null) == 0) 
                  { 
                      return false; 
                  } 
                  
                  break; 

                case "double": 

                   if ((double)property.GetValue(this, 
                                                 null) == 0) 
                   { 
                       return false; 
                   } 

                   break; 

                 case "float": 

                     if ((float)property.GetValue(this, 
                                                  null) == 0) 
                     { 
                          return false; 
                     } 

                      break; 

                 case "single": 

                     if ((Single)property.GetValue(this, 
                                                  null) == 0) 
                     { 
                          return false; 
                     } 

                      break; 

                  case "string": 
                  
                      if ((string)property.GetValue(this, 
                                                    null) == String.Empty) 
                      {
                          return false; 
                      } 
                      
                      break; 

                  case "guid": 
                  
                      if ((Guid)property.GetValue(this, 
                                                  null) == Guid.Empty) 
                      {
                          return false; 
                      } 
                      
                      break;
                      

                  default: 

                      if (property.GetValue(this, 
                                            null) == null) 
                      { 
                          return false; 
                      } 
                      
                      break; 
              } 
            } 
            catch (Exception) { throw; } 
            return true; 
         } 
 
 

Robbe has been a Microsoft MVP in C# since 2004.  He is also the co-founder of NullSkull.com which provides .NET articles, book reviews, software reviews, and software download and purchase advice.