Unit testing non-public types using reflection
Let's say you want to unit test the following class:
using System;
internal class Foo
{
internal Foo(object parameter)
{
if (parameter == null)
throw new ArgumentException();
}
internal int ReturnZero()
{
return 1;
}
}
First of all, besides being pointless, it is buggy. The constructor should throw an ArgumentNullException and ReturnZero() should return 0. More importantly for this post though, it is not public. Now, you could sign your test assembly and use the InternalsVisibleTo attribute; however, that won't help you with types and members that are declared as private or members declared as protected. But I love reflection and with reflection I can call into anything I want though the code looks more complicated than it should. That said, I may decide to write the following test code to be executed by MSTest.exe/VSTT.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Reflection;
[TestClass]
public class FooTests
{
private Type fooType;
private ConstructorInfo fooConstructor;
private MethodInfo returnZeroMethod;
public FooTests()
{
fooType = Type.GetType("Foo, Foo", true, false);
fooConstructor = fooType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic,
null,
new Type[] { typeof(object) },
null);
returnZeroMethod = fooType.GetMethod("ReturnZero",
BindingFlags.Instance | BindingFlags.NonPublic);
}
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Foo_ctor_parameter_Null()
{
fooConstructor.Invoke(new object[] { null });
}
[TestMethod]
public void Foo_ReturnZero()
{
object fooInstance = fooConstructor.Invoke(new object[] { new object() });
Assert.AreEqual<int>(0, (int)returnZeroMethod.Invoke(fooInstance, null));
}
}
This works fine and both tests fail as expected. However, because we are using reflection and Invoke() throws a TargetInvocationException when the target methods throws with the InnerException property set to the original exception our test result for Foo_ctor_parameter_Null() contains the slightly cryptic message "Test method FooTests.Foo_ctor_parameter_Null threw exception System.Reflection.TargetInvocationException, but exception System.ArgumentNullException was expected. Exception message: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.ArgumentException: Value does not fall within the expected range. ". Not a big deal but not very nice to read when examining test failures. If only there was an overload of Invoke() that allows us to tell it to let the original exception through. But there isn't and we can't create one. But we can create an extension method to mimic this behavior:
using System.Reflection;
public static class ConstructorInfoExtensions
{
public static object Invoke(this ConstructorInfo constructor,
object[] parameters,
bool throwOriginalException)
{
if (throwOriginalException)
{
try
{
return constructor.Invoke(parameters);
}
catch (TargetInvocationException e)
{
if (e.InnerException != null)
throw e.InnerException;
else
throw e;
}
}
else
return constructor.Invoke(parameters);
}
}
This extension method looks like an overload which I prefer over a method with a name like InvokeAndSuppressTargetInvocationException(). If throwOriginalException is true and a TargetInvocationException occurs it will catch it and throw TargetInvocationException.InnerException instead unless InnerException is null. Now we can change Foo_ctor_parameter_Null() to:
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Foo_ctor_parameter_Null()
{
fooConstructor.Invoke(new object[] { null }, true);
}
The test of course still fails but now we get the message "Test method FooTests.Foo_ctor_parameter_Null threw exception System.ArgumentException, but exception System.ArgumentNullException was expected. Exception message: System.ArgumentException: Value does not fall within the expected range. " which is exactly what we would get if there was no reflection involved.
By the way, talking of ExpectedException and reflection: There is the general issue that Invoke() can actually throw a couple of different exceptions including the very common ones ArgumentException and InvalidOperationException. In case you are expecting one of those exception types (see the API reference for a full list) you need to be extra careful to make sure that you don't end up with tests that pass simply because Invoke() is throwing instead of the target of invocation.
Last but not least I'd like to mention that we can create another extension method like the one above to be used in case we are invoking test methods through reflection. Granted this is a special case but as shown in How would you test a... C# code generator with Visual Studio Team Test there are use cases for this.
using System.Reflection;
namespace SnapshotGeneratorTests
{
public static class MethodBaseExtensions
{
public static object Invoke(this MethodBase method,
object obj,
object[] parameters,
bool throwOriginalException)
{
if (throwOriginalException)
{
try
{
return method.Invoke(obj, parameters);
}
catch (TargetInvocationException e)
{
if (e.InnerException != null)
throw e.InnerException;
else
throw e;
}
}
else
return method.Invoke(obj, parameters);
}
}
}
This posting is provided "AS IS" with no warranties, and confers no rights.
Comments
- Anonymous
June 19, 2008
The comment has been removed