65.9K
CodeProject 正在变化。 阅读更多。
Home

动态单元测试

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2011年8月30日

CPOL

3分钟阅读

viewsIcon

29653

downloadIcon

185

动态单元测试

引言

在为一个具有复杂业务逻辑的应用程序编写单元测试时,我遇到了一些只公开少量public方法,但有很多private方法的类。由于核心逻辑主要嵌入在这些private方法中,我认为在测试中不能跳过它们。因此,我开始寻找一种在我的测试项目中调用它们的方法。我发现 MsTest 已经有一些调用private方法的快捷方式(即PrivateObjectPrivateType类)。不幸的是,我已经熟悉 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方法,每次在声明为dynamicDynamicObjectBase实例上调用方法时,都会调用该方法。

该类使用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日 - 简化了示例代码
动态单元测试 - CodeProject - 代码之家
© . All rights reserved.