如何对私有方法进行单元测试?

510

我正在构建一个类库,其中包含一些公共和私有方法。我希望能够单元测试私有方法(主要是在开发过程中,但也可能对未来的重构有用)。

正确的操作方式是什么?


3
也许是我漏掉了什么,或者可能只是因为这个问题在互联网年代上已经过时了,但是现在对私有方法进行单元测试变得非常容易和直截了当,使用Visual Studio可以在需要时生成必要的访问器类,并将测试逻辑预填充为接近所需用于简单功能测试的代码段。请参阅例如http://msdn.microsoft.com/en-us/library/ms184807%28VS.90%29.aspx - mjv
5
这似乎是与 https://dev59.com/bHVD5IYBdhLWcg3wRpaX 相近的内容。 - Raedwald
6
不要对内部单元进行单元测试:http://blog.ploeh.dk/2015/09/22/unit-testing-internals - Mark Seemann
自 2012 年起,Visual Studio 已将私有访问器(Private Accessors)列为弃用项。 - Blaine DeLancey
显示剩余3条评论
32个回答

8

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;
    }

8
生成的文件只存在于VS2005中。在2008中,它们是在背景中自动生成的。它们是可憎的。而且相关的Shadow任务在构建服务器上不太稳定。 - Ruben Bartelink
在VS2012-2013中,访问器已被弃用。 - Zephan Schroeder

5

对于任何想要在没有烦恼和杂乱的情况下运行私有方法的人,这个方法可以与使用旧式反射 (Reflection) 的任何单元测试框架配合使用。

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");

5
在CodeProject上,有一篇文章简要讨论了测试私有方法的优缺点。它随后提供了一些反射代码来访问私有方法(类似于Marcus上面提供的代码)。我发现样例唯一的问题是代码没有考虑重载方法。
您可以在这里找到这篇文章:http://www.codeproject.com/KB/cs/testnonpublicmembers.aspx

4

14
我不喜欢使用InternalsVisibleTo,因为我之前有意将这个方法设为私有。 - swilliams

4

我不太喜欢使用编译器指令,因为它们会很快地使事情变得混乱。如果您真的需要使用它们,可以将它们放在部分类中,并在生成产品版本时让构建过程忽略该.cs文件以减轻影响。


你会在生产版本中包含测试访问器(以测试编译器优化等),但在发布版本中排除它们。但我在挑刺,无论如何我还是点了赞,因为我认为把那些东西放在一个地方是个好主意。感谢这个想法。 - CAD bloke

3
有时,测试私有声明可能是有益的。 基本上,编译器只有一个公共方法:Compile(string outputFileName,params string [] sourceSFileNames)。我相信你明白,在不测试每个“隐藏”声明的情况下,要测试这样的方法将会很困难!
这就是为什么我们创建了Visual T#:以使测试更加容易。它是一个免费的.NET编程语言(与C# v2.0兼容)。
我们添加了'.-'运算符。它的行为与'.'运算符相同,除了您可以在不更改测试项目中的任何内容的情况下访问任何隐藏声明。
请访问我们的网站:下载它,免费

3
我很惊讶还没有人提到这一点,但我采用的解决方案是在类内部创建一个静态方法来进行测试。这样可以访问所有公共和私有属性进行测试。
此外,在脚本语言(具有面向对象能力的语言,如Python、Ruby和PHP)中,您可以使文件在运行时自行测试。这是确保更改不会破坏任何内容的快速方法。这显然是测试所有类的可扩展解决方案:只需运行它们全部即可。(您还可以在其他语言中使用void main来始终运行其测试)。

1
虽然这种方法很实用,但并不十分优雅。这可能会在代码库中造成一些混乱,也无法将测试与真正的代码分离开来。外部测试的能力可以打开脚本自动化测试的能力,而不是手动编写静态方法。 - Darren Reid
这并不排除你在外部进行测试...只需按照自己的方式调用静态方法即可。代码库也不会混乱...你可以根据方法的名称来命名。我使用"runTests",但任何类似的名称都可以。 - rube
你是对的,这并不排除进行外部测试的可能性,但这会产生更多的代码,即使让代码库变得混乱。每个类都可能有很多私有方法需要测试,这些方法在一个或多个构造函数中初始化其变量。要进行测试,你将不得不编写与需要测试的方法数量至少相同的静态方法,并且测试方法可能需要大量初始化正确的值。这将使代码的维护更加困难。正如其他人所说,测试类的行为是更好的方法,其余部分应该足够小,以便进行调试。 - Darren Reid
我使用与其他人相同的行数进行测试(实际上更少,因为您稍后将会看到)。您不必测试所有私有方法。只需测试需要测试的那些 :) 您也不需要在单独的方法中测试每个方法。我通过一个调用来完成。这实际上使代码的维护更加容易,因为我的所有类都具有相同的总体单元测试方法,该方法逐行运行所有私有和受保护的单元测试。然后,整个测试工具箱在所有我的类上调用同一方法,并且所有维护都驻留在我的类内 - 包括测试。 - rube
如果您建议在要测试的代码/程序集中添加单元测试代码,那么这是一个坏主意,原因有很多,其中最小的风险是安全问题。 - Dave Black

3
首先,你不应该测试你的代码私有方法。你应该测试“公共接口”或API,即类的公共部分。API是你向外部调用方公开的所有公共方法。
原因是一旦你开始测试类的私有方法和内部细节,就会将类的实现(私有内容)与测试耦合在一起。这意味着当你决定更改实现细节时,你也必须更改测试。
因此,你应该避免使用InternalsVisibleToAttribute。
以下是Ian Cooper的一个很棒的演讲,涉及了这个主题:Ian Cooper: TDD, where did it all go wrong

3
CC -Dprivate=public

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


2
这是什么?这适用于Visual Studio吗? - YeahStu
1
在我使用的系统上,“CC”是命令行编译器。 “-Dfoo = bar” 相当于“#define foo bar”。因此,这个编译选项有效地将所有私有内容更改为公共内容。 哈哈! - Mark Harrison
在Visual Studio中,设置构建环境中的一个定义。 - Mark Harrison

3

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

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

  /**
   *
   * @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));

只需按照原始私有函数中参数的顺序发出参数即可。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接