公开私有方法对其进行单元测试......好主意?

jcv*_*dan 290 c# java unit-testing

主持人注意: 这里已经发布了39个答案(其中一些已被删除). 在您发布答案之前,请考虑是否可以在讨论中添加有意义的内容.你很可能只是重复别人已经说过的话.


我偶尔发现自己需要在公共类中创建一个私有方法,只是为它编写一些单元测试.

通常这是因为该方法包含在类中的其他方法之间共享的逻辑,并且它自己测试逻辑更加整洁,或者另一个原因可能是我想测试同步线程中使用的逻辑而不必担心线程问题.

其他人发现自己这样做,因为我真的不喜欢这样做吗?我个人认为奖金超过了公开方法的问题,并没有真正提供课外的任何服务......

UPDATE

感谢大家的回答,似乎激起了人们的兴趣.我认为普遍的共识是测试应该通过公共API进行,因为这是一个类将被使用的唯一方式,我同意这一点.我上面提到过的几个我上面提到的案例都是不常见的案例,我认为这样做的好处是值得的.

但是,我可以看到每个人都指出它永远不应该发生.当我考虑更多时,我认为更改代码以适应测试是一个坏主意 - 毕竟我认为测试是一种支持工具,并且如果你愿意,将系统更改为"支持支持工具",则是公然的糟糕的做法.

Mur*_*nik 183

注意:
这个答案最初针对问题发布的.单独测试单独测试是否是通过getter公开私有实例变量的一个很好的理由?它被合并到这个中,所以它可能对那里提供的用例有点特别.

作为一般性陈述,我通常都会重构"生产"代码,以便更容易测试.但是,我认为这不是一个好的电话.一个好的单元测试(通常)不应该关心类的实现细节,只关心它的可见行为.相反,露出内部书库测试,可以测试类返回你期望它调用该命令后,页面first()last().

例如,考虑这个伪代码:

public class NavigationTest {
    private Navigation nav;

    @Before
    public void setUp() {
        // Set up nav so the order is page1->page2->page3 and
        // we've moved back to page2
        nav = ...;
    }

    @Test
    public void testFirst() {
        nav.first();

        assertEquals("page1", nav.getPage());

        nav.next();
        assertEquals("page2", nav.getPage());

        nav.next();
        assertEquals("page3", nav.getPage());
    }

    @Test
    public void testLast() {
        nav.last();

        assertEquals("page3", nav.getPage());

        nav.previous();
        assertEquals("page2", nav.getPage());

        nav.previous();
        assertEquals("page1", nav.getPage());
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我完全同意你的回答.许多开发人员都倾向于使用默认访问修饰符,并证明良好的开源框架使用此策略,因此它必须是正确的.只是因为你不能意味着你应该这样做.+1 (7认同)
  • 虽然其他人提供了有用的见解,但我不得不说这是我认为最适合这个问题的解决方案.它使测试完全不知道如何在内部实现预期的行为.+10! (6认同)

bla*_*ank 149

就个人而言,我宁愿使用公共API进行单元测试,而且我绝对不会公开私有方法只是为了让它易于测试.

如果你真的想要单独测试私有方法,那么在Java中你可以使用Easymock/Powermock来实现这一点.

你必须务实,并且你也应该意识到难以测试的原因.

" 听取测试 " - 如果难以测试,是否告诉你有关你设计的事情?您是否可以通过公共API测试对这种方法的测试是否微不足道并轻松覆盖?

以下是Michael Feathers在" 有效使用遗留代码 "中所说的内容

"很多人花了很多时间试图弄清楚如何解决这个问题......真正的答案是,如果你有测试私有方法的冲动,那么这个方法不应该是私有的;如果将方法公之于众困扰你,很有可能,因为它是一个单独的责任的一部分;它应该在另一个阶级." [ M. Feathers 有效处理遗产守则(2005年)]

  • +1用于"收听测试".如果您觉得需要测试私有方法,那么该方法中的逻辑很复杂,应该在一个单独的帮助器类中._Then_你可以测试助手类. (49认同)
  • 为Easymock/Powermock +1,永远不要公开私有方法 (5认同)
  • "倾听测试"似乎已经失败了.这是一个缓存链接:http://webcache.googleusercontent.com/search?q = cache = 6s3cEcf3oAIJ:static.mockobjects.com/labels/listening%2520to%2520the%2520tests.html+http://static.mockobjects. COM /标签/收听%2520to%2520the%2520tests.html&HL = EN&GL = AU&条= 1 (2认同)

Eri*_*ert 64

正如其他人所说,有点怀疑是对私人方法进行单元测试; 单元测试公共接口,而不是私有实现细节.

也就是说,当我想要对C#中的私有内容进行单元测试时,我使用的技术是将可访问性保护从私有降级到内部,然后使用InternalsVisibleTo将单元测试程序集标记为友元程序集.然后,单元测试组件将被允许将内部处理为公开,但您不必担心意外添加到公共表面区域.


Blu*_*eft 43

很多答案建议只测试公共界面,但恕我直言这是不现实的 - 如果一个方法做了5步的事情,你会想要分别测试这五个步骤,而不是一起测试.这需要测试所有五种方法,否则这些方法(除了测试之外)private.

测试"私有"方法的常用方法是为每个类提供自己的接口,并制作"私有"方法public,但不要在接口中包含它们.这样,它们仍然可以进行测试,但它们不会破坏界面.

是的,这将导致文件和类膨胀.

是的,这确实使得publicprivate说明符变得冗余.

是的,这是屁股的痛苦.

不幸的是,这是我们为使代码可测试而做出的众多牺牲之一.未来的语言(或者甚至是未来的C#/ Java版本)可能具有使类和模块可测试性更加方便的功能; 但与此同时,我们必须跳过这些箍.


有些人会争辩说这些步骤中的每一步都应该是它自己的类,但我不同意 - 如果它们都共享状态,则没有理由创建五个单独的类,其中有五种方法可以.更糟糕的是,这导致文件和类膨胀. 此外,它会感染你的模块的公共API -所有这些类必然是public,如果你想从另一个模块测试他们(可能是,或包括在同一模块中的测试代码,这意味着航运与您的产品测试代码).

  • +1:对我来说是最好的答案.如果汽车制造商出于同样的原因没有测试其部分车型,我不确定有人会买它.即使我们应该将代码更改为公共代码,也应该测试代码的重要部分. (7认同)
  • 上半场很好的答案 (2认同)

kan*_*kan 27

单元测试应测试公共合同,这是在代码的其他部分中如何使用类的唯一方法.私有方法是实现细节,您不应该测试它,只要公共API正常工作,实现无关紧要,并且可以在不更改测试用例的情况下进行更改.

  • 这是有缺陷的逻辑.单元测试的目的是尽早发现错误.通过不测试私有方法,您将在测试中留下空白,直到很久以后才能发现.在私有方法很复杂且公共接口不易执行的情况下,应对其进行单元测试. (11认同)
  • @Casey:如果私有方法没有被公共接口行使,为什么会出现? (5认同)
  • @DaSilva_Ireland:以后很难维护测试用例。唯一真正的类用例是通过公共方法。即所有其他代码只能通过公共 API 使用该类。所以,如果你想改变实现和私有方法测试用例失败,这并不意味着你做错了什么,而且,如果你只测试私有,而不是完全测试公共,它不会覆盖一些潜在的错误. 理想情况下,测试用例应涵盖所有可能的情况,并且仅涵盖类实际使用的情况。 (2认同)

Sim*_*mY4 21

IMO,您应该编写测试,而不是对您的类在内部实现的方式做出深刻的假设.您可能希望稍后使用另一个内部模型重构它,但仍然提供先前实现所提供的相同保证.

记住这一点我建议你专注于测试你的合同是否仍然存在,无论你的班级目前有什么内部实施.基于属性的公共API测试.

  • 我同意在重构代码之后不需要重写的单元测试更好.而且不仅仅是因为你花时间重写单元测试.一个更重要的原因是我认为单元测试最重要的目的是在重构后代码仍能正常运行.如果您在重构后必须重写所有单元测试,那么您将失去单元测试的好处. (5认同)

Thi*_*ilo 20

把它打包私有怎么样?然后您的测试代码可以看到它(以及您的包中的其他类),但它仍然对您的用户隐藏.

但实际上,你不应该测试私有方法.这些是实施细节,而不是合同的一部分.他们所做的一切都应该通过调用公共方法来解决(如果他们的代码在那里没有被公共方法行使,那么应该去).如果私有代码太复杂,那么该类可能做了太多事情并且不需要重构.

将方法公之于众是一项重大承诺.一旦你这样做,人们将能够使用它,你不能再只是改变它们.


Fin*_*las 17

更新:我在其他许多地方为这个问题添加了更广泛和更完整的答案.这可以在我的博客上找到.

如果我需要公开测试它,通常会暗示被测系统不遵循单一责任原则.因此,应该引入一个缺少的类.将代码提取到新类后,将其公开.现在您可以轻松测试,并且您正在关注SRP.你的其他类只需要通过合成调用这个新类.

公开方法/使用langauge技巧,例如将代码标记为可见以测试组合应始终是最后的手段.

例如:

public class SystemUnderTest
{
   public void DoStuff()
   {
      // Blah
      // Call Validate()
   }

   private void Validate()
   {
      // Several lines of complex code...
   }
}
Run Code Online (Sandbox Code Playgroud)

通过引入验证器对象来重构它.

public class SystemUnderTest
{
    public void DoStuff()
    {
       // Blah
       validator.Invoke(..)
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我们要做的就是测试验证器是否被正确调用.实际的验证过程(以前的私有逻辑)可以在纯粹的隔离中进行测试.不需要复杂的测试设置来确保验证通过.

  • @dormisher,关于您的帐户服务,将SRP视为"改变的唯一理由".如果你的CRUD操作自然会一起改变,那么它们就适合SRP.通常,您会更改这些因为数据库架构可能会更改,这自然会影响插入和更新(尽管可能并不总是删除). (4认同)
  • 我个人认为 SRP 可能太过分了,例如,如果我编写一个帐户服务来处理应用程序中的更新/创建/删除帐户,您是否告诉我应该为每个操作创建一个单独的类?例如,关于验证,当它永远不会在其他地方使用时,是否真的值得将验证逻辑提取到另一个类?在我看来,这只会降低代码的可读性和简洁性,并用多余的类打包您的应用程序! (3认同)
  • 这一切都取决于场景。一个人对“做一件事”的定义可能与另一个人不同。在这种情况下,对于您自己来说,显然您希望测试的私有代码很复杂/设置起来很痛苦。您想要测试它的事实本身就证明它做得太多了。因此,是的,拥有一个新对象将是理想的。 (2认同)
  • 至于让你的代码可读性更差,我强烈不同意。使用一个类进行验证比将验证嵌入到其他对象中更容易阅读。您仍然拥有相同数量的代码,只是没有嵌套在一个大类中。我宁愿有几个班级把一件事做得很好,也不愿有一个大班级。 (2认同)

csa*_*ano 13

一些很棒的答案.有一件事我没有看到一提的是,随着测试驱动开发(TDD),在被重构阶段创建私有方法(看提取方法的重构模式的一个例子),因此应该已经具备了必要的测试覆盖.如果正确完成(当然,在正确性方面,你会得到一堆混合的意见),你不必担心必须公开私有方法,以便你可以测试它.


Alf*_*ong 11

为什么不将堆栈管理算法拆分成实用程序类?实用程序类可以管理堆栈并提供公共访问器.其单元测试可以专注于实现细节.算法上棘手的类的深度测试非常有助于解决边缘情况并确保覆盖.

然后,您当前的类可以干净地委派给实用程序类,而不会暴露任何实现细节.其测试将与其他人推荐的分页要求相关.


Tom*_*rys 10

在java中,还可以选择将其打包为私有(即不使用可见性修饰符).如果您的单元测试与正在测试的类位于同一个包中,那么它应该能够看到这些方法,并且比将该方法完全公开更安全一些.


ada*_*ham 10

私有方法通常用作"帮助"方法.因此,它们只返回基本值,而不会对特定的对象实例进行操作.

如果要测试它们,有几个选项.

  • 使用反射
  • 给方法包访问权限

或者,您可以使用辅助方法创建一个新类作为公共方法,如果它对于新类来说是一个足够好的候选者.

这里有一篇非常好的文章.

  • 测试私有代码的反思是一个可怕的想法. (5认同)
  • @Finglas 为什么你不想测试实现细节?它是需要工作的代码,如果它更频繁地更改,您预计它会更频繁地中断,这比公共 API 更有理由对其进行更多测试。当您的代码库稳定后,假设将来某个时候有人出现并更改了私有方法,这会破坏您的类内部的工作方式。测试类内部的测试会立即告诉他们他们破坏了某些东西。是的,这会使维护测试变得更加困难,但这是您对全面覆盖测试所期望的。 (2认同)

Pio*_*rak 8

如果您使用的是C#,则可以将方法设为内部.这样你就不会污染公共API.

然后将属性添加到dll

[assembly:InternalsVisibleTo("MyTestAssembly")]

现在,所有方法都可以在MyTestAssembly项目中看到.也许并不完美,但更好的方法是将私人方法公之于众,以便进行测试.


Dan*_*iuc 7

如果需要,使用反射来访问私有变量.

但实际上,您并不关心类的内部状态,您只是想测试公共方法在您可以预期的情况下返回您期望的内容.


Joh*_*hny 6

在单元测试方面,你绝对不应该添加更多的方法; 我相信你最好只做一个关于你的first()方法的测试用例,在每次测试之前调用它; 那么你可以多次调用 - next(),previous()last()查看结果是否符合你的期望.我想如果你不为你的课程添加更多方法(仅用于测试目的),你会坚持"黑盒子"测试原则;


ben*_*y23 5

我想说这是一个坏主意,因为我不确定你是否会获得任何好处以及潜在的问题.如果你正在改变一个调用的契约,只是为了测试一个私有方法,你就不会测试它如何被使用,而是创建一个你从未打算过的人工场景.

此外,通过将该方法声明为公开,可以说在六个月的时间内(在忘记了公开方法的唯一原因是用于测试之后),你(或者如果你已经把项目交给了)一个完全不同的人赢了使用它,导致潜在的意外后果和/或维护噩梦.


Noe*_*Yap 5

首先看看该方法是否应该被提取到另一个类并公开.如果不是这种情况,请使用@VisibleForTesting使其受保护并使用Java注释.


小智 5

在您的更新中,您说使用公共API进行测试是件好事.这里实际上有两所学校.

  1. 黑盒测试

    黑匣子学校说这个课程应该被认为是一个黑盒子,没有人能看到它内部的实现.测试这个的唯一方法是通过公共API - 就像类的用户将使用它一样.

  2. 白盒测试.

    白盒学校认为自然地使用关于类的实现的知识,然后测试类以知道它按预期工作.

我真的不能参与讨论.我只是觉得知道测试类(或库或其他)有两种不同的方法会很有趣.


Jor*_*dão 5

您想要单独测试的私有方法表明您的类中隐藏着另一个“概念”。将该“概念”提取到其自己的类中,并将其作为单独的“单元”进行测试。

观看此视频,了解有关该主题的非常有趣的观点。


cot*_*eyr 5

你永远不应该让你的测试决定你的代码。我不是在谈论 TDD 或其他 DD,我的意思是,这正是您要问的。您的应用程序是否需要公开这些方法。如果确实如此,则测试它们。如果没有,那么不要为了测试而将它们公开。与变量和其他相同。让您的应用程序的需求决定代码,并让您的测试测试需求是否得到满足。(同样,我的意思不是先测试,我的意思是更改类结构以满足测试目标)。

相反,您应该“测试更高”。测试调用私有方法的方法。但是您的测试应该测试您的应用程序需求,而不是您的“实施决策”。

例如(这里是 bod 伪代码);

   public int books(int a) {
     return add(a, 2);
   }
   private int add(int a, int b) {
     return a+b;
   } 
Run Code Online (Sandbox Code Playgroud)

没有理由测试“添加”,您可以测试“书籍”。

永远不要让您的测试为您做出代码设计决策。测试您是否获得了预期的结果,而不是您如何获得该结果。

  • “永远不要让你的测试为你做出代码设计决策”——我必须不​​同意。难以测试是一种代码味道。如果你无法测试它,你怎么知道它没有损坏? (2认同)