Jolt.Testing : Dependency Injection Support

In its roots, dependency injection is a form of the strategy/policy design pattern. The implementation of a class is composed of smaller classes, all implementing their respective interfaces, and access to those classes is granted at the interface level. Thus, it is possible for the implementation of the top-level class to be configured at run-time by providing different implementations of an interface.

Dependency injection is an excellent tool for reducing the scope of unit testing as classes can be tested in isolation. The dependencies of a class are tested first, then the top-level class is tested by composing it of mock objects. Each mock object fixes its implementation so that it exercises a code path or aspect of the top-level class, hence reducing the permutations of interactions between the dependency and dependent classes.

Frameworks such as Rhino.Mocks and TypeMock.NET support the construction of mock objects, however most of these frameworks (1) suffer from one major drawback -- how does one mock sealed classes or static methods? Two examples that illustrate this problem are as follows:
  • If your class is dependent on the file system through the System.IO.File class, how do you test it without interacting with the file system?
  • You class performs actions and associates them with a time stamp of execution via the System.DateTime.Now property. How do you accurately verify the value of the time stamp?

Table of Contents

  1. Code Generation
  2. Design Scope
  3. Usage Examples
    1. Generation of interface and proxy types
    2. Generation of generic interface and proxy types
    3. Overriding the return type of a generated method
    4. Accessing XML doc comments for generated types
    5. Generation of assembly containing proxy types
    6. Signing generated assemblies
    7. Accessing XML doc comments for generated assemblies
    8. Using generated assemblies
    9. Using ProxyAssemblyGen.exe

Code Generation

Static methods can not implement an interface, and sealed classes can not be derived from. As a result, the mocking framework can not generate a proxy for the dependency since runtime-proxy generation generally relies on inheritance. The recommended solution to this problem is to manually create a proxy class that forwards to the methods of the subject class, where the methods of the proxy are either virtual or implement an interface. The top-level class is now changed to depend on the proxy, directly or via its interface.

code-generation-before.png CreateFile() is implemented in terms of System.IO.File.Create() and LogTimeStamp() is implemented in terms of System.DateTime.Now.
code-generation-after.png CreateFile() is implemented in terms of FileSystemProxy.CreateFile(), which forwards to System.IO.File.Create(). LogTimeStamp() is implemented in terms of ClockProxy.Now, which forwards to System.DateTime.Now.


Anyone who has employed this technique may have realized that the process is automated via code generation; declaring the proxy classes and interfaces by hand is a monotonous and error prone task! The Jolt.Testing library provides classes to perform this type of code generation at two different levels, favoring the interface and proxy style of implementation.
  • Types available at runtime
    • Method-level operations that generate the declarations and implementation of the proxy and interface types, returned as System.Type instances.
    • These are low-level operations intended to facilitate the implementation of 3rd party frameworks.
    • See Jolt.Testing.CodeGeneration.ProxyTypeBuiler.
  • Types available in assemblies
    • Aggregates the method-level operations to provide persisted assemblies, containing many proxy and interface types.
    • These higher level operations consist of the more common use cases, and can be exposed to a shell program for user-input (Jolt.Testing provides such a program).
    • See Jolt.Testing.CodeGeneration.ProxyAssemblyBuilder and Jolt.Testing.CodeGeneration.ProxyAssemblyDriver.

Design Scope

The dependency injection code generation tools of the Jolt.Testing library are designed to support the following members of a sujbect type.
  • All public instance and static methods
  • All public instance and static properties
  • All public instance and static indexers
  • All public instance and static events
  • All public instance constructors

Furthermore, the following configuration aspects are supported.
  • Assembly file name
  • Proxy/interface type namespace
  • Restriction of generation to static members, methods, properties, and events, or any combination of

Usage Examples


The following code snippet demonstrates how to generate an interface and proxy type for a class dependent on the System.DateTime.Now property and System.DateTime.FromFileTimeUtc() method.

using Jolt.Testing.CodeGeneration;
using System;
using System.Reflection;
 
void CreateProxyTypes()
{
  string namespaceOfGeneratedTypes = "root";
  Type subjectType = typeof(DateTime);
  
  ProxyTypeBulder builder = new ProxyTypeBuilder(namespaceOfGeneratedTypes, subjectType);
  builder.AddProperty(sujbectType.GetProperty("Now", BindingFlags.Static | BindingFlags.Public));
  builder.AddMethod(subjectType.GetMethod("FromFileTimeUtc"));
 
  Type proxyInterface = builder.CreateInterface();
  Type proxy = builder.CreateProxy();
}


The following code snippet demonstrates how to generate an interface and proxy type for the generic class Factory and its generic methods.

using Jolt.Testing.CodeGeneration;
using System.Linq;
using System.Reflection;
 
public static class Factory<T>
{
  public static T Create() { /* ... */ }
  public static T Create<TParam>(TParam param) { /* ... */ }
  public static T Create<TParam1, TParam2>(TParam1 p1, TParam2 p2) { /* ... */ }
  
  public static bool DoSomethingElse<TParam>(TParam p) { /* ... */ }
}

void CreateProxyTypes()
{
  string namespaceOfGeneratedTypes = "root";
  Type subjectType = typeof(Factory<>);
  
  ProxyTypeBulder builder = new ProxyTypeBuilder(namespaceOfGeneratedTypes, subjectType);
  builder.AddMethod(subjectType.GetMethods().Single(m => !m.IsGenericMethod));
  builder.AddMethod(subjectType.GetMethods().Single(m => m.IsGenericMethod && m.GetParameters().Length == 2));
  builder.AddMethod(subjectType.GetMethod("DoSomethingElse", BindingFlags.Static | BindingFlags.Public));
 
  Type proxyInterface = builder.CreateInterface();
  Type proxy = builder.CreateProxy();  
}


In order to use a generated type and its methods with a strongly-typed mocking framework, it may be necessary to override some return types of a generated method (2). For example, consider the System.IO.File class, and the generated interface that accompanies it. If one were to mock the IFile.Open method, the mocking framework would still require the method to return a System.IO.FileStream object, which may be cumbersome if the intent of mocking the method was to bypass the file system and return a abstract System.IO.Stream.

The following example demonstrates how to override the return type for a proxy/interface method and property.

using Jolt.Testing.CodeGeneration;
using System;
using System.IO;

public class ExceptionFactory<T, U>
    where T : U, new()
    where U : Exception, new()
{
    public PathTooLongException CreatePathTooLongException() { /* ... */ }
    public T CreateException() { /* ... */ }
    public V CreateException_Generic<V>() where V : U, new() { /* ... */ }

    public PathTooLongException PathTooLongException { get { /* ... */ } }
    public T Exception { get { /* ... */ } }
}

void CreateProxyTypes()
{
  string namespaceOfGeneratedTypes = "root";
  Type subjectType = typeof(ExceptionFactory<,>);
  Type desiredExceptionType = typeof(IOException);
  Type desiredGenericExceptionType = subjectType.GetGenericArguments()[1];

  ProxyTypeBulder builder = new ProxyTypeBuilder(namespaceOfGeneratedTypes, subjectType);
  builder.AddMethod(subjectType.GetMethod("CreatePathTooLongException"), desiredExceptionType);
  builder.AddMethod(subjectType.GetMethod("CreateException"), desiredGenericExceptionType);
  builder.AddMethod(subjectType.GetMethod("CreateException_Generic"), desiredGenericExceptionType);
  builder.AddProperty(subjectType.GetProperty("PathTooLongException"), desiredExceptionType);
  builder.AddProperty(subjectType.GetProperty("Exception"), desiredGenericExceptionType);

  Type proxyInterface = builder.CreateInterface();
  Type proxy = builder.CreateProxy();  
}


The following code snippet demonstrates how to access the XML doc comments that are created for a generated interface and proxy type.

using Jolt.Testing.CodeGeneration;
using System;
using System.Reflection;
using System.Xml;
 
void CreateProxyTypesXml()
{
  string namespaceOfGeneratedTypes = "root";
  Type subjectType = typeof(DateTime);
  bool produceDocComments = true;
 
  ProxyTypeBulder builder = new ProxyTypeBuilder(namespaceOfGeneratedTypes, subjectType, produceDocComments);
  builder.AddProperty(sujbectType.GetProperty("Now", BindingFlags.Static | BindingFlags.Public));
  builder.AddMethod(subjectType.GetMethod("FromFileTimeUtc"));
 
  Type proxyInterface = builder.CreateInterface();
  Type proxy = builder.CreateProxy();
 
  if (builder.ProducesXmlDocComments)  // Determines if doc comments for subjectType were found.
  {
    using (XmlReader reader = builder.CreateXmlDocCommentReader())
    {
      DoSomethingWith(reader);
    }
  }
}


The following code snippet demonstrates how to generate a persistent assembly containing abstractions of the System.IO.File and System.Messaging.MessageQueue classes.

using Jolt.Testing.CodeGeneration;
using System.IO;
using System.Messaging;
 
void CreateProxyAssembly()
{
  string namespaceOfGeneratedTypes = "Root.Proxies";
  string assemblyFileFullPath = Path.Combine(Path.GetTempPath(), "my.dll");
  
  ProxyAssemblyBulder builder = new ProxyAssemblyBuilder(namespaceOfGeneratedTypes, assemblyFileFullPath);
  builder.AddType(typeof(File));
  builder.AddType(typeof(MessageQueue));
 
  Assembly proxyAssembly = builder.CreateAssembly();
}


The following code snipped demonstrates how to specify the strong-name key-pair that will sign the generated persistent assembly. Note that this example shows the programmatic specification of the key-pair. You may also refer to the key-pair by listing its path in the application configuration file (see the usage of ProxyAssemblyGen.exe for more information).

using Jolt.Testing.CodeGeneration;
using System.IO;
using System.Messaging;
using System.Reflection;
 
void CreateAndSignProxyAssembly()
{
  string namespaceOfGeneratedTypes = "Root.Proxies.Signed";
  string assemblyFileFullPath = Path.Combine(Path.GetTempPath(), "my.dll");
  
  // Alternatively, use application configuration or the ctor overload accepting the path to the key-pair.
  ProxyAssemblyBuilderSettings settings = new ProxyAssemblyBuilderSettings();
  settings.KeyPair = new StrongNameKeyPair(GetKeyPairStream());

  ProxyAssemblyBulder builder = new ProxyAssemblyBuilder(namespaceOfGeneratedTypes, assemblyFileFullPath, settings);
  builder.AddType(typeof(File));
  builder.AddType(typeof(MessageQueue));
 
  Assembly proxyAssembly = builder.CreateAssembly();
  byte[] publicKeyToken = proxyAssembly.GetName().GetPublicKeyToken();
}



The following code snippet demonstrates how to access the XML doc comments that are created for a generated persistent assembly.

using Jolt.Testing.CodeGeneration;
using System.IO;
using System.Messaging;
using System.Xml;
 
void CreateProxyAssemblyXml()
{
  string namespaceOfGeneratedTypes = "Root.Proxies";
  string assemblyFileFullPath = Path.Combine(Path.GetTempPath(), "my.dll");
 
  // Production of XML doc comments is disabled by default.
  // Enable feature through settings, or app.config.
  ProxyAssemblyBulder builder = new ProxyAssemblyBuilder(namespaceOfGeneratedTypes, assemblyFileFullPath, new ProxyAssemblyBuilderSettings(/* ... */));
 
  builder.AddType(typeof(File));
  builder.AddType(typeof(MessageQueue));
 
  if (builder.Settings.EmitXmlDocComments)
  {
    using (XmlReader reader = builder.CreateXmlDocCommentReader())
    {
      DoSomethingWith(reader);
    }
  }
  
  Assembly proxyAssembly = builder.CreateAssembly();
  Stream s = File.Open(Path.ChangeExtension(assemblyFileFullPath, "xml");
}


The following code snippet demonstrates how to refactor an existing class to use the types from the assembly in a previous example.

using Root.Proxies;
using System.IO;
using System.Messaging;
 
public class Client
{
  // public constructor hides the use of proxies from the caller.
  public Client() : this(new FileProxy(), new MessageQueueProxy()) {}
  
  // internal constructor, generally not available to caller, allows dependency injection.
  internal Client(IFile file, IMessageQueue messageQueue)
  {
    m_file = file;
    m_messageQueue = messageQueue;
  }
 
  public void DoWork()
  {
    m_file.Create("file.txt");
    m_messageQueue.Create("queuePath");
  }
  
  private readonly IFile m_file;
  private readonly IMessageQueue m_messageQueue;
}


The following example demonstrates how to use ProxyAssemblyGen.exe to create an assembly with multiple types. Assume that the Factory type is the one defined in the generics example, from the namespace Jolt.Testing.Examples.

To specify which subject types are processed by ProxyAssemblyGen.exe, construct an XML file enumerating the desired types. The names of such types must adhere to the .NET requirements for assembly-qualified names. You may also specify a return type override for all methods on the given type, as depicted for the System.IO.File type.

<?xml version="1.0" encoding="UTF-8"?>
<RealSubjects xmlns="Jolt.Testing.CodeGeneration.Xml">
  <Type name="System.IO.File, mscorlib">
    <OverrideReturnType name="System.IO.FileStream, mscorlib" desiredTypeName="System.IO.Stream, mscorlib"/>
    <OverrideReturnType name="System.IO.StreamReader, mscorlib" desiredTypeName="System.IO.TextReader, mscorlib"/>
  </Type>
  <Type name="System.DateTime, mscorlib" />
  <Type name="System.Messaging.MessageQueue, System.Messaging" />
  <Type name="Jolt.Testing.Examples.Factory`1, AssemblyName, Version=0.0.0.0, Culture=netural, PublicKeyToken=ffffffffffffffff" />
</RealSubjects>
ProxyAssemblyGen.exe is a console application and accepts as its first parameter the path to the XML created above. You can see which aspects of ProxyAssemblyGen.exe are configurable by invoking the program with no arguments, or by inspecting its configuration file.

C:\ProxyAssemblyGen.exe .\types.xml

C:\ProxyAssemblyGen.exe
Usage:
ProxyAssemblyGen.exe subjectTypesFullPath [proxyAssemblyNamespace [proxyAssemblyFullPath] ]

'subjectTypesFullPath' is the full path to an XML configuration file containing the real subject types.
'proxyAssemblyNamespace' is the optional namespace of the generated proxy assembly.
'proxyAssemblyFullPath' is the optional target path of the generated proxy assembly.

C:\type ProxyAssemblyGen.exe.config
(... abbreviated for brevity ...)
<proxyBuilderSettings emitMethods="true"
                      emitProperties="false"
                      emitEvents="false"
                      emitStatics="true"
                      emitXmlDocComments="false"
                      keyPairFullPath="C:\folder\strong-name.snk" />




[1] TypeMock.NET behaves as a debugger/profiler and is capable of intercepting method calls at the IL-level. As a result, it can intercept the call of any method, on any type. On the other hand, Rhino.Mocks generates dynamic proxy classes (not objects) at runtime and hence cannot intercept method calls that are static or those that exist on sealed types.


[2] Please refer to ProxyTypeBuilder Usability Issue for more information.

Last edited Dec 7, 2009 at 2:33 AM by SteveGuidi, version 19

Comments

No comments yet.