Thursday, March 25, 2010

Unit Testing with Private Accessors Part 2

In the previous post, I demo'ed my static helper class method of accessing private members.  In this post I wanted to explore the possibility of creating a T4 code generator that given a type creates a strongly typed wrapper that tests can access simply and with very readable test code.
Lets start at the end and work back to the beginning. Here's the use case...

Here's a sample of how my static helper class would look in a use case:


   24         [Test]
   25         public void TestUsage() {
   26             var target = new PrivateAccessorGeneratorTestClass();
   27 
   28             PrivateAccessor.SetProperty(target"privateInt"1);
   29             PrivateAccessor.SetProperty(target"PrivateObject"new List<string>(new[] { "Ben","Rees" }));
   30             PrivateAccessor.SetProperty(target"privateString""something");
   31             PrivateAccessor.InvokeMethod(target"VoidMethod");
   32 
   33             var methodResult = PrivateAccessor.InvokeMethod<int>(target"IntMethod"newobject[] { 1"2" });
   34             var privateIntResult = (int)PrivateAccessor.GetField(target"privateInt");
   35             var privateObjectResult = (IList<string>)PrivateAccessor.GetProperty(target,"PrivateObject");
   36             var privateStringResult = (string)PrivateAccessor.GetProperty(target,"privateString");
   37 
   38             Assert.AreEqual(1privateIntResult);
   39             Assert.AreEqual(1methodResult);
   40             Assert.AreEqual(2privateObjectResult.Count);
   41             Assert.AreEqual("something"privateStringResult);
   42         }


And here's how the new use case would look using the generated strongly typed wrapper...

    8         [Test]
    9         public void TestUsage() {
   10             var target = new PrivateAccessorGeneratorTestClass();
   11             var accessor = new PrivateAccessorGeneratorTestClass_Accessor(target);
   12 
   13             accessor.privateInt = 1;
   14             accessor.PrivateObject = new List<string>(new[] { "Ben""Rees" });
   15             accessor.privateString = "something";
   16             accessor.VoidMethod();
   17             var result = accessor.IntMethod(1"2");
   18 
   19             Assert.AreEqual(1accessor.privateInt);
   20             Assert.AreEqual(1result);
   21             Assert.AreEqual(2accessor.PrivateObject.Count);
   22             Assert.AreEqual("something"accessor.privateString);
   23         }
The target is the class with non-public members to which we need to access and the accessor is the strongly typed generated wrapper class that allows easy access and takes care of the finding and invoking the members using reflection .

Here is the target class (for clarity):

   26     public class PrivateAccessorGeneratorTestClass {
   27         public PrivateAccessorGeneratorTestClass(string data) {
   28             // omitted for clarity - not important
   29         }
   30 
   31         internal int privateInt { getprivate set; }
   32 
   33         internal List<string> PrivateObject { getprivate set; }
   34 
   35         private string privateString { getset; }
   36 
   37         private void VoidMethod() {
   38             // omitted for clarity - not important
   39         }
   40 
   41         protected int IntMethod(int istring s) {
   42             // omitted for clarity - not important
   43         }
   44     }

The generator produces the PrivateAccessorGeneratorTestClass_Accessor class based on the type it is give, which in this example case is the PrivateAccessorGeneratorTestClass.
I won't go into detail on how the generator works just now, suffice to say it just enumerators through all non-public members and outputs a wrapping method to call the method on the target type using reflection. 

This all looks pretty good from a use case point of view.  However there are a number of down sides:

Pros / Cons:
+ Nice readably use case syntax.
+ Easy to use in testing.
- Requires maintenance of the Code Generator TT file. A list of types must be given to the generator to produce an accessor for each type required in testing.
- The code generator doesn't automatically generate during compile (could be automated with a batch file). [Edit 26-March] Fixed see below.
- Once the code generator has run, it file-locks the assemblies it references, which quite often stops the build process because it needs to rewrite the DLL file.
- Simply renaming or removing a member's name, will still successfully compile, because the code generator must be run manually. [Edit 26-March] fixed see below.

There are some ways around these downsides.  One idea I will pursue is to make a copy of the solution DLL's and the TT generator file can reference those. This copy an take place pre-build, and so could the execution of the code generator.  This might circumvent the locking issue.  (Although it could just defer it to the second build). Then renaming a member name would cause a compile error on the second compile after the change.

More research I think.

[Edit 26-March-2010]
I have explored automating template transform on building the solution (in Vs not CI).  I wondered if this might circumvent the problem with file locking references to class libraries.  This is how I did it:

  1. Add a batch file to your solution with the following text:
    "%CommonProgramFiles%\microsoft shared\TextTemplating\10.0\texttransform.exe" -out "%1.cs" -P "%ProgramFiles%\Reference Assemblies\Microsoft\Framework\v3.5" -P %2 "%1.tt"

    (Without the carriage returns)
  2. Add a Pre-Build task to the project which contains the TT file:
    $(SolutionDir)RunTemplate.bat $(ProjectDir)AccessorGeneratorTestClass.g $(OutDir)

    Replace "AccessGeneratorTest.g" with the name of your template to transform.  Note that the .TT extension is missing.  My actual file is named AccessGeneratorTest.g.tt in this example, and in the above commandline I have dropped the .TT.
  3. Build the solution, this will transform the template first.

However it doesn't prevent the T4 engine from file locking the referenced Dll's.  This really limits the ease of use of these templates.

I still need to explore copying the DLL's on a Pre-Build task. Although the chances for that to work are slim.

[Edit 1-April-2010]
A colleague of mine (thanks Simon) found an excellent toolkit for T4 called funnily enough T4 Toolkit.  Its written by Oleg Sych a very respectable developer and authority on all things T4.

Inside this toolkit there is a alternative preprocessor to reference an assembly and it copies the assembly instead of referencing it in place. And this gets around the reference locking issue.  Wow, thanks Oleg.  To use Oleg's toolkit you MUST install it with his MSI you cannot simply reference the dll.

So now the Pros vs Cons of using a generated accessor the Pros far out way the Cons.

Here's a use case template of the T4 using Oleg's preprocessor.

// <autogenerated/>
// Last generated <#= DateTime.Now #>
<#@ template language="C#" hostspecific="true"#>

<#@ assembly name="System" #>

<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="bin\debug\FrameworkWpf.dll" #>
<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="bin\debug\FrameworkTestToolkit.dll" #>
<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="bin\debug\WpfAppTemplate.exe" #>

<#@ output extension=".cs" #>

<#@ import namespace="System" #>
<#@ import namespace="FrameworkTestToolkit" #>

namespace WpfAppTemplateTest {
using System;
using System.Reflection;
<# 
    // Add new types into the below array:
    Type[] types = new Type[] { 
typeof(FrameworkWpf.SafeEvent),
typeof(FrameworkWpf.Mvvm.ControllerBase),
typeof(FrameworkTestToolkit.PrivateAccessorGeneratorTestClass),
typeof(WpfAppTemplate.PostController),
typeof(WpfAppTemplate.ShellController),
};


// Do not modify this code
foreach (Type type in types) {
PrivateAccessorGenerator builder = new PrivateAccessorGenerator(type, WriteLine, Error, Warning);
builder.Generate();
}
#>
}

Happy generations.
[Edit] See this post for more information: http://blog.rees.biz/2010/03/private-accessor-t4-generation-for.html

1 comment:

  1. See this post for more details on the T4 generator. http://blog.rees.biz/2010/03/private-accessor-t4-generation-for.html

    ReplyDelete