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

Eri*_*sky 470 .net tdd unit-testing private

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

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

Jer*_*ans 344

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

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

  • @sleske - 我不太同意.如果*功能*没有改变,那么测试就没有理由破坏,因为测试应该真的是测试行为/状态,而不是实现.这就是jtr对它的意义,使你的测试变得脆弱.在理想的世界中,您应该能够重构代码并使测试仍然通过,验证您的重构没有改变系统的功能. (39认同)
  • 选项2使单元测试必须了解函数的底层实现.我不喜欢那样做.我一般认为单元测试应该测试函数而不假设任何实现. (36认同)
  • 好吧,如果你改变实现,测试*应该*打破.TDD意味着首先改变测试. (30认同)
  • 抱歉天真(对测试的经验不多),但不是单元测试自己测试每个代码模块的想法?我真的不明白为什么应该从这个想法中排除私人方法. (25认同)
  • 测试实现的缺点是,如果对实现引入任何更改,测试将很容易中断.这是不可取的,因为重构与在TDD中编写测试一样重要. (15认同)
  • 我还使用单元测试作为一种测试工具 - 我可以非常快速地测试我的应用程序的一些部分,例如,创建一个GUI来测试它们.在这种情况下,我发现测试私有方法非常有用. (12认同)
  • 请考虑以下情形:一个类,包含业务逻辑,其中所有方法都是私有的,只有一个方法是公共的 - UpdateData(myType myObject).为什么暴露班级内部?局外人不应该知道更新中涉及的内容.我真的不想测试更新本身,但是在更新之前发生了数据转换和验证等.该类非常小,我无法证明使用公共方法创建单独的类,每个公式都包含一个验证,转换等方法.单元测试类的私有方法要简单得多. (5认同)
  • @sleske和@ alconja/jtr - 我认为你们两个都是通过break-sleske测试失败来表达不同的东西.alconja/jtr意味着编译失败.私有方法经历快速重构.那些了解私有方法的测试因此往往是脆弱的...因为你需要回过头来修改测试代码并改变实现.正义路径:)是测试不应该改变,除非需求/功能/行为改变 (4认同)
  • @OMill 问题在于“代码模块”是什么。这通常是一些实现接口的类。如果你开始掏空这个类并在其内部随意调用,你就不能指望它能够维持对一致状态的保证。另外,如果您有兴趣测试私有方法的行为,为什么要停在那里呢?谁来测试我们个人 LOC、语句或表达式的行为? (2认同)
  • 好的测试是自下而上的,而不是自上而下的。私有方法通常是需要自己测试的内部基础。而这些测试通常最好保存在另一个程序集中......因此问题。 (2认同)

TcK*_*cKs 120

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

  • 我错过了什么?当它实际上没有回答测试私有方法的具体问题时,为什么这是可接受的答案?InternalsVisibleTo只暴露标记为内部的方法,而不是OP请求的那些标记为私有的方法(以及我在此处登陆的原因).我想我需要继续使用PrivateObject作为Seven的回答? (120认同)
  • 呸.这会被编译到已发布的程序集中. (86认同)
  • @Mike,你可以,但是你只能在调试代码上运行单元测试,而不是发布代码.由于版本代码已经过优化,您可能会看到不同的行为和不同的时间.在多线程代码中,这意味着您的单元测试将无法正确检测竞争条件.更好的方法是通过@ AmazedSaint的建议使用反射或使用内置的PrivateObject/PrivateType.这允许您在Release版本中查看私有化,假设您的测试工具以完全信任的方式运行(MSTest在本地运行) (20认同)
  • @Jay - 无法在`InternalsVisibleTo`属性周围使用`#if DEBUG`使其不适用于发布代码? (14认同)
  • @Jay我知道这有点晚了,但是有一个选择就是像Mike建议的那样在`InternalsVisibleTo`周围使用`#if RELEASE_TEST`之类的东西,并制作一个定义`RELEASE_TEST`的发布构建配置的副本.您可以通过优化来测试发布代码,但是当您实际构建发布时,将省略您的测试. (5认同)
  • @Jay - 请原谅我的无知,但是这个属性被编译到你发布的程序集中有什么不好? (4认同)

Sev*_*ven 117

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

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[] {/* ... */} );
Run Code Online (Sandbox Code Playgroud)

  • 私有访问器[在Visual Studio 2012中已弃用](http://msdn.microsoft.com/en-us/library/vstudio/hh507838.aspx). (17认同)
  • 从VS 2011开始,不推荐使用测试私有方法的访问器方法.http://blogs.msdn.com/b/visualstudioalm/archive/2012/03/08/what-s-new-in-visual-studio-11-beta-unit-testing.aspx (3认同)
  • @RyanGates您引用的站点上的第一个解决方案表明访问私有成员的解决方案是**"使用PrivateObject类来帮助访问代码中的内部和私有API.这可以在Microsoft.VisualStudio.QualityTools中找到. UnitTestFramework.dll程序集."** (3认同)
  • 您如何调用私有静态方法? (2认同)

Dar*_*ank 78

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

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

  • 我担心我仍然不同意你的观点.将每个组件作为黑盒子处理,可以毫无问题地交换模块.如果你有一个必须做`X`的`FooService`,你应该关心的是它确实在请求时确实做了`X`._How_它应该没关系.如果通过接口无法识别类中的问题(不太可能),它仍然是有效的`FooService`.如果这是通过界面可见的问题,那么公共成员的测试应该检测到它.整点应该是只要车轮转动正常,它就可以用作车轮. (19认同)
  • @Basic:这个答案完全错误的逻辑.需要私有方法的经典案例是当您需要公共方法重用某些代码时.您将此代码放入某些PrivMethod.这种方法不应暴露给公众,但需要进行测试以确保依赖于PrivMethod的公共方法能够真正依赖它. (9认同)
  • @Dima当然,如果`PrivMethod`出现问题,那么调用`PrivMethod`的`PubMethod`的测试应该公开它?将`SimpleSmtpService`更改为`GmailService`时会发生什么?突然之间,您的私有测试指向不再存在的代码,或者可能以不同的方式运行并且会失败,即使应用程序可能按设计完美运行​​.如果有适用于两个电子邮件发件人的复杂处理,也许它应该在一个'EmailProcessor`中,可以由两者使用并单独测试? (6认同)
  • 一种通用的方法是,如果你的内部逻辑足够复杂,你觉得它需要单元测试,也许它需要被提取到某种辅助类中,它有一个可以进行单元测试的公共接口.然后你的"父"类可以简单地使用这个帮助器,每个人都可以进行适当的单元测试. (5认同)
  • @miltonb我们可能会从不同的开发风格中看到这一点.WRT内部,我不倾向于单元测试它们.如果出现问题(通过接口测试确定),可以通过附加调试器轻松跟踪,或者类太复杂而且应该拆分(使用新类单元的公共接口测试)恕我直言 (2认同)

Jas*_*son 50

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

班级:

...

protected void APrivateFunction()
{
    ...
}

...
Run Code Online (Sandbox Code Playgroud)

用于测试的子类:

...

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

...
Run Code Online (Sandbox Code Playgroud)

  • 您甚至可以将该子类放入单元测试文件中,而不是弄乱真正的类。狡猾+1。 (2认同)
  • 此功能不是私有的,受保护的,最终结果......您使代码不那么安全/暴露给子类型私有功能 (2认同)

Big*_*una 21

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 whereas that method is private for a reason as it's an implementation detail. One should only be concerned with the behaviour of the public interface not on how it's implemented under the covers.

If I want to test the behaviour of the private method, by using common refactorings, I can extract its code into another class (maybe with package level visibility so ensure it's not part of a public API). I can then test its behaviour in isolation.

The product of the refactoring means that private method is now a separate class that has become a collaborator to the original class. Its behaviour will have become well understood via its own unit tests.

I can then mock its behaviour when I try to test the original class so that I can then 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.

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

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

  • 这个答案完全错误的逻辑.需要私有方法的经典案例是当您需要公共方法重用某些代码时.您将此代码放入某些PrivMethod.这种方法不应暴露给公众,但需要进行测试以确保依赖于PrivMethod的公共方法能够真正依赖它. (16认同)
  • 不同意,你应该只测试公共接口,否则为什么需要私有方法.在这种情况下让他们全部公开并测试所有这些.如果你正在测试私人方法,那么你就会破坏血液循环.如果你想测试一个私有方法并且它在多个公共方法中使用,那么它应该被移动到它自己的类并进行单独测试.所有公共方法应该委托给那个新类.这样你仍然有测试原始类的接口,您可以验证行为没有更改,并且您对委派的私有方法有单独的测试. (9认同)

ama*_*int 17

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

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

因此诞生了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();
Run Code Online (Sandbox Code Playgroud)


Unk*_*own 12

那么你可以用两种方式对私有方法进行单元测试

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

    PrivateObject obj= new PrivateObject(PrivateClass);
    //now with this obj you can call the private method of PrivateCalss.
    obj.PrivateMethod("Parameters");
    
    Run Code Online (Sandbox Code Playgroud)
  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 });
    
    Run Code Online (Sandbox Code Playgroud)


phi*_*red 10

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

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


小智 9

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

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


Mar*_*ing 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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

使用派生自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;
    }
Run Code Online (Sandbox Code Playgroud)

  • gen'd文件仅存在于VS2005中.2008年,他们在幕后制作.他们是一个戒律.并且关联的Shadow任务在构建服务器上是不稳定的. (8认同)

Ped*_*dro 5

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

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

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


Eri*_*one 5

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

Assert.AreEqual( 
  ReflectionTools.InvokePrivate(
    new ClassOfMethod(), 
    "PrivateMethod"), 
  "Expected Result");
Run Code Online (Sandbox Code Playgroud)