关于.net:你如何对私有方法进行单元测试?

How do you unit test private methods?

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

正确的方法是什么?


如果要对私有方法进行单元测试,可能会出现问题。单元测试(一般来说)是用来测试类的接口,这意味着它是公共(和受保护)方法。当然,您可以"黑客"解决方案(即使只是通过公开方法),但您也可能需要考虑:

  • 如果您想要测试的方法确实值得测试,那么将其转移到自己的类中可能是值得的。
  • 向调用私有方法的公共方法添加更多测试,测试私有方法的功能。(正如评论员所指出的,只有当这些私有方法的功能确实是公共接口的一部分时,才应该这样做。如果它们实际上执行了对用户隐藏的功能(即单元测试),那么这可能是不好的。

  • 如果使用.NET,则应使用InternalsVisibleToAttribute。


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

    微软为此提供了两种机制:

    访问器

    • 转到类定义的源代码
    • 右键单击类的名称
    • 选择"创建专用访问器"
    • 选择应在其中创建访问器的项目=>您将得到一个名为foo_accessor的新类。这个类将在编译期间动态生成,并将所有公共成员都私有化。

    然而,当涉及到原始类接口的更改时,这种机制有时有点难处理。所以,大多数时候我都避免使用这个。

    privateObject类另一种方法是使用Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject

    1
    2
    3
    4
    5
    6
    7
    8
    // 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[] {/* ... */} );


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

    您可以尝试只使用公共接口来覆盖所有内部案例,理论上可以完全使用公共接口来测试每个内部方法(或者至少每个重要的方法),但最终您可能不得不站在您的头上才能实现这一点,并且测试案例之间的连接是通过公共接口运行的。Erface及其设计用于测试的解决方案的内部部分可能难以或不可能识别。已经指出,确保内部机器正常工作的个别测试非常值得重构所带来的微小的测试更改——至少这是我的经验。如果您必须对每个重构的测试进行巨大的更改,那么这可能没有意义,但是在这种情况下,您可能应该重新考虑您的设计。一个好的设计应该足够灵活,可以在不进行大规模重新设计的情况下进行大多数更改。


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

    班级:

    1
    2
    3
    4
    5
    6
    7
    8
    ...

    protected void APrivateFunction()
    {
        ...
    }

    ...

    测试子类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ...

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

    ...


    (P)I think a more fundamental question should be asked is that why are you trying to test the private method in the first place.That is a code smell that you're trying to test the private method through that class'public interface where that method is private for a reason a s it's an implementation detail.其中之一只应与公共界面的行为有关,而不是与《公约》之下的执行情况有关。(p)(P)如果我想通过使用共同refactorings检验私人方法的行为,我可以将其法典摘录到其他分类中(或许可以带上包装级别的粘贴性,以确保它不是公共投资政策审查的一部分)。I can them test its behaviour in isolation.(p)(P)The product of the refactoring means that private method is now a separate class that has become a collaborator to the original class.他们的行为将通过他们自己的测试来证明。(p)(P)I can them mock its behaviour when I try to test the original class so that I can them concentrate on test the behaviour of that class'public interface rather than having to test a combinatorial explosion of the public interface and the behaviour of all its private methods.(p)(P)I see this analogous to driving a car.When I drive a car I don't drive with the bonnet up so I can see the engine is working.I rely on the interface the car provides,namely the rev counter and the speedometer to know the engine is working.I rely on the fact that the car actually moves when I press the gas pedal.如果我想测试引擎,我可以做检查在异位。页:1(p)(P)of course testing private methods directly may be a last resort if you have a legalcy application but I would prefer that legalcy code is refactored to enable better testing.Michael Feathers has written a great book on this very subject.http://www.amazon.co.uk/working-effectively-legalcy-Robert-Martin/DP/0131177052(p)


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

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

    由此诞生了accessprivatewrapper——http://amazedsaint.blogspot.com/2010/05/accessprivatewrapper-c-40-dynamic.html——它是一个快速的wrapper类,可以让使用C 4.0动态特性和反射的工作变得容易。

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

    1
    2
    3
    4
    5
    6
        //Note that the wrapper is dynamic
        dynamic wrapper = AccessPrivateWrapper.FromType
            (typeof(SomeKnownClass).Assembly,"ClassWithPrivateConstructor");

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

    (P)你可以用两种方法测试私人方法(p)

  • (P)You can create instance of EDOCX1 theocx1 genital 0 sill class the syntax is as follows(p)字母名称
  • (P)You can use reflection.(p)字母名称

  • 我还使用了InternalsVisibleToAttribute方法。同样值得一提的是,如果为了实现这一点而让您以前的私有方法内部化感到不舒服,那么也许它们无论如何都不应该是直接单元测试的主题。

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


    (P)There are 2 types of private methods.Static private methods and non static private methods(instance methods).The following 2 articles explain how to unit test private methods with examples.(p)

  • Unit testing static private methods
  • Unit testing non static private methods

  • MS测试有一个很好的内置功能,通过创建一个名为vscodegenaccessors的文件,使私有成员和方法在项目中可用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    [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派生的类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    [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;
        }


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

    你可以在这里找到文章:

    http://www.codeproject.com/kb/cs/testnopublicmembers.aspx


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


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


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

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

    因此,应避免使用InternalsVisibleToAttribute。

    下面是伊恩库珀的一篇精彩的演讲,内容涵盖了这个主题:伊恩库珀:TDD,这一切都出了什么问题?


    有时候,测试私有声明可能会很好。基本上,编译器只有一个公共方法:compile(string outputfilename,params string[]sourcesfilename)。我相信您理解,如果不测试每个"隐藏"的声明,就很难测试这种方法!

    这就是为什么我们已经创建了可视化T:以便进行更简单的测试。它是一种免费的.NET编程语言(兼容C v2.0)。

    我们增加了".-"接线员。它的行为类似于"."运算符,但您也可以从测试中访问任何隐藏的声明,而无需更改测试项目中的任何内容。

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


    (P)I'm surprised nobody has said this yet,but a solution I have employed is to make a static method inside the class to test itself.This gives you access to everything public and private to test with.(p)(P)Furthermore,in a scripting language(with oo ability,like Python,ruby and php),you can make the file test itself when run.使你的变化成为现实的一条快车道,没有打破任何东西。这一点使得一个scalable solution to testing all your classes:Just run them all.(You can also do this in other languages with a void main which always runs its tests as well).(p)


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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
      /**
       *
       * @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->u callmethod(''u somefunctionname',array(param1,param2,param3));

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


    对于任何一个想要运行私有方法而不需要所有的fess和mess的人来说。这适用于任何只使用良好的旧反射的单元测试框架。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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;
        }
    }

    然后在实际测试中,您可以这样做:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Assert.AreEqual(
      ReflectionTools.InvokePrivate(
        typeof(StaticClassOfMethod),
       "PrivateMethod"),
     "Expected Result");

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


    (P)这是一项很好的条款,涉及到对私人方法的检验。但我并不确定什么是最好的,以使你的应用指定具体的测试(它像为测试而创建的测试)或使用思考的测试。我们最美的地方就是第二条路(p)


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

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

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

    1
    dogReflector.GetProperty("Age");

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


    1
    CC -Dprivate=public

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


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

    1
    2
    3
    4
    Class target = new Class();
    PrivateObject obj = new PrivateObject(target);
    var retVal = obj.Invoke("PrivateMethod");
    Assert.AreEqual(retVal);

    (P)这里是一个例子,首先是方法标志:(p)字母名称(P)这里的测试:(p)字母名称


    这样做的一种方法是让方法protected并编写一个继承要测试的类的测试夹具。这样,您就不会改变方法public,但是您启用了测试。


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

    2)如果是新代码,则您有以下选项:

    • 使用反射(到复杂)
    • 在同一类中编写单元测试(通过测试代码也在其中)
    • 重构并使该方法在某种类型的Util类中公开
    • 使用@visiblefortesting注释并删除private

    我更喜欢注释方法,既简单又不复杂。唯一的问题是我们提高了能见度,我认为这不是一个大问题。我们应该一直对接口进行编码,所以如果我们有一个接口myservice和一个实现myserviceimpl,那么我们可以有相应的测试类,即myservice test(测试接口方法)和myserviceimpletest(测试私有方法)。所有客户机无论如何都应该使用该接口,因此在某种程度上,即使私有方法的可见性有所提高,它也不应该真正重要。


    在调试模式下生成时,还可以将其声明为public或internal(带有internalsVisibleToAttribute):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        /// <summary>
        /// This Method is private.
        /// </summary>
    #if DEBUG
        public
    #else
        private
    #endif
        static string MyPrivateMethod()
        {
            return"false";
        }

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


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

    使一个方法成为公共的,为了对其进行单元测试,会破坏封装,公开实现细节。

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


    Java语言

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

    对于以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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;
        }
    }

    测试等级为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class ClassToTestTest{

        @Test
        public void testMethodToTest(){

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

            ....    

        }
    }

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

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


    (P)Also note that the internal swisible toatrribute has a requirement that your Assembly be strong named,which creates it's own set of problems if you're working in a solution that had not that requirement before.I use the accessor to test private methods.See this question that for an example of this.(p)