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

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个回答

361

如果你想对一个私有方法进行单元测试,那么可能存在问题。通常情况下,单元测试是用来测试类的接口,也就是它的公共(和受保护的)方法。当然,你可以通过“黑科技”来解决这个问题(即使只是将方法设置为公共方法),但是你也可以考虑以下几点:

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

44
选项2要求单元测试必须了解函数的底层实现,我不喜欢这样做。我通常认为单元测试应该在不假设任何实现细节的情况下测试函数。 - Herms
17
测试实现的缺点是,如果您对实现进行任何更改,测试将很容易发生故障。这是不可取的,因为在测试驱动开发中重构与编写测试同样重要。 - JtR
32
好的,如果你改变了实现,测试应该会出错。TDD意味着先更改测试。 - sleske
42
@sleske - 我不完全同意。如果功能没有改变,那么测试不应该会失败,因为测试应该真正测试行为/状态,而不是实现。这就是jtr所说的使测试变得脆弱的含义。在理想的情况下,您应该能够重构代码,并且仍然能够通过测试来验证您的重构没有改变系统的功能。 - Alconja
34
抱歉我的经验比较浅(对测试还不是很熟悉),但单元测试的理念不是测试每个代码模块吗?我不太明白为什么私有方法要被排除在外。 - OMill
显示剩余6条评论

127

89
呃,这会被编译进你发布的程序集中。 - Jay
16
在发布代码中,一个人是否可以在“InternalsVisibleTo”属性周围使用“#if DEBUG”来使其不适用? - mpontillo
21
@Mike,你可以这样做,但是你只能在调试代码上运行单元测试,无法在发布代码上运行。因为发布代码是经过优化的,可能会出现不同的行为和不同的时间。在多线程代码中,这意味着你的单元测试无法适当地检测到竞态条件。更好的方法是使用@AmazedSaint下面建议的反射或使用内置的PrivateObject/PrivateType。这允许你在Release构建中看到私有成员,假设你的测试环境以完全信任的方式运行(本地运行的MSTest就是这种情况)。 - Jay
127
我该注意什么?为什么这个回答会被接受,即使它实际上并没有回答关于测试私有方法的特定问题?InternalsVisibleTo 只会暴露标记为 internal 的方法,而不是像 OP 请求的那样标记为 private 的方法(这也是我来到这里的原因)。我猜我需要继续使用像 Seven 回答的 PrivateObject 吗? - Darren Lewis
6
@Jay 我知道可能有点晚了,但可以考虑像Mike建议的那样,在"InternalsVisibleTo"周围使用类似于"#if RELEASE_TEST"的东西,并创建一个定义"RELEASE_TEST"的发布构建配置的副本。这样你就可以使用优化测试你的发布代码,但当你真正进行发布时,测试将被省略。 - Shaz
显示剩余5条评论

122

测试私有方法可能并不常用。然而,我有时会从测试方法中调用私有方法,这样可以避免为测试数据生成编写重复代码...

Microsoft 提供了两种机制:

访问器(Accessors)

  • 转到类定义的源代码
  • 右键单击类名
  • 选择“创建私有访问器”
  • 选择应创建访问器的项目 => 最终你将得到一个名为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[] {/* ... */} );

3
你如何调用私有静态方法? - StuperUser
20
私有访问器在Visual Studio 2012中已被弃用。 - Ryan Gates
3
从2011年起,测试私有方法的访问器方法被弃用。详情请见:http://blogs.msdn.com/b/visualstudioalm/archive/2012/03/08/what-s-new-in-visual-studio-11-beta-unit-testing.aspx - Sanjit Misra
1
阅读Microsoft网站上的文档(在此处)我没有看到任何提到PrivateObject类被弃用的内容。我正在使用MSVS 2013,它按预期工作。 - stackunderflow
3
在你提供的网站上,第一种解决方案指出访问私有成员变量的方法是使用"PrivateObject"类来辅助在代码中访问内部和私有的API。该类可以在"Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll"程序集中找到。 - stackunderflow
私有对象(PrivateObject)方法在我的VS2017中运行良好。我没有尝试“私有访问器”(Private Accessor)方法,从其他评论中得知它现在可能已经过时了?(也许这个答案应该分成两个独立的答案!) - Jon Schneider

90
我不同意“只需测试外部接口”的理念。这有点像说汽车修理店只需测试轮子是否转动。是的,我最终关心的是外部行为,但我希望我的私人内部测试更加具体和直观。是的,如果我进行重构,可能需要更改一些测试,但除非它是大规模重构,否则我只需要更改少数测试,而其他(未更改的)内部测试仍然正常工作,这表明重构已成功。
您可以尝试仅使用公共接口覆盖所有内部情况,从理论上讲,可能可以通过使用公共接口完全测试每个内部方法(或至少是每个重要的方法),但您可能需要倒立来实现这一点,并且运行的测试用例与它们所设计测试的解决方案的内部部分之间的联系可能很难或不可能确定。拥有指向性强的单独测试,以保证内部机制正常工作,值得进行重构时进行的小型测试更改 - 至少这是我的经验。如果您必须针对每次重构进行巨大的测试更改,那么也许这并没有意义,但在这种情况下,也许您应该重新考虑整个设计。良好的设计应足够灵活,可以允许进行大多数更改而无需进行大规模重构。

21
很抱歉,我依然不同意你的观点。将每个组件视为黑盒可以在不出问题的情况下随时替换模块。如果你有一个需要执行XFooService,那么你关心的只是请求时它确实执行X。它如何执行并不重要。如果类中存在接口无法检测到的问题(不太可能),它仍然是一个有效的FooService。如果这是一个可通过接口看到的问题,公共成员的测试应该能够检测到它。整个重点应该是只要轮子正常运转,它就可以用作轮子。 - Basic
6
一个通用的方法是,如果您的内部逻辑足够复杂,以至于您觉得需要进行单元测试,那么也许它需要被提取到某种带有公共接口的辅助类中进行单元测试。然后,您的“父”类可以简单地利用这个辅助程序,每个人都可以适当地进行单元测试。 - Mir
12
@Basic:这个回答的逻辑完全错误。需要使用私有方法的经典情况是当您需要某些代码被公共方法重复使用时。您将此代码放入某个PrivMethod中。这个方法不应该暴露给公众,但需要测试以确保依赖于PrivMethod的公共方法确实可以依赖它。 - Dima
6
如果PrivMethod存在问题,那么调用PrivMethodPubMethod的测试应该能够暴露出来。当你将SimpleSmtpService更改为GmailService时会发生什么?突然间,你的私有测试指向的代码不再存在,或者可能工作方式不同并且会失败,尽管应用程序可能按设计完美运行。如果有适用于两个电子邮件发送器的复杂处理过程,也许应该放在一个名为EmailProcessor的地方,可以由两个服务共享,并进行单独测试。 - Basic
2
@miltonb 我们可能从不同的开发风格来看待这个问题。关于内部实现,我不倾向于进行单元测试。如果有问题(由接口测试确定),要么通过附加调试器轻松跟踪,要么该类过于复杂,应该拆分(并对新类的公共接口进行单元测试)在我看来。 - Basic
显示剩余7条评论

52

在我很少需要测试私有函数的情况下,我通常会将它们改为受保护状态,然后编写一个带有公共封装函数的子类。

该类:

...

protected void APrivateFunction()
{
    ...
}

...

测试用的子类:

...

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

...

2
你甚至可以将那个子类放在你的单元测试文件中,而不是弄乱真正的类。聪明的做法加一分。 - Tim Abell
如果可能的话,我总是把所有与测试相关的代码放在单元测试项目中。这只是伪代码。 - Jason Jackson
3
这个函数不是私有的,它是受保护的。结果是你让你的代码不够安全,暴露了私有功能给子类使用。 - War

21
我认为应该先问一个更根本的问题,那就是为什么你要首先尝试测试私有方法。这表明你正在尝试通过该类的公共接口来测试私有方法,而该方法是私有的,因为它是实现细节。人们只应该关注公共接口的行为,而不是它在底层的实现方式。
如果我想测试私有方法的行为,可以使用常见的重构方法,将其代码提取到另一个类中(可能具有包级别可见性,以确保它不是公共API的一部分)。然后我可以隔离地测试它的行为。
重构的产物意味着私有方法现在是一个单独的类,已经成为原始类的协作者。通过它自己的单元测试,人们会更加了解其行为。
然后我可以在尝试测试原始类时模拟其行为,这样我就可以集中精力测试该类的公共接口的行为,而不必测试所有私有方法和公共接口的行为。
我认为这就像开车一样。当我驾车时,我不会把引擎盖打开来确认引擎是否正常运转。我依赖于汽车提供的接口,即转速表和速度表,来了解引擎是否正常工作。我相信只要我按下油门,汽车就会动起来。如果我想测试引擎,我可以在隔离环境中进行检查。 :D
当然,直接测试私有方法可能是最后的选择,如果你拥有一个遗留应用程序,但我更希望重构遗留代码以实现更好的测试。迈克尔·菲瑟斯写了一本关于这个主题的好书。http://www.amazon.co.uk/Working-Effectively-Legacy-Robert-Martin/dp/0131177052

21
这个回答的逻辑完全错误。需要使用私有方法的典型情况是当你需要一些代码被公共方法重用时。你可以将这些代码放在一个私有方法中,这个方法不应该暴露给公众,但需要进行测试以确保依赖于 PrivMethod 的公共方法可以真正依赖它。 - Dima
在初始开发阶段这样做是有意义的,但你是否想在标准回归测试中包含私有方法的测试?如果是这样,如果实现发生更改,它可能会破坏测试套件。另一方面,如果你的回归测试仅关注外部可见的公共方法,那么如果私有方法后来出现问题,回归测试套件仍应该能够检测到错误。然后,如果必要,你可以重新使用旧的私有测试。 - Alex Blakemore
10
不同意,你应该只测试公共接口,否则为什么需要私有方法。在这种情况下,将它们全部公开并测试所有方法。如果你测试私有方法,那么你就会破坏封装性。如果你想测试一个私有方法,并且它被多个公共方法使用,那么它应该被移到自己的类中并进行隔离测试。然后,所有的公共方法都应该委托到这个新类中。通过这种方式,你仍然可以测试原始类的接口,并验证行为是否未发生改变,同时你还可以针对已委托的私有方法编写单独的测试。 - Big Kahuna
@Big Kahuna - 如果你认为没有必要对私有方法进行单元测试,那么你可能从未接触过大型/复杂项目。很多时候,像客户特定验证这样的公共函数最终会以20行代码的形式调用非常简单的私有方法,以使代码更易读,但你仍然需要测试每个单独的私有方法。如果测试公共函数20次,当单元测试失败时,将会非常难以调试。 - Pedro.The.Kid
1
我在一家富时100公司工作。我认为我在这段时间里见过几个复杂的项目,谢谢。如果您需要测试到那个级别,那么每个私有方法作为单独的协作者,应该被隔离地测试,因为它意味着它们具有需要测试的个体行为。主调解器对象的测试然后只成为一个交互测试。它只测试正确的策略是否被调用。您的情况听起来像是所讨论的类没有遵循SRP。它没有单一的变更原因,而是20个=> SRP违规。阅读GOOS书籍或Uncle Bob。YMWV - Big Kahuna

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

13

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

  1. you can create instance of PrivateObject class the syntax is as follows

    PrivateObject obj= new PrivateObject(PrivateClass);
    //now with this obj you can call the private method of PrivateCalss.
    obj.PrivateMethod("Parameters");
    
  2. You can use reflection.

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

回答不错,但是对于问题#1,你的语法是错误的。你需要先声明一个PrivateClass实例并使用它。https://dev59.com/wGox5IYBdhLWcg3wklCE - SharpC

10

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

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


我喜欢测试行为而不是实现的观点。如果将单元测试与实现(私有方法)绑定,那么测试将变得脆弱,并且需要在实现更改时进行修改。 - Bob Horn

9

提供一些示例,而不仅仅是给出链接。 - vinculis
看起来很丑。没有智能提示。微软这个解决方案真的糟糕。我震惊了! - alerya
测试私有静态方法的最简单方法 - Grant

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