我正在构建一个类库,它将有一些公共和私有方法。我希望能够对私有方法进行单元测试(主要是在开发时,但它也可能对未来的重构有用)。

这样做的正确方法是什么?

有帮助吗?

解决方案

如果您使用的是.net,则应使用 InternalsVisibleToAttribute

其他提示

如果要对私有方法进行单元测试,可能会出现问题。单元测试(一般来说)用于测试类的接口,即其公共(和受保护)方法。你当然可以“破解”一个解决方案(即使只是通过公开方法),但你可能还想考虑:

  1. 如果您要测试的方法确实值得测试,那么将它移到自己的类中可能是值得的。
  2. 向调用私有方法的公共方法添加更多测试,测试私有方法的功能。 (正如评论员指出的那样,如果这些私有方法的功能真的与公共接口有关,那么你应该只这样做。如果它们实际上执行了对用户隐藏的功能(即单元测试),这可能是坏事)。

测试私有方法可能没有用。然而,我有时也喜欢从测试方法中调用私有方法。大多数时候为了防止测试数据生成的代码重复...

Microsoft 为此提供了两种机制:

配件

  • 转到类定义的源代码
  • 右键单击班级名称
  • 选择“创建私有访问器”
  • 选择应创建访问者的项目=>您最终将获得带有名称foo_accessor的新类。该类将在编译期间动态生成,并为所有成员提供公共可用。

然而,当涉及到原始类的接口的改变时,该机制有时有点棘手。所以,大多数时候我避免使用这个。

私有对象类另一种方法是使用 Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject

// Wrap an already existing instance
PrivateObject accessor = new PrivateObject( objectInstanceToBeWrapped );

// Retrieve a private field
MyReturnType accessiblePrivateField = (MyReturnType) accessor.GetField( "privateFieldName" );

// Call a private method
accessor.Invoke( "PrivateMethodName", new Object[] {/* ... */} );

我不同意“您应该只对测试外部接口感兴趣”哲学。这有点像说汽车维修店应该只进行测试,看车轮是否转动。是的,最终我对外部行为感兴趣,但我喜欢我自己的私人内部测试更具体和更重要。是的,如果我重构,我可能不得不改变一些测试,但除非它是一个巨大的重构,我只需要改变一些,而其他(未改变的)内部测试仍然有效的事实是一个很好的指标,重构已经成功。

您可以尝试仅使用公共界面覆盖所有内部案例,理论上可以通过使用公共界面完全测试每个内部方法(或至少每个重要的方法),但您可能必须站在您的要实现这一点,并且通过公共接口运行的测试用例与他们设计用于测试的解决方案的内部部分之间的联系可能很难或无法辨别。有针对性的,确保内部机器正常工作的个别测试非常值得重构的小测试变化 - 至少这是我的经验。如果你必须对每次重构的测试做出巨大的改变,那么也许这没有意义,但在这种情况下,也许你应该完全重新考虑你的设计。一个好的设计应该足够灵活,以允许大多数更改,而无需大量重新设计。

在极少数情况下,我想测试私有函数,我通常会将它们修改为受保护,而我已经编写了一个带有公共包装函数的子类。

班级:

...

protected void APrivateFunction()
{
    ...
}

...

测试的子类:

...

[Test]
public void TestAPrivateFunction()
{
    APrivateFunction();
    //or whatever testing code you want here
}

...

我认为应该问一个更基本的问题是你为什么要首先尝试测试私有方法。这是一个代码气味,你试图通过该类的公共接口测试私有方法,而该方法是私有的,因为它是一个实现细节。人们应该只关注公共接口的行为,而不是关于它如何在封面下实施。

如果我想测试私有方法的行为,通过使用常见的重构,我可以将其代码提取到另一个类(可能具有包级别可见性,因此确保它不是公共API的一部分)。然后我可以孤立地测试它的行为。

重构的产品意味着私有方法现在是一个单独的类,它已成为原始类的协作者。通过自己的单元测试,它的行为将得到很好的理解。

当我尝试测试原始类时,我可以模拟它的行为,这样我就可以专注于测试该类的公共接口的行为,而不必测试公共接口的组合爆炸和所有行为它的私人方法。

我认为这类似于驾驶汽车。当我驾驶汽车时,我没有驾驶发动机罩,所以我可以看到发动机正在工作。我依靠汽车提供的接口,即转速计和速度表来了解发动机是否工作。当我按下油门踏板时,我依靠汽车实际移动的事实。如果我想测试引擎,我可以单独检查它。 :d

当然,如果您有遗留应用程序,直接测试私有方法可能是最后的手段,但我希望重构遗留代码以实现更好的测试。 Michael Feathers写了一本关于这个主题的好书。 http://www.amazon.co.uk/Working-Effectively-Legacy-罗伯特 - 马丁/ DP / 0131177052

私人类型,内部人员和私人成员都是出于某种原因,并且通常你不想直接搞砸他们。如果你这样做,你很可能会在以后休息,因为无法保证创建这些程序集的人会保留私有/内部实现。

但是,有时,在对编译或第三方程序集进行一些黑客攻击/探索时,我最终想要使用私有或内部构造函数初始化私有类或类。或者,有时,当处理我无法改变的预编译遗留库时 - 我最终会针对私有方法编写一些测试。

因此诞生了AccessPrivateWrapper - http://amazedsaint.blogspot .com / 2010/05 / accessprivatewrapper-c-40-dynamic.html - 这是一个快速的包装类,使用C#4.0动态功能和反射,可以轻松完成工作。

您可以创建内部/私人类型,例如

    //Note that the wrapper is dynamic
    dynamic wrapper = AccessPrivateWrapper.FromType
        (typeof(SomeKnownClass).Assembly,"ClassWithPrivateConstructor");

    //Access the private members
    wrapper.PrivateMethodInPrivateClass();

您可以通过两种方式对私有方法进行单元测试

  1. 你可以创建 PrivateObject 类的实例,语法如下

    PrivateObject obj= new PrivateObject(PrivateClass);
    //now with this obj you can call the private method of PrivateCalss.
    obj.PrivateMethod("Parameters");
    
  2. 您可以使用反射。

    PrivateClass obj = new PrivateClass(); // Class containing private obj
    Type t = typeof(PrivateClass); 
    var x = t.InvokeMember("PrivateFunc", 
        BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Public |  
            BindingFlags.Instance, null, obj, new object[] { 5 });
    

我还使用了InternalsVisibleToAttribute方法。值得一提的是,如果你为了达到这个目的而内部使用你以前的私人方法感到不舒服,那么也许他们不应该成为直接单元测试的主题。

毕竟,你正在测试你的类的行为,而不是它的特定实现 - 你可以改变后者而不改变前者,你的测试仍然应该通过。

有两种类型的私有方法。静态私有方法和非静态私有方法(实例方法)。以下2篇文章解释了如何使用示例对私有方法进行单元测试。

  1. 单元测试静态私有方法
  2. 单元测试非静态私有方法

MS Test内置了一个很好的功能,它通过创建一个名为VSCodeGenAccessors的文件使项目中的私有成员和方法可用

[System.Diagnostics.DebuggerStepThrough()]
    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TestTools.UnitTestGeneration", "1.0.0.0")]
    internal class BaseAccessor
    {

        protected Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject m_privateObject;

        protected BaseAccessor(object target, Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType type)
        {
            m_privateObject = new Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject(target, type);
        }

        protected BaseAccessor(Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType type)
            :
                this(null, type)
        {
        }

        internal virtual object Target
        {
            get
            {
                return m_privateObject.Target;
            }
        }

        public override string ToString()
        {
            return this.Target.ToString();
        }

        public override bool Equals(object obj)
        {
            if (typeof(BaseAccessor).IsInstanceOfType(obj))
            {
                obj = ((BaseAccessor)(obj)).Target;
            }
            return this.Target.Equals(obj);
        }

        public override int GetHashCode()
        {
            return this.Target.GetHashCode();
        }
    }

使用派生自BaseAccessor的类

,例如

[System.Diagnostics.DebuggerStepThrough()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TestTools.UnitTestGeneration", "1.0.0.0")]
internal class SomeClassAccessor : BaseAccessor
{

    protected static Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType m_privateType = new Microsoft.VisualStudio.TestTools.UnitTesting.PrivateType(typeof(global::Namespace.SomeClass));

    internal SomeClassAccessor(global::Namespace.Someclass target)
        : base(target, m_privateType)
    {
    }

    internal static string STATIC_STRING
    {
        get
        {
            string ret = ((string)(m_privateType.GetStaticField("STATIC_STRING")));
            return ret;
        }
        set
        {
            m_privateType.SetStaticField("STATIC_STRING", value);
        }
    }

    internal int memberVar    {
        get
        {
            int ret = ((int)(m_privateObject.GetField("memberVar")));
            return ret;
        }
        set
        {
            m_privateObject.SetField("memberVar", value);
        }
    }

    internal int PrivateMethodName(int paramName)
    {
        object[] args = new object[] {
            paramName};
        int ret = (int)(m_privateObject.Invoke("PrivateMethodName", new System.Type[] {
                typeof(int)}, args)));
        return ret;
    }

在CodeProject上,有一篇文章简要讨论了测试私有方法的优缺点。然后它提供了一些反映代码来访问私有方法(类似于Marcus上面提供的代码。)我在样本中发现的唯一问题是代码没有考虑重载方法。

你可以在这里找到这篇文章:

http://www.codeproject.com/KB/cs/testnonpublicmembers.aspx

声明它们 internal ,然后使用 InternalsVisibleToAttribute 允许您的单元测试程序集查看它们。

我倾向于不使用编译器指令,因为它们很快就会混乱。如果你真的需要它们的一种缓解方法是将它们放在一个部分类中,让你的构建在制作生产版本时忽略该.cs文件。

您不应该首先测试代码的私有方法。您应该测试“公共接口”或API,即您的类的公共事物。 API是您向外部呼叫者公开的所有公共方法。

原因是,一旦开始测试类的私有方法和内部,就会将类的实现(私有事物)与测试结合起来。这意味着当您决定更改实施细节时,您还必须更改测试。

因此,您应该避免使用InternalsVisibleToAtrribute。

以下是Ian Cooper的精彩演讲,内容涉及这个主题: Ian Cooper:TDD,这一切都出了问题

我很惊讶没有人说过这个,但我采用的解决方案是在类中创建一个静态方法来测试自己。这使您可以访问公共和私人测试的所有内容。

此外,在脚本语言(具有OO功能,如Python,Ruby和PHP)中,您可以在运行时使文件自行测试。确保您的更改不会破坏任何内容的快捷方式。这显然是一个可扩展的解决方案来测试你的所有类:只需运行它们。 (您也可以使用其他语言执行此操作,其中void main也始终运行其测试。)

我想在这里创建一个清晰的代码示例,您可以在任何要测试私有方法的类上使用它。

在您的测试用例类中,只需包含这些方法,然后按指示使用它们。

  /**
   *
   * @var Class_name_of_class_you_want_to_test_private_methods_in
   * note: the actual class and the private variable to store the 
   * class instance in, should at least be different case so that
   * they do not get confused in the code.  Here the class name is
   * is upper case while the private instance variable is all lower
   * case
   */
  private $class_name_of_class_you_want_to_test_private_methods_in;

  /**
   * This uses reflection to be able to get private methods to test
   * @param $methodName
   * @return ReflectionMethod
   */
  protected static function getMethod($methodName) {
    $class = new ReflectionClass('Class_name_of_class_you_want_to_test_private_methods_in');
    $method = $class->getMethod($methodName);
    $method->setAccessible(true);
    return $method;
  }

  /**
   * Uses reflection class to call private methods and get return values.
   * @param $methodName
   * @param array $params
   * @return mixed
   *
   * usage:     $this->_callMethod('_someFunctionName', array(param1,param2,param3));
   *  {params are in
   *   order in which they appear in the function declaration}
   */
  protected function _callMethod($methodName, $params=array()) {
    $method = self::getMethod($methodName);
    return $method->invokeArgs($this->class_name_of_class_you_want_to_test_private_methods_in, $params);
  }

$ this-> _callMethod('_ someFunctionName',array(param1,param2,param3));

只需按照它们在原始私有函数中出现的顺序发出参数

对于任何想要运行私有方法的人而言,没有所有的烦恼和混乱。这适用于任何单元测试框架,只使用旧的反射。

public class ReflectionTools
{
    // If the class is non-static
    public static Object InvokePrivate(Object objectUnderTest, string method, params object[] args)
    {
        Type t = objectUnderTest.GetType();
        return t.InvokeMember(method,
            BindingFlags.InvokeMethod |
            BindingFlags.NonPublic |
            BindingFlags.Instance |
            BindingFlags.Static,
            null,
            objectUnderTest,
            args);
    }
    // if the class is static
    public static Object InvokePrivate(Type typeOfObjectUnderTest, string method, params object[] args)
    {
        MemberInfo[] members = typeOfObjectUnderTest.GetMembers(BindingFlags.NonPublic | BindingFlags.Static);
        foreach(var member in members)
        {
            if (member.Name == method)
            {
                return typeOfObjectUnderTest.InvokeMember(method, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.InvokeMethod, null, typeOfObjectUnderTest, args);
            }
        }
        return null;
    }
}

然后在您的实际测试中,您可以执行以下操作:

Assert.AreEqual( 
  ReflectionTools.InvokePrivate(
    typeof(StaticClassOfMethod), 
    "PrivateMethod"), 
  "Expected Result");

Assert.AreEqual( 
  ReflectionTools.InvokePrivate(
    new ClassOfMethod(), 
    "PrivateMethod"), 
  "Expected Result");

MbUnit为这个名为Reflector的包装器提供了一个很好的包装器。

Reflector dogReflector = new Reflector(new Dog());
dogReflector.Invoke("DreamAbout", DogDream.Food);

您还可以设置属性

并获取值
dogReflector.GetProperty("Age");

关于“测试私人”我同意......在完美的世界里。进行私人单元测试是没有意义的。但在现实世界中,您最终可能希望编写私有测试而不是重构代码。

这是很好的文章关于私人方法的单元测试。但是我不确定什么更好,让你专门为测试而设计的应用程序(就像创建测试仅用于测试)或使用反射进行测试。 我们大多数人都会选择第二种方式。

我使用 PrivateObject 类。但如前所述,最好避免测试私有方法。

Class target = new Class();
PrivateObject obj = new PrivateObject(target);
var retVal = obj.Invoke("PrivateMethod");
Assert.AreEqual(retVal);
CC -Dprivate=public

" CC"是我使用的系统上的命令行编译器。 -Dfoo = bar 相当于 #define foo bar 。因此,此编译选项有效地将所有私有内容更改为公共。

这是一个例子,首先是方法签名:

private string[] SplitInternal()
{
    return Regex.Matches(Format, @"([^/\[\]]|\[[^]]*\])+")
                        .Cast<Match>()
                        .Select(m => m.Value)
                        .Where(s => !string.IsNullOrEmpty(s))
                        .ToArray();
}

以下是测试:

/// <summary>
///A test for SplitInternal
///</summary>
[TestMethod()]
[DeploymentItem("Git XmlLib vs2008.dll")]
public void SplitInternalTest()
{
    string path = "pair[path/to/@Key={0}]/Items/Item[Name={1}]/Date";
    object[] values = new object[] { 2, "Martin" };
    XPathString xp = new XPathString(path, values);

    PrivateObject param0 = new PrivateObject(xp);
    XPathString_Accessor target = new XPathString_Accessor(param0);
    string[] expected = new string[] {
        "pair[path/to/@Key={0}]",
        "Items",
        "Item[Name={1}]",
        "Date"
    };
    string[] actual;
    actual = target.SplitInternal();
    CollectionAssert.AreEqual(expected, actual);
}

执行此操作的方法是让您的方法 protected 并编写一个继承您要测试的类的测试夹具。这样,您也不会将方法 public ,而是启用测试。

1)如果您有遗留代码,那么测试私有方法的唯一方法是通过反射。

2)如果是新代码,那么您有以下选择:

  • 使用反射(复杂)
  • 在同一类中编写单元测试(在其中也有测试代码使生产代码丑陋)
  • 重构并在某种 util 类中公开该方法
  • 使用@VisibleForTesting注释并删除私有

我更喜欢注释的方式,最简单,最不复杂。唯一的问题是我们提高了可见性,我认为这不是一个大问题。我们应该始终对接口进行编码,因此如果我们有一个接口 MyService 和一个实现 MyServiceImpl 那么我们就可以拥有相应的测试类,即 MyServiceTest (测试接口方法)和 MyServiceImplTest (测试私有方法)。无论如何,所有客户端都应该使用该接口,因此在某种程度上,即使私有方法的可见性已经增加,但这并不重要。

在调试模式下构建时,您也可以将其声明为public或internal(使用InternalsVisibleToAttribute):

    /// <summary>
    /// This Method is private.
    /// </summary>
#if DEBUG
    public
#else
    private
#endif
    static string MyPrivateMethod()
    {
        return "false";
    }

它使代码膨胀,但在发布版本中它将是 private

在我看来,你应该只测试你的classe的公共API。

将方法公之于众,为了对其进行单元测试,打破了封装暴露实现细节。

良好的公共API解决了客户端代码的直接目标,并完全解决了这一目标。

您可以从 Visual studio 2008 生成私有方法的测试方法。当您为私有方法创建单元测试时,测试引用文件夹将添加到您的测试项目中,并且访问器将添加到该文件夹​​中。单元测试方法的逻辑中也引用了访问器。此访问器允许您的单元测试调用您正在测试的代码中的私有方法。详细信息请查看

http://msdn.microsoft.com/en-us/library/bb385974.aspx

另请注意,InternalsVisibleToAtrribute要求程序集强名称,如果您正在使用之前没有该要求的解决方案,则会产生一系列问题。我使用访问器来测试私有方法。请参见此问题举个例子。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top