This is an extract from the Unit Testing Succinctly eBook, by Marc Clifton, kindly provided by Syncfusion.
A unit test engine in a reflective language (such as any .NET language) has three parts:
- Loading assemblies that contain the tests.
- Using reflection to find the test methods.
- Invoking the methods and validating the results.
This article provides code examples of how this works, putting together a simple unit test engine. If you’re not interested in the under-the-hood investigation of unit test engines, feel free to skip this article.
The code here assumes that we are writing a test engine against the Visual Studio unit test attributes defined in the Microsoft.VisualStudio.QualityTools.UnitTestFramework assembly. Other unit test engines may use other attributes for the same purposes.
Loading Assemblies
Architecturally, your unit tests should either reside in a separate assembly from the code being tested, or at a minimum, should only be included in the assembly if it is compiled in “Debug” mode. The benefit of putting the unit tests in a separate assembly is that you can also unit test the non-debug, optimized production version of the code.
That said, the first step is to load the assembly:
static bool LoadAssembly(string assemblyFilename, out Assembly assy, out string issue) { bool ok = true; issue = String.Empty; assy = null; try { assy = Assembly.LoadFile(assemblyFilename); } catch (Exception ex) { issue = "Error loading assembly: " + ex.Message; ok = false; } return ok; }
Note that professional unit test engines load assemblies into a separate application domain so that the assembly can be unloaded or reloaded without restarting the unit test engine. This also allows the unit test assembly and dependent assemblies to be recompiled without shutting down the unit test engine first.
Using Reflection to Find Unit Test Methods
The next step is to reflect over the assembly to identify the classes that are designated as a “test fixture,” and within those classes, to identify the test methods. A basic set of four methods support the minimum unit test engine requirements, the discovery of test fixtures, test methods, and exception handling attributes:
/// <summary> /// Returns a list of classes in the provided assembly that have a "TestClass" attribute. /// </summary> static IEnumerable<Type> GetTestFixtures(Assembly assy) { return assy.GetTypes().Where(t => t.GetCustomAttributes(typeof(TestClassAttribute), false).Length == 1); } /// <summary> /// Returns a list of methods in the test fixture that are decorated with the "TestMethod" attribute. /// </summary> static IEnumerable<MethodInfo> GetTestMethods(Type testFixture) { return testFixture.GetMethods().Where(m => m.GetCustomAttributes( typeof(TestMethodAttribute), false).Length == 1); } /// <summary> /// Returns a list of specific attributes that may be decorating the method. /// </summary> static IEnumerable<AttrType> GetMethodAttributes<AttrType>(MethodInfo method) { return method.GetCustomAttributes(typeof(AttrType), false).Cast<AttrType>(); } /// <summary> /// Returns true if the method is decorated with an "ExpectedException" attribute while exception type is the expected exception. /// </summary> static bool IsExpectedException(MethodInfo method, Exception expectedException) { Type expectedExceptionType = expectedException.GetType(); return GetMethodAttributes<ExpectedExceptionAttribute>(method). Where(attr=>attr.ExceptionType == expectedExceptionType).Count() != 0; }
Invoking Methods
Once this information has been compiled, the engine invokes the test methods within a try-catch block (we don’t want the unit test engine itself crashing):
static void RunTests(Type testFixture, Action<string> result) { IEnumerable<MethodInfo> testMethods = GetTestMethods(testFixture); if (testMethods.Count() == 0) { // Don't do anything if there are no test methods. return; } object inst = Activator.CreateInstance(testFixture); foreach (MethodInfo mi in testMethods) { bool pass = false; try { // Test methods do not have parameters. mi.Invoke(inst, null); pass = true; } catch (Exception ex) { pass = IsExpectedException(mi, ex.InnerException); } finally { result(testFixture.Name + "." + mi.Name + ": " + (pass ? "Pass" : "Fail")); } } }
Finally, we can put this code together into a simple console application that takes the unit test assembly as parameter results in a usable, but simple engine:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace SimpleUnitTestEngine { class Program { static void Main(string[] args) { string issue; if (!VerifyArgs(args, out issue)) { Console.WriteLine(issue); return; } Assembly assy; if (!LoadAssembly(args[0], out assy, out issue)) { Console.WriteLine(issue); return; } IEnumerable<Type> testFixtures = GetTestFixtures(assy); foreach (Type testFixture in testFixtures) { RunTests(testFixture, t => Console.WriteLine(t)); } } static bool VerifyArgs(string[] args, out string issue) { bool ok = true; issue = String.Empty; if (args.Length != 1) { issue = "Usage: SimpleUnitTestEngine <assembly filename>"; ok = false; } else { string assemblyFilename = args[0]; if (!File.Exists(assemblyFilename)) { issue = "The filename '" + args[0] + "' does not exist."; ok = false; } } return ok; } ... the rest of the code ...
The result of running this simple test engine is displayed in a console window, for example:
Comments