动态单元测试





5.00/5 (2投票s)
动态单元测试
引言
在为一个具有复杂业务逻辑的应用程序编写单元测试时,我遇到了一些只公开少量public
方法,但有很多private
方法的类。由于核心逻辑主要嵌入在这些private
方法中,我认为在测试中不能跳过它们。因此,我开始寻找一种在我的测试项目中调用它们的方法。我发现 MsTest 已经有一些调用private
方法的快捷方式(即PrivateObject
和PrivateType
类)。不幸的是,我已经熟悉 NUnit,并且不想在开发过程中切换到另一个测试框架。我曾经想将private
方法标记为internal
,因为如果使用InternalsVisibleTo
属性标记程序集本身,则可以从它们所在的程序集外部调用具有该访问修饰符的方法,如下所示(代码来自AssemblyInfo.cs)
[assembly: InternalsVisibleTo("Business.Tests")]
背景
在将一些修饰符从private
更改为internal
之后,我很快感到有些沮丧,因为我看到我只是为了我的测试而扩大了这些成员的范围。所以我认为反射可能是完成这项任务的更好方法。这是一个简单的private
方法的例子,用于测试
public class Range<T> : IRange<T> where T : IComparable<T>
{
public T Start { get; set; }
public T End { get; set; }
//... other methods omitted
private static bool Overlaps(IRange<T> first, IRange<T> second)
{
return first.Start.Between(second) || first.End.Between(second);
}
}
这是使用反射调用它的Test
方法(它使用 NUnit 2.5.10 版本)
[Test,
Description("Tests two ranges that don't overlap.")]
public void OverlapTest([Random(0, 1000, 5)] int firstStart,
[Random(1001, 2000, 5)] int firstEnd,
[Random(2001, 5000, 5)] int secondStart,
[Random(5001, 10000, 5)] int secondEnd)
{
var methodInfo = typeof(Range<int>).GetMethod
("Overlaps", BindingFlags.NonPublic | BindingFlags.Static);
var result = methodInfo.Invoke(null, new[]
{
new Range<int>(firstStart, firstEnd),
new Range<int>(secondStart, secondEnd)
});
Assert.False((bool)result);
}
我从之前的经验中了解到,对反射的幼稚使用会大大降低依赖它的程序的效率。在特定情况下,使用随机值作为参数会导致多次调用Test
方法,并随后调用Type.GetMethod
方法。现在,人们也可以忽略单元测试中的性能问题,但这里也存在冗长的代码问题。使用上面的语法会导致大量的“仪式性”代码。所以我开始考虑使用 C# 4 的一个新特性,即dynamic
关键字和DynamicObject
类。我刚刚读完这篇不错的文章 Understanding the Dynamic Keyword in C# 4,其中展示了一些使用dynamic
关键字更简洁地调用 Microsoft Excel 的 COM 方法的示例。我认为它对我的单元测试也可能有用。所以我编写了DynamicObjectBase
类
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Reflection;
namespace Business.Test
{
public class DynamicObjectBase: DynamicObject
{
private readonly Type _type;
private readonly Object _instance;
private readonly Dictionary<string, MethodInfo> _methods =
new Dictionary<string, MethodInfo>();
public DynamicObjectBase(Object instance)
{
_instance = instance;
_type = instance.GetType();
}
public DynamicObjectBase(Type type)
{
_instance = null;
_type = type;
}
public override bool TryInvokeMember
(InvokeMemberBinder binder, object[] args, out object result)
{
if (!_methods.ContainsKey(binder.Name))
{
var methodInfo = _type.GetMethod(binder.Name,
BindingFlags.NonPublic | BindingFlags.Static |
BindingFlags.Instance | BindingFlags.Public);
_methods.Add(binder.Name, methodInfo);
}
var method = _methods[binder.Name];
result = method.Invoke(method.IsStatic ? null : _instance, args);
return true;
}
}
}
Using the Code
上面的代码覆盖了TryInvokeMember
方法,每次在声明为dynamic
的DynamicObjectBase
实例上调用方法时,都会调用该方法。
该类使用MethodInfo
对象的缓存,并在其构造函数中接受要查找private
方法的类的实例或类型。
如果一个类只包含static
方法,我们可以调用接受Type
实例的构造函数。相反,如果我们要测试实例private
方法,则必须使用接受Object
作为参数的构造函数。在Test
类中,我们可以创建DynamicObjectBase
的实例,并在其上调用private
方法,无论是instance
还是static
。因此,Type.GetMethod
方法只会被调用一次,并在后续调用中使用MethodInfo
的缓存实例。但是,对MethodInfo.Invoke
的调用仍然存在,并且在 CPU 负载方面是最昂贵的。所以性能测试只显示了执行时间上非常小的增益。但是我们已经从测试中剥离了混乱的反射代码,所以看看重写的单元测试中的调用代码是多么的不冗长
private readonly dynamic _range;
public RangeFixture()
{
_range = new DynamicObjectBase(typeof(Range<int>));
}
[Test,
Description("Tests two ranges that don't overlap.")]
public void DynamicOverlapTest([Random(0, 1000, 5)] int firstStart,
[Random(1001, 2000, 5)] int firstEnd,
[Random(2001, 5000, 5)] int secondStart,
[Random(5001, 10000, 5)] int secondEnd)
{
var result = _range.Overlaps(new Range<int>(firstStart, firstEnd),
new Range<int>(secondStart, secondEnd));
Assert.False(result);
}
关注点
DynamicObject
类不仅适用于与需要运行时类型检查的对象(曾经称为“后期绑定”)进行交互,而且还适用于隐藏一些冗长的反射代码,这些代码动态调用类的成员。它非常适合编写简洁易懂的单元测试,就像上面描述的场景一样。
历史
- 2011年8月30日 - 首次发布
- 2011年8月31日 - 简化了示例代码