Building Your Own Unit Testing Framework in C# - Part 2

2/13/2011

In the last post, I talked about a couple of simple classes that you could use to create your own unit testing framework.  However I never really talked about a front end for the system. In this post I will show a bit of code that will allow us to dynamically load our DLL from last time and run a series of tests. In order to do that we need to create two separate items, the DLL loader and the test runner. The first item that we will talk about is the DLL loader.

Our system needs a way to load the framework DLL dynamically so our runners aren't tied to a specific version of the framework. By doing this we can add new features as we go and not require the runner to be recompiled/updated (we just drop in the new framework DLL and we're good to go). In order to do this we really only need two classes, a main class to act as our manager and a configuration class:

   1: /*
   2: Copyright (c) 2011 <a href="http://www.gutgames.com">James Craig</a>
   3: 
   4: Permission is hereby granted, free of charge, to any person obtaining a copy
   5: of this software and associated documentation files (the "Software"), to deal
   6: in the Software without restriction, including without limitation the rights
   7: to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   8: copies of the Software, and to permit persons to whom the Software is
   9: furnished to do so, subject to the following conditions:
  10: 
  11: The above copyright notice and this permission notice shall be included in
  12: all copies or substantial portions of the Software.
  13: 
  14: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16: FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17: AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18: LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20: THE SOFTWARE.*/
  21:  
  22: #region Usings
  23: using System;
  24: using System.Collections.Generic;
  25: using System.Linq;
  26: using System.Text;
  27: using Utilities.DataTypes.Patterns.BaseClasses;
  28: using System.Reflection;
  29: using MoonUnitLoader.Configuration;
  30: #endregion
  31:  
  32: namespace MoonUnitLoader
  33: {
  34:     /// <summary>
  35:     /// Manager class for unit testing framework
  36:     /// </summary>
  37:     public class Manager : Singleton<Manager>
  38:     {
  39:         #region Constructor
  40:  
  41:         /// <summary>
  42:         /// Constructor
  43:         /// </summary>
  44:         protected Manager()
  45:             : base()
  46:         {
  47:             Configuration = Gestalt.Manager.Instance.GetConfigFile<Configuration.Configuration>("MoonUnitLoader");
  48:             AssemblyName Name = AssemblyName.GetAssemblyName(Configuration.AssemblyLocation);
  49:             MoonUnitAssembly = AppDomain.CurrentDomain.Load(Name);
  50:             MoonUnitManagerType = MoonUnitAssembly.GetType("MoonUnit.Manager");
  51:             PropertyInfo InstanceProperty = MoonUnitManagerType.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
  52:             MethodInfo Method = InstanceProperty.GetGetMethod();
  53:             MoonUnitManager = Method.Invoke(null, new object[] { });
  54:             TestAssembly = MoonUnitManagerType.GetMethod("Test", new Type[] { typeof(Assembly) });
  55:             TestArray = MoonUnitManagerType.GetMethod("Test", new Type[] { typeof(Type[]) });
  56:             TestType = MoonUnitManagerType.GetMethod("Test", new Type[] { typeof(Type) });
  57:         }
  58:  
  59:         #endregion
  60:  
  61:         #region Function
  62:  
  63:         /// <summary>
  64:         /// Tests an assembly
  65:         /// </summary>
  66:         /// <param name="AssemblyToTest">Assembly to test</param>
  67:         /// <returns>XML string containing the results</returns>
  68:         public string Test(Assembly AssemblyToTest)
  69:         {
  70:             return (string)TestAssembly.Invoke(MoonUnitManager, new object[] { AssemblyToTest });
  71:         }
  72:  
  73:         /// <summary>
  74:         /// Tests a list of types
  75:         /// </summary>
  76:         /// <param name="Types">Types to test</param>
  77:         /// <returns>XML string containing the results</returns>
  78:         public string Test(Type[] Types)
  79:         {
  80:             return (string)TestArray.Invoke(MoonUnitManager, new object[] { Types });
  81:         }
  82:  
  83:         /// <summary>
  84:         /// Tests a type
  85:         /// </summary>
  86:         /// <param name="Type">Type to test</param>
  87:         /// <returns>XML string containing the results</returns>
  88:         public string Test(Type Type)
  89:         {
  90:             return (string)TestType.Invoke(MoonUnitManager, new object[] { Type });
  91:         }
  92:  
  93:         #endregion
  94:  
  95:         #region Properties
  96:  
  97:         private Configuration.Configuration Configuration { get; set; }
  98:         private Assembly MoonUnitAssembly { get; set; }
  99:         private object MoonUnitManager { get; set; }
 100:         private Type MoonUnitManagerType { get; set; }
 101:         private MethodInfo TestAssembly { get; set; }
 102:         private MethodInfo TestArray { get; set; }
 103:         private MethodInfo TestType { get; set; }
 104:  
 105:         #endregion
 106:     }
 107: }
 108:  

The bit of code above is our manager class. It simply loads the DLL for the framework, finds the three Test functions on the main manager class and saves them off for later. It then replicates the functions that are on the framework's Manager class but simply feeds the data to the framework. As for the configuration class, I'm using Gestalt.Net for my configuration which simplifies things a great deal. Anyway, the class itself looks like this:

   1: /*
   2: Copyright (c) 2011 <a href="http://www.gutgames.com">James Craig</a>
   3: 
   4: Permission is hereby granted, free of charge, to any person obtaining a copy
   5: of this software and associated documentation files (the "Software"), to deal
   6: in the Software without restriction, including without limitation the rights
   7: to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   8: copies of the Software, and to permit persons to whom the Software is
   9: furnished to do so, subject to the following conditions:
  10: 
  11: The above copyright notice and this permission notice shall be included in
  12: all copies or substantial portions of the Software.
  13: 
  14: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16: FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17: AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18: LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20: THE SOFTWARE.*/
  21:  
  22: #region Usings
  23: using System;
  24: using System.Collections.Generic;
  25: using System.Linq;
  26: using System.Text;
  27: using Gestalt;
  28: using Gestalt.Attributes;
  29: #endregion
  30:  
  31: namespace MoonUnitLoader.Configuration
  32: {
  33:     /// <summary>
  34:     /// Configuration class
  35:     /// </summary>
  36:     [Config(Name = "MoonUnitLoader")]
  37:     public class Configuration : Config<Configuration>
  38:     {
  39:         #region Constructor
  40:  
  41:         /// <summary>
  42:         /// Constructor
  43:         /// </summary>
  44:         public Configuration()
  45:             : base()
  46:         {
  47:         }
  48:  
  49:         #endregion
  50:  
  51:         #region Properties
  52:  
  53:         /// <summary>
  54:         /// Location of MoonUnit DLL
  55:         /// </summary>
  56:         public virtual string AssemblyLocation { get { return ""; } set { } }
  57:  
  58:         #endregion
  59:     }
  60: }

All it does is defines an AssemblyLocation property. It will be overridden by our runner later on and thus does nothing at present. And that's it. Our framework loader is set up, so that just leaves our runner. To make things simpler I'm only going to show a console app, but you could easily build a Visual Studio add in or something. Anyway, the simple console app is going to take some arguments in, call our framework loader, and then run our tests. In turn it will show us our results in either html or xml depending on the options we specify:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Reflection;
   6: using System.IO;
   7: using System.Xml.XPath;
   8: using System.Xml.Xsl;
   9: using System.Xml;
  10: using Utilities.IO;
  11: using Utilities.Environment;
  12:  
  13: namespace UnitTestApp
  14: {
  15:     class Program
  16:     {
  17:         static void Main(string[] args)
  18:         {
  19:             Console.WriteLine("MoonUnit console test runner");
  20:             Console.WriteLine("Copyright (c) 2011 James Craig");
  21:             if (args.Length == 0)
  22:             {
  23:                 PrintInstructions();
  24:                 return;
  25:             }
  26:             Utilities.Environment.ArgsParser Parser = new Utilities.Environment.ArgsParser();
  27:             List<Option> Options = Parser.Parse(args);
  28:             string AssemblyFile="";
  29:             bool HTML=false;
  30:             foreach (Option Option in Options)
  31:             {
  32:                 if (Option.Command.ToLower() == "assembly"&&Option.Parameters.Count>0)
  33:                 {
  34:                     AssemblyFile = Option.Parameters[0].Replace("\"", "");
  35:                 }
  36:                 else if (Option.Command.ToLower() == "html")
  37:                 {
  38:                     HTML = true;
  39:                 }
  40:             }
  41:             if (string.IsNullOrEmpty(AssemblyFile))
  42:             {
  43:                 PrintInstructions();
  44:                 return;
  45:             }
  46:             string FileName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\Result." + (HTML ? "html" : "xml");
  47:             foreach (Option Option in Options)
  48:             {
  49:                 if (Option.Command.ToLower() == "file")
  50:                 {
  51:                     FileName = Option.Parameters[0].Replace("\"", "");
  52:                 }
  53:             }
  54:             Gestalt.Manager.Instance.RegisterConfigFile(typeof(Config).Assembly);
  55:             AssemblyName Name = AssemblyName.GetAssemblyName(AssemblyFile);
  56:             Assembly TestAssembly = AppDomain.CurrentDomain.Load(Name);
  57:             string Output = MoonUnitLoader.Manager.Instance.Test(TestAssembly);
  58:             if (HTML)
  59:             {
  60:                 string XSLTFileName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\HTML.xslt";
  61:                 Output = Transform(Output, XSLTFileName);
  62:             }
  63:             Utilities.IO.FileManager.SaveFile(Output, FileName);
  64:             System.Diagnostics.Process.Start(FileName);
  65:         }
  66:  
  67:         private static string Transform(string Output,string XSLT)
  68:         {
  69:             using (StreamReader Reader = new StreamReader(XSLT))
  70:             {
  71:                 XPathDocument doc = new XPathDocument(new StringReader(Output));
  72:                 XslCompiledTransform xslTransform = new XslCompiledTransform();
  73:                 XmlTextReader transformReader = new XmlTextReader(Reader);
  74:                 xslTransform.Load(transformReader);
  75:  
  76:                 using (StringWriter Writer = new StringWriter())
  77:                 {
  78:                     xslTransform.Transform(doc, null, Writer);
  79:                     return Writer.ToString();
  80:                 }
  81:             }
  82:         }
  83:  
  84:         private static void PrintInstructions()
  85:         {
  86:             string Name = Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().Location);
  87:             Console.WriteLine("Usage: {0} [options]", Name);
  88:             Console.WriteLine();
  89:             Console.WriteLine("Options:");
  90:             Console.WriteLine("     /Assembly <AssemblyFileLocation>    Specifies the assembly to test");
  91:             Console.WriteLine("     /File <FileLocation>                Outputs the results to the file specified");
  92:             Console.WriteLine("     /HTML                               Outputs the results to HTML");
  93:             Console.WriteLine();
  94:         }
  95:     }
  96:  
  97:     public class Config : MoonUnitLoader.Configuration.Configuration
  98:     {
  99:         protected override string ConfigFileLocation { get { return "./Loader.config"; } }
 100:         public override string AssemblyLocation { get { return _AssemblyLocation; } set { _AssemblyLocation = value; } }
 101:         private string _AssemblyLocation = "";
 102:     }
 103: }

The code above is two classes. The first to note is the Config class. This class inherits from our configuration class from the framework loader. It states that we should create a Loader.config file and overrides the AssemblyLocation property (thus allowing us to specify where the framework is on our system). The other class is the actual main test runner program. This class uses a couple of items from my utility library to simplify things (my args parser and FileManager classes). Basically the application parses the args sent in, looking for three options. The first option is /Assembly, which actually says where the assembly is that holds the tests, /File specifies the location to save the output, and /HTML tells the system to output the results in HTML instead of XML. Once it has determined the options that are specified, it sets up the framework loader, and runs the assembly that contains our tests. Once it gets the results it determines how to save the results and where (and when done with that task it ends). The only function that might give you pause is Transform. In that function all that the system is using an XSLT file to transform the XML results into an HTML doc. And in case you're wondering, I'm using the following XSLT document:

 

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
   3:     xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
   4: >
   5:     <xsl:output method="html" indent="yes"/>
   6:  
   7:     <xsl:template match="/">
   8:         <html>
   9:             <body>
  10:                 <table>
  11:                     <tr>
  12:                         <th>Method</th>
  13:                         <th>Result</th>
  14:                     </tr>
  15:                     <xsl:for-each select="MoonUnit/Tests/Test">
  16:                         <tr>
  17:                             <td>
  18:                                 <xsl:for-each select="Method">
  19:                                     <xsl:value-of select="@name"/>
  20:                                 </xsl:for-each>
  21:                             </td>
  22:                             <td>
  23:                                 <xsl:for-each select="Passed">
  24:                                     Passed
  25:                                 </xsl:for-each>
  26:                                 <xsl:for-each select="Failed">
  27:                                     Failed<br />
  28:                                     Expected: <xsl:value-of select="Expected"/><br />
  29:                                     Result: <xsl:value-of select="Result"/><br />
  30:                                     Error Message: <xsl:value-of select="ErrorText"/><br />
  31:                                     Stack Trace: <xsl:value-of select="Trace"/><br />
  32:                                 </xsl:for-each>
  33:                             </td>
  34:                         </tr>
  35:                     </xsl:for-each>
  36:                 </table>
  37:             </body>
  38:         </html>
  39:     </xsl:template>
  40: </xsl:stylesheet>
That's it, it simply creates a table that says if a test passed or failed and why it failed. It's not pretty, but it gets the job done. Anyway, that's all there is to it. With that we have our unit testing framework in a working state. It's not perfect (and I would still suggest xUnit.Net or one of the other ones out there), but it should give you a decent starting spot for building your own if you ever decide you want to. That being said, you can always go to the MoonUnit Codeplex site and simply download the code that I talk about here (plus any updates that I may have made). So go to the site, download the code, try it out, and happy coding.


Comments