The problem
You write a class in true SOLID tradition, and then you want to write the unit tests (or you do true TDD and do the reverse).
All well and good until you want to test some “internal” protected or private methods.
Purists would say you only need to test the external public interface. My response is that this is rubbish, you should test bite-sized pieces of fuctionality, especially if you are writing base classes (and no I don’t want to get into a discussion of programming against interfaces only and not using inheritance)
The Solution
After some googling and manipulation I discovered the Microsoft PrivateObject class (in the Microsoft.VisualStudio.QualityTools.UnitTestFramework assembly). This allows the “wrapping” of the class to be tested and then it’s protected or private methods can be invoked.
I discovered a wrapper to this PrivateObject class which makes unit testing straightforward e.g. if class ClassToTest has a protected method with signature ProctedMethod(int,string) then it can be invoked for testing as:
var x = new ClasstoTest();
dynamic target = new DynamicAccessor(x, typeof(ClassToTest));
target.ProtectedMethod(1,"abcd");
This has worked fine and dandy until trying to test a class with generic methods. I found some information on StackOverflow (this article, and this one) I modified the class to use reflection and it works for most generic methods (it’s still not perfect).
The Code
/// <summary>
/// DynamicAccessor
///
/// Class to be utilised in Unit Tests to allow protected and private
/// members and properties to be called from external tests
///
/// Not perfect and based on someone else's hard word along with
/// a bit of hot-rod magic
///
/// </summary>
public class DynamicAccessor : DynamicObject
{
public PrivateObject privateObject;
private object parentObject;
public DynamicAccessor(object d)
{
this.privateObject = new PrivateObject(d);
}
public DynamicAccessor(object d, Type t)
{
this.parentObject = d;
this.privateObject = new PrivateObject(d, new PrivateType(t));
}
/// <summary>
/// The try invoke member.
///
/// Doesn't work as is with Generic<T> methods
///
/// so use a combination of
/// https://stackoverflow.com/questions/5492373/get-generic-type-of-call-to-method-in-dynamic-object
/// to get type params and
/// https://stackoverflow.com/questions/42006662/testing-private-static-generic-methods-in-c-sharp
/// to invoke
///
/// Hot Rod Programming (DJ)
///
/// </summary>
/// <param name="binder">
/// The binder.
/// </param>
/// <param name="args">
/// The args.
/// </param>
/// <param name="result">
/// The result.
/// </param>
/// <returns>
/// The <see cref="bool"/>.
/// </returns>
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
try
{
// https://stackoverflow.com/questions/5492373/get-generic-type-of-call-to-method-in-dynamic-object
var csharpBinder = binder.GetType().GetInterface("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder");
var typeArgs = (csharpBinder.GetProperty("TypeArguments").GetValue(binder, null) as IList<Type>);
//------------------------------------
// if typeargs then a generic method |
//------------------------------------
if (typeArgs.Any())
{
//----------------------------------------------------------------------------------------------------
// Use reflection to get to method |
// https://stackoverflow.com/questions/42006662/testing-private-static-generic-methods-in-c-sharp |
//----------------------------------------------------------------------------------------------------
MethodInfo fooMethod = this.parentObject.GetType().GetMethod(binder.Name, BindingFlags.NonPublic | BindingFlags.Instance);
if (fooMethod == null)
{
result = null;
return false;
}
//-------------------------------------------
// Turn Into Generic and invoke with params |
//-------------------------------------------
MethodInfo genericFooMethod = fooMethod.MakeGenericMethod(typeArgs.ToArray());
result = genericFooMethod.Invoke(this.parentObject, args);
}
else
{
//-----------------------------
// Non Generic so just invoke |
//-----------------------------
result = privateObject.Invoke(binder.Name, args);
}
return true;
}
catch (MissingMethodException)
{
result = null;
return false;
}
}
public void SetProperty(string propName, object o)
{
this.privateObject.SetProperty(propName, o);
}
}