Nov 202014
 

There are times when you need to write unit tests and it is important to ensure that you get full code coverage.  That first sentence alone will tip you off to the fact that this is not a TDD scenario, but rather a situation where you need to create test cases for pre-existing code.  It is easy to overlook methods or properties when backfilling tests, so it would be nice to have a tool to help in these situations.  Thankfully, you can use .NET reflection to trawl through your assemblies, writing stub code, looking for the properties and/or methods that you have interest in and making sure that you don’t overlook important items.

I have written a quick example to illustrate how useful reflection can be in auto-generating tests and test stubs. The point here is not that I have written the be-all-end-all test generator, but to show that you can quickly cover some ground in brownfield testing with the proper application of the System.Reflection library.

In this quick example, I have 3 classes that each have some string properties:

namespace AssemblyToTest
{
    public class Class1
    {

        public string String1 { get; set; }
        public string String2 { get; set; }
        public string String3 { get; set; }
        public string String4 { get; set; }
        public string String5 { get; set; }
        public string String6 { get; set; }

    }
    
    public class Class2
    {
        public string String1 { get; set; }
        public string String2 { get; set; }
        public string String3 { get; set; }
        public string String4 { get; set; }
        public string String5 { get; set; }
        public string String6 { get; set; }
    }

    public class Class3
    {
        public string String1 { get; set; }
        public string String2 { get; set; }
        public string String3 { get; set; }
        public string String4 { get; set; }
        public string String5 { get; set; }
        public string String6 { get; set; }
    }
}

Let’s say that you’re interested in looking for all public string properties in classes in your assembly to ensure that you have proper string validation.  In TDD, you would have already written the tests before writing the code, so problem solved!  However, in many cases, you may find yourself retrofitting old code with new tests.  In this example, you can use reflection to load the assembly in which you are interested, loop through all of the types in the assembly, and then loop through the properties of each type.  Armed with this information, you can then use a writer to generate a unit test code file, complete with stubs.  You could even generate some of the test code, if you have enough domain information.

Check out this simple program for generating stub tests:

using System;
using System.IO;
using System.Linq;
using System.Reflection;  // reflection namespace

namespace UnitTestGenerator
{
    /// <summary>
    /// This program returns/generates a list of all properties for all classes in the given assembly that are of type string.  
    /// This program borrows from code found at the following sites:
    /// http://www.csharp-examples.net/reflection-property-names/
    /// http://stackoverflow.com/questions/1315665/c-list-all-classes-in-assembly
    /// http://msdn.microsoft.com/en-us/library/system.reflection.assemblyname(v=vs.110).aspx
    /// http://msdn.microsoft.com/en-us/library/1009fa28(v=vs.110).aspx
    /// http://stackoverflow.com/questions/3723934/using-propertyinfo-to-find-out-the-property-type
    /// </summary>
    /// 
    /// This program generates unit test stubs for all of the string properties of all of the types in an assembly.  It will need to 
    /// be modified to specifically generate the test stubs desired.  
    /// Argument 0 = Path to the assembly
    /// Argument 1 = Path and file name for output file
    class Program
    {
        static void Main(string[] args)
        {
            if (!ValidateArguments(args)){return;}

            using (var sw = File.CreateText(args[1]))
            {
                WriteUsingBlock(sw);
                WriteClassHeader(sw);
              
                var a = Assembly.LoadFrom(args[0]);
                foreach (var type in a.GetTypes())
                {
                    // get all public static properties of MyClass type
                    var propertyInfos = type.GetProperties();

                    // sort properties by name
                    Array.Sort(propertyInfos,
                        delegate(PropertyInfo propertyInfo1, PropertyInfo propertyInfo2)
                        {
                            return propertyInfo1.Name.CompareTo(propertyInfo2.Name);
                        });

                    // write property names
                    foreach (var propertyInfo in propertyInfos)
                    {
                        if (propertyInfo.PropertyType == typeof(string))
                            WriteTestForProperty(sw, type.Name.Replace("`", ""), propertyInfo.Name);
                    }
                }

                WriteClassFooter(sw);
            }
        }

        private static void WriteUsingBlock(TextWriter sw)
        {
            sw.WriteLine("using System;");
            sw.WriteLine("using System.Linq;");
            sw.WriteLine("using Microsoft.VisualStudio.TestTools.UnitTesting;");
            sw.WriteLine("");
        }

        private static void WriteClassHeader(TextWriter sw)
        {
            sw.WriteLine("namespace MyProject.Tests");
            sw.WriteLine("{");
            sw.WriteLine("    [TestClass]");
            sw.WriteLine("    public class MyTests");
            sw.WriteLine("    {");    
        }

        private static void WriteClassFooter(TextWriter sw)
        {
            sw.WriteLine("    }");
            sw.WriteLine("}");
        }

        private static void WriteTestForProperty(TextWriter sw, string typeName, string propertyName)
        {
            sw.WriteLine("      [TestMethod]");
            sw.WriteLine("      public void MyTests_{0}_{1}()", typeName, propertyName);
            sw.WriteLine("      {");
            sw.WriteLine("          // Put in code here for your test");
            sw.WriteLine("      }");
            sw.WriteLine("");
        }

        private static bool ValidateArguments(string[] args)
        {
            if (!args.Any() || args[0] == "/?" || args[0] == "/h" || args[0] == "/help" || args.Count() != 2)
            {
                Console.WriteLine("GenerateUnitTests takes 2 arguments.  The first is the dll for which tests will be created and the second is the output file.");
                Console.WriteLine("Usage: GenerateUnitTests <dll file path> <output file>");
                return false;
            }

            return true;
        }

    }
}

After running the generator code, you will get stub code that looks like this:

using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MyProject.Tests
{
    [TestClass]
    public class MyTests
    {
      [TestMethod]
      public void MyTests_Class1_String1()
      {
          // Put in code here for your test
      }

      [TestMethod]
      public void MyTests_Class1_String2()
      {
          // Put in code here for your test
      }

      [TestMethod]
      public void MyTests_Class1_String3()
      {
          // Put in code here for your test
      }

      [TestMethod]
      public void MyTests_Class1_String4()
      {
          // Put in code here for your test
      }

      // The generator created many more tests, but I have 
      // omitted them for the sake of brevity.

This is just stub code from the example, but you could expand the generator to fit your testing needs.

TDD is definitely the desired way to generate greenfield code.  The benefits of focus alone prove that out.  However, when you have to retrofit tests for when refactoring or generating tests on existing code, using .NET’s reflection capability will save an enormous amount of “pattern pounding” and it will make sure you are thorough!

  One Response to “Using .NET’s System.Reflection to Aid Unit Test Creation”

  1. It is incredible post….

    Thank you so much

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>