498

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

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

4

31 回答 31

356

如果您想对私有方法进行单元测试,则可能有问题。单元测试(一般而言)旨在测试类的接口,即其公共(和受保护)方法。您当然可以“破解”解决方案(即使只是通过公开方法),但您可能还需要考虑:

  1. 如果您要测试的方法确实值得测试,那么将它移到自己的类中可能是值得的。
  2. 向调用私有方法的公共方法添加更多测试,测试私有方法的功能。(正如评论员所指出的,只有当这些私有方法的功能确实是公共接口的一部分时,您才应该这样做。如果它们实际上执行对用户隐藏的功能(即单元测试),这可能很糟糕)。
于 2008-10-30T15:54:26.683 回答
127

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

于 2008-10-30T15:52:37.793 回答
120

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

Microsoft 为此提供了两种机制:

访问器

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

但是,当涉及到更改原始类的接口时,该机制有时会有些棘手。所以,大多数时候我避免使用它。

PrivateObject 类 另一种方法是使用 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[] {/* ... */} );
于 2010-10-29T13:50:23.910 回答
82

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

您可以尝试仅使用公共接口覆盖所有内部案例,理论上可以完全使用公共接口测试每个内部方法(或至少每个重要的方法),但您可能最终不得不站在头上才能实现这以及通过公共接口运行的测试用例与它们设计用于测试的解决方案的内部部分之间的联系可能很难或无法辨别。已经指出,保证内部机器正常工作的个别测试非常值得重构带来的微小测试更改 - 至少这是我的经验。如果每次重构都必须对测试进行重大更改,那么这可能没有意义,但在这种情况下,也许您应该完全重新考虑您的设计。

于 2010-10-19T06:34:08.920 回答
52

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

班上:

...

protected void APrivateFunction()
{
    ...
}

...

用于测试的子类:

...

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

...
于 2008-10-30T16:04:04.777 回答
22

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

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

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

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

我认为这类似于驾驶汽车。当我开车时,我不会开着引擎盖开车,所以我可以看到引擎在工作。我依靠汽车提供的界面,即转速表和速度表来了解发动机是否在工作。我相信当我踩下油门踏板时汽车实际上会移动。如果我想测试引擎,我可以单独对其进行检查。:D

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

于 2012-10-24T16:30:01.230 回答
16

由于某种原因,私有类型、内部结构和私有成员都是如此,而且您通常不想直接与它们打交道。如果你这样做了,你以后很可能会崩溃,因为不能保证创建这些程序集的人会保留私有/内部实现。

但是,有时,在对已编译或第三方程序集进行一些破解/探索时,我自己最终想要初始化一个私有类或具有私有或内部构造函数的类。或者,有时,在处理我无法更改的预编译遗留库时——我最终会针对私有方法编写一些测试。

因此诞生了 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();
于 2010-05-25T07:44:15.870 回答
13

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

  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 });
    
于 2012-03-27T07:51:36.880 回答
10

我还使用了 InternalsVisibleToAttribute 方法。还值得一提的是,如果您对将以前的私有方法设置为内部方法来实现这一点感到不舒服,那么也许它们不应该成为直接单元测试的主题。

毕竟,您正在测试您的类的行为,而不是它的具体实现——您可以更改后者而不更改前者,并且您的测试仍然应该通过。

于 2008-10-30T15:55:36.773 回答
9

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

  1. 单元测试静态私有方法
  2. 单元测试非静态私有方法
于 2011-07-22T12:32:05.833 回答
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;
    }
于 2008-10-30T16:10:23.483 回答
5

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

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

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

于 2008-11-04T18:35:05.543 回答
4

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

于 2008-10-30T15:51:10.590 回答
4

我倾向于不使用编译器指令,因为它们很快就会使事情变得混乱。如果您确实需要它们,减轻它的一种方法是将它们放在部分类中,并让您的构建在制作生产版本时忽略该 .cs 文件。

于 2008-10-30T15:59:57.623 回答
4

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

原因是一旦你开始测试你的类的私有方法和内部结构,你就会将你的类的实现(私有的东西)耦合到你的测试中。这意味着当您决定更改实现细节时,您还必须更改测试。

出于这个原因,您应该避免使用 InternalsVisibleToAtrribute。

这是 Ian Cooper 的精彩演讲,涵盖了这个主题:Ian Cooper:TDD,哪里出了问题

于 2014-07-03T00:49:27.360 回答
3

有时,测试私有声明可能会很好。从根本上说,编译器只有一个公共方法:Compile(string outputFileName, params string[] sourceSFileNames)。我敢肯定你明白,如果不测试每个“隐藏”声明,就很难测试这样的方法!

这就是我们创建 Visual T#: 以简化测试的原因。它是一种免费的 .NET 编程语言(兼容 C# v2.0)。

我们添加了 '.-' 运算符。它的行为就像'。运算符,除非您还可以访问测试中的任何隐藏声明,而无需更改测试项目中的任何内容。

看看我们的网站:免费下载

于 2010-05-01T17:24:05.783 回答
3

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

此外,在脚本语言(具有 OO 能力,如 Python、Ruby 和 PHP)中,您可以让文件在运行时自行测试。确保您的更改不会破坏任何东西的好方法。这显然为测试所有类提供了一个可扩展的解决方案:只需运行它们。(你也可以在其他语言中使用 void main 来执行此操作,它也总是运行它的测试)。

于 2011-06-06T09:24:59.727 回答
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));

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

于 2015-07-16T19:34:25.740 回答
3

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

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");
于 2015-12-16T18:32:30.460 回答
2
CC -Dprivate=public

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

于 2008-10-31T07:44:44.207 回答
2

MbUnit 有一个很好的包装器,叫做 Reflector。

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

您还可以从属性中设置和获取值

dogReflector.GetProperty("Age");

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

于 2009-08-24T07:12:54.437 回答
2

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

于 2012-04-11T15:14:21.753 回答
2

在我看来,你应该只对你的类的公共 API 进行单元测试。

公开一个方法,以便对其进行单元测试,会破坏暴露实现细节的封装。

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

于 2016-11-26T14:33:43.570 回答
2

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

Class target = new Class();
PrivateObject obj = new PrivateObject(target);
var retVal = obj.Invoke("PrivateMethod");
Assert.AreEqual(retVal);
于 2017-04-21T11:57:22.870 回答
1

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

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);
}
于 2012-05-17T18:05:35.183 回答
1

一种方法是使用您的方法protected并编写一个测试夹具,该夹具继承您要测试的类。这样,您也不会转动您的方法public,而是启用测试。

于 2014-09-22T18:34:04.820 回答
1

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

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

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

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

于 2015-06-18T15:40:29.023 回答
1

您还可以在调试模式下构建时将其声明为公共或内部(使用 InternalsVisibleToAttribute):

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

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

于 2015-09-28T07:36:57.987 回答
0

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

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

于 2010-05-28T20:39:14.550 回答
0

另请注意,InternalsVisibleToAtrribute 要求您的程序集是强命名的,如果您正在使用以前没有该要求的解决方案,这会产生它自己的一组问题。我使用访问器来测试私有方法。有关此示例,请参阅此问题。

于 2011-04-12T20:08:57.847 回答
-1

对于JAVA语言

在这里,您可以使用模拟行为覆盖测试类的特定方法。

对于以下代码:

public class ClassToTest 
{
    public void methodToTest()
    {
        Integer integerInstance = new Integer(0);
        boolean returnValue= methodToMock(integerInstance);
        if(returnValue)
        {
            System.out.println("methodToMock returned true");
        }
        else
        {
            System.out.println("methodToMock returned true");
        }
        System.out.println();
    }
    private boolean methodToMock(int value)
    {
        return true;
    }
}

测试类将是:

public class ClassToTestTest{

    @Test
    public void testMethodToTest(){

        new Mockup<ClassToTest>(){
            @Mock
            private boolean methodToMock(int value){
                return true;
            }
        };

        ....    

    }
}
于 2018-01-29T19:35:56.853 回答