Making a private method public to unit test it…good idea?
版主注:这里已经发布了39个答案(有些已被删除)。在发布答案之前,请考虑是否可以为讨论添加一些有意义的内容。你很可能只是重复别人已经说过的话。
我偶尔会发现自己需要在类中公开一个私有方法,只是为了为它编写一些单元测试。
通常这是因为该方法包含类中其他方法之间共享的逻辑,并且更整洁地单独测试逻辑,或者另一个原因可能是我希望测试同步线程中使用的逻辑,而不必担心线程问题。
其他人发现自己这样做是因为我真的不喜欢这样做吗??我个人认为奖金超过了公开一种方法的问题,这种方法并不能真正提供课外服务…
更新
感谢大家的回答,似乎激起了人们的兴趣。我认为普遍的共识是应该通过公共API进行测试,因为这是使用类的唯一方式,我同意这一点。我上面提到的几个我会在上面做这些的案例都是不常见的,我认为这样做的好处是值得的。
然而,我可以看到每个人都指出,这不应该真的发生。当我更多地考虑它的时候,我认为改变你的代码来适应测试是一个坏主意——毕竟我认为测试在某种程度上是一个支持工具,如果你愿意的话,改变一个系统来"支持一个支持工具"是明显的坏做法。
Note:
This answer was originally posted for the question Is unit testing alone ever a good reason to expose private instance variables via getters? which was merged into this one, so it may be a tad specific to the usecase presented there.
作为一个一般性的声明,我通常都是为了重构"生产"代码以使其更容易测试。不过,我不认为这是个好电话。好的单元测试(通常)不应该关心类的实现细节,只关心它的可见行为。您可以测试类在调用
例如,考虑以下伪代码:
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 | 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()); } } |
就我个人而言,我更愿意使用公共API进行单元测试,而且我绝对不会为了方便测试而将私有方法公开。
如果您确实想单独测试私有方法,在Java中可以使用EasyMoCK/PrimeMcCK来实现这一点。
你必须实事求是,你也应该知道为什么事情很难测试的原因。
"听测试"——如果很难测试,那是不是告诉了你一些关于你的设计?您能否重构到这样一个地方:通过公共API进行测试,这个方法的测试将是琐碎且容易覆盖的?
以下是Michael Feathers在"有效地处理遗留代码"中所说的话
"Many people spend a lot of time trying ot figure out how to get around this problem ... the real answer is that if you have the urge to test a private method, the method shouldn't be private; if making the method public bothers you, chances are, it is because it is part of a separate reponsibility; it should be on another class." [Working Effectively With Legacy Code (2005) by M. Feathers]
正如其他人所说,有点怀疑是单元测试私有方法;单元测试公共接口,而不是私有实现细节。
也就是说,当我想对C中私有的东西进行单元测试时,我使用的技术是将可访问性保护从私有降级为内部,然后使用InternalsVisibleTo将单元测试程序集标记为友元程序集。单元测试组件将被允许将内部构件视为公共的,但是您不必担心意外地添加到公共的表面积中。
很多答案都建议只测试公共接口,但我认为这是不现实的——如果一个方法做了5个步骤的事情,那么您将希望分别测试这5个步骤,而不是全部测试。这就需要测试所有五种方法,否则这五种方法(测试除外)可能是
测试"private"方法的常用方法是给每个类自己的接口,并使"private"方法
是的,这将导致文件和类膨胀。
是的,这确实使
是的,这是屁股疼。
不幸的是,这是我们为使代码可测试而做出的众多牺牲之一。也许未来的语言(或者甚至是未来版本的C /爪哇)将具有使类和模块可测试性更方便的特性,但同时,我们必须跳过这些环。
有些人会争辩说,这些步骤中的每一个都应该是自己的类,但我不同意——如果它们都共享状态,就没有理由在五个方法可以做到的地方创建五个独立的类。更糟糕的是,这会导致文件和类膨胀。此外,它还会影响模块的公共API——如果您想从另一个模块测试这些类(或者将测试代码包含在同一个模块中,这意味着将测试代码与产品一起装运),那么所有这些类都必须是
单元测试应该测试公共契约,这是类如何在代码的其他部分中使用的唯一方法。私有方法是实现细节,您不应该测试它,只要公共API工作正常,实现就不重要,并且可以在不改变测试用例的情况下进行更改。
在我看来,您应该编写测试,而不是对类如何在内部实现进行深入的假设。您可能希望稍后使用另一个内部模型对其进行重构,但仍然要做出与前一个实现相同的保证。
记住这一点,我建议您集中精力测试您的合同是否仍然有效,不管您的类目前有什么内部实现。公共API的基于属性的测试。
让它成为私有包怎么样?然后您的测试代码可以看到它(以及包中的其他类),但它仍然对您的用户隐藏。
但实际上,您不应该测试私有方法。这些是实施细节,不是合同的一部分。它们所做的一切都应该通过调用公共方法来覆盖(如果其中有公共方法未执行的代码,那么应该这样做)。如果私有代码太复杂,那么类可能做了太多的事情,并且缺少重构。
公开一种方法是很大的承诺。一旦你这样做了,人们就可以使用它,你不能再改变它们了。
更新:我在许多其他地方为这个问题添加了一个更广泛、更完整的答案。这可以在我的博客上找到。
如果我需要公开一些东西来测试它,这通常意味着被测试的系统不遵循单一的责任性原则。因此,应该引入一个缺少的类。将代码提取到新类中后,将其公开。现在您可以很容易地进行测试,并且您正在遵循SRP。您的另一个类只需通过组合调用这个新类。
公开方法/使用langauge技巧(如将代码标记为对测试组合可见)应始终是最后的手段。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class SystemUnderTest { public void DoStuff() { // Blah // Call Validate() } private void Validate() { // Several lines of complex code... } } |
通过引入一个验证器对象来重构这个。
1 2 3 4 5 6 7 8 | public class SystemUnderTest { public void DoStuff() { // Blah validator.Invoke(..) } } |
现在我们要做的就是测试验证程序是否被正确调用。验证的实际过程(以前的私有逻辑)可以在纯隔离中进行测试。不需要进行复杂的测试设置来确保验证通过。
一些很好的答案。我没有看到提到的一件事是,使用测试驱动开发(TDD),私有方法是在重构阶段创建的(查看提取方法以获得重构模式的示例),因此应该已经有了必要的测试覆盖范围。如果做得正确(当然,当涉及到正确性时,你会得到一个混合的意见包),你不必担心必须公开一个私有方法,这样你就可以测试它。
为什么不把堆栈管理算法分解成一个实用程序类呢?实用程序类可以管理堆栈并提供公共访问器。它的单元测试可以集中在实现细节上。算法复杂类的深度测试对于解决边缘情况和确保覆盖非常有帮助。
然后,当前类可以干净地委托给实用程序类,而不公开任何实现细节。它的测试将与其他人推荐的分页要求相关。
在爪哇,也有选择使它包私有(即离开能见度修改器)。如果单元测试与被测试的类在同一个包中,那么它应该能够看到这些方法,并且比将该方法完全公开要安全一些。
私有方法通常用作"助手"方法。因此,它们只返回基本值,从不在对象的特定实例上操作。
如果您想测试它们,您有几个选项。
- 使用反射
- 授予方法包访问权限
或者,您可以使用helper方法创建一个新的类作为公共方法,如果它是一个新类的足够好的候选者。
这方面有一篇很好的文章。
如果您使用C,您可以将方法设置为内部方法。这样就不会污染公共API。
然后将属性添加到dll
[程序集:InternalsVisibleTo("MyTestAssembly")]
现在,所有方法都在MyTestAssembly项目中可见。也许不完美,但最好是公开私有方法来测试它。
如果需要,可以使用反射来访问私有变量。
但实际上,您并不关心类的内部状态,您只想测试公共方法是否返回了您可以预期的情况下所期望的内容。
在单元测试方面,你绝对不应该增加更多的方法;我相信你最好做一个关于你的
我会说这是个坏主意,因为我不确定你是否从中得到任何好处和潜在的问题。如果您要更改调用的约定,只是为了测试私有方法,那么您不是在测试类的使用方式,而是创建一个您从未打算发生的人工场景。
此外,通过将方法声明为公共方法,可以说在六个月内(在忘记将方法公开的唯一原因是为了测试之后),您(或者如果您已经移交了项目)完全不同的人不会使用它,从而导致潜在的意外后果和/或维护噩梦。
首先看看是否应该将该方法提取到另一个类中并公开。如果不是这样的话,让它受到包保护,在Java中用@ VisualFielTestEngt注解。
在您的更新中,您说只使用公共API进行测试是很好的。这里实际上有两所学校。
黑盒测试
黑匣子学校说,这个类应该被视为一个黑匣子,没有人能看到里面的实现。唯一的测试方法是通过公共API——就像类的用户将使用它一样。
白盒测试。
白盒学校自然地认为,它使用有关课程实施的知识,然后测试课程,以了解它应该如何工作。
我真的不能站在讨论的一边。我只是想知道有两种不同的方法来测试一个类(或者一个库或者其他什么的)是很有趣的。
要单独测试的私有方法表明类中隐藏了另一个"概念"。将这个"概念"提取到它自己的类中,并作为一个单独的"单元"对其进行测试。
请看这段视频,了解这个主题的真正有趣之处。
实际上,在某些情况下,您应该这样做(例如,当您正在实现一些复杂的算法时)。只需将它打包为私有,这就足够了。但在大多数情况下,您可能有太复杂的类,需要将逻辑分解到其他类中。
你永远不应该让你的测试命令你的代码。我不是说TDD或其他DDS,我的意思是,确切地说,你要什么。你的应用需要这些方法公开吗?如果有,那么测试它们。如果没有,那么就不要仅仅为了测试而公开它们。变量和其他变量也一样。让应用程序的需求指定代码,让测试测试满足需求。(同样,我不是说先测试或者不测试,我的意思是改变类结构以实现测试目标)。
相反,你应该"测试更高"。测试调用私有方法的方法。但是您的测试应该测试您的应用程序需求,而不是您的"实现决策"。
例如(此处为BOD伪代码);
1 2 3 4 5 6 | public int books(int a) { return add(a, 2); } private int add(int a, int b) { return a+b; } |
没有理由测试"添加",你可以测试"书"。
永远不要让您的测试为您做代码设计决策。测试你是否得到了预期的结果,而不是你如何得到那个结果。
我通常将这些方法保留为
我经常在类中添加一个名为
有时这个方法被封装在一个IFDEF块中(我主要写在C++中),这样它就不会被编译以释放。但是,在发行版中,提供验证方法来检查程序的对象树,这通常很有用。
我倾向于同意,让IT部门测试的好处大于增加一些成员的可见性的问题。稍微的改进是使其受保护和虚拟化,然后在测试类中重写它以公开它。
或者,如果您想单独测试它的功能,它是否不建议您的设计中缺少一个对象?也许您可以将它放在一个单独的可测试类中……然后您的现有类只委托给这个新类的一个实例。
guava有一个@visiblefortesting注释,用于标记扩大了范围(package或public)的方法,否则将扩大范围。我对同一件事使用了@private注释。
虽然必须测试公共API,但有时获取通常不公开的内容既方便又明智。
什么时候?
- 在toto中,通过将一个类分成多个类,使它的可读性显著降低,
- 只是为了让它更具可测试性,
- 提供一些进入内脏的测试通道就可以做到这一点。
宗教似乎胜过工程学。
我通常将测试类保存在与被测试类相同的项目/程序集中。这样,我只需要
这使您的构建过程有些复杂,需要过滤掉测试类。我通过将所有测试类命名为
当然,这只适用于你问题的C/.NET部分。
不,因为有更好的剥那只猫皮的方法。
一些单元测试工具依赖于类定义中的宏,当在测试模式中构建时,宏会自动扩展以创建挂钩。非常C风格,但它是有效的。
一个更简单的OO习语是让您想要测试的任何东西"受保护"而不是"私有"。测试工具从被测试的类继承,然后可以访问所有受保护的成员。
或者你选择"朋友"选项。就个人而言,这是C++的一个特点,因为它打破了封装规则,但它恰巧是C++实现某些特性所必需的,所以嘿嘿。
不管怎样,如果您是单元测试,那么您同样可能需要向这些成员注入值。白盒短信是完全有效的。这真的会破坏你的封装。
在.NET中有一个特殊的类,名为
在msdn或堆栈溢出上查看更多关于它的信息
(我想知道到目前为止还没有人提到过。)
在有些情况下,这是不够的,在这种情况下,你将不得不使用反射。
尽管如此,我还是坚持不测试私有方法的一般性建议,但是通常也有例外。
正如其他人的评论所广泛指出的,单元测试应该关注公共API。但是,撇开优缺点和理由不谈,您可以使用反射在单元测试中调用私有方法。当然,您需要确保您的JRE安全性允许它。调用私有方法是Spring框架与ReflectionUtils一起使用的方法(参见
下面是一个带有私有实例方法的小示例类。
1 2 3 4 5 | public class A { private void doSomething() { System.out.println("Doing something private."); } } |
以及执行私有实例方法的示例类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class B { public static final void main(final String[] args) { try { Method doSomething = A.class.getDeclaredMethod("doSomething"); A o = new A(); //o.doSomething(); // Compile-time error! doSomething.setAccessible(true); // If this is not done, you get an IllegalAccessException! doSomething.invoke(o); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } } } |
执行b,将打印
一切都是实用主义。您的单元测试在某种程度上是代码的客户机,为了获得良好的代码覆盖率,您需要使代码具有可测试性。如果测试代码是非常复杂的,那么您的解决方案将是一个潜在的失败,以便您能够在没有有效公共接缝的情况下设置必要的角落案例。使用国际奥委会在这方面也有帮助。
单元测试的重点是确认该单元的公共API的工作。应该不需要只为测试而公开一个私有方法,如果是这样,那么应该重新考虑您的接口。私有方法可以被认为是公共接口的"助手"方法,因此可以通过公共接口进行测试,因为它们将调用私有方法。
我能看到您有"需要"这样做的唯一原因是您的类没有被正确地设计为要进行测试。
回答得很好。ihmo,@blueraja-danny-pflughoeft的最佳答案之一。好的。
Lots of answers suggest only testing the public interface, but IMHO
this is unrealistic - if a method does something that takes 5 steps,
you'll want to test those five steps separately, not all together.
This requires testing all five methods, which (other than for testing)
might otherwise be private.Ok.
首先,我要强调的是,"我们应该公开一个私有方法来进行单元测试吗"这个问题是一个客观正确的答案取决于多个参数的问题。所以我认为在某些情况下我们不需要,而在其他情况下我们应该。好的。
这里有些答案可以概括为:"这样做通常是好的",或者"从不,这是坏的"。不要欺骗API,只测试公共行为"。这让我非常恼火,因为测试和实现的设计质量是一个重要的问题,这个问题意味着对两者都有许多后果。好的。
将公共方法设为私有方法还是将私有方法提取为其他类(新的或现有的)中的公共方法?好的。
对于可接受扩大
为了避免所有这些重复,在许多情况下,一个更好的解决方案是在新类或现有类中提取
嘲笑私人方法?好的。
当然,使用反射或将工具作为powermock是可能的,但ihmo我认为这通常是绕过设计问题的一种方法。测试类是另一个类。
模拟被测对象的公共方法?好的。
您可以将修改器
Creates a spy of the real object. The spy calls real methods unless they are > > stubbed.
Ok.
Real spies should be used carefully and occasionally, for example when
dealing with legacy code.Ok.
根据经验,使用
下面是我用来决定
案例1)如果该方法被调用一次,则永远不要生成
案例2)如果多次调用
如何决定?好的。
private 方法在测试中不会产生重复。->保持方法的私有性。好的。private 方法在测试中产生重复。也就是说,您需要重复一些测试,为单元使用private 方法测试public 方法的每个测试断言相同的逻辑。->如果重复处理可能使提供给客户的API成为一部分(没有安全问题、没有内部处理等),则将private 方法提取为新类中的public 方法。->否则,如果重复处理不需要将提供给客户的API的一部分(安全问题、内部处理等),则不要将private 方法的可见性扩大到public 。您可以保持它不变,或者将方法移动到一个private 包类中,该类永远不会成为API的一部分,也永远不会被客户机访问。好的。
代码示例好的。
实例依赖于Java和以下库:JUnit、AsjtJJ(断言匹配器)和MoCito。但我认为整体方法对C也是有效的。好的。
1)
这里是一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class Computation { public int add(String a, String b) { int[] ints = mapToInts(a, b); return ints[0] + ints[1]; } public int minus(String a, String b) { int[] ints = mapToInts(a, b); return ints[0] - ints[1]; } public int multiply(String a, String b) { int[] ints = mapToInts(a, b); return ints[0] * ints[1]; } private int[] mapToInts(String a, String b) { return new int[] { Integer.parseInt(a), Integer.parseInt(b) }; } } |
测试代码如下:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class ComputationTest { private Computation computation = new Computation(); @Test public void add() throws Exception { Assert.assertEquals(7, computation.add("3","4")); } @Test public void minus() throws Exception { Assert.assertEquals(2, computation.minus("5","3")); } @Test public void multiply() throws Exception { Assert.assertEquals(100, computation.multiply("20","5")); } } |
我们可以看到,调用
2)当
这里有一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class MessageService { public Message createMessage(String message, Credentials credentials) { Header header = createHeader(credentials, message, false); return new Message(header, message); } public Message createEncryptedMessage(String message, Credentials credentials) { Header header = createHeader(credentials, message, true); // specific processing to encrypt // ...... return new Message(header, message); } public Message createAnonymousMessage(String message) { Header header = createHeader(Credentials.anonymous(), message, false); return new Message(header, message); } private Header createHeader(Credentials credentials, String message, boolean isEncrypted) { return new Header(credentials, message.length(), LocalDate.now(), isEncrypted); } } |
测试代码如下:好的。
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 46 47 | import java.time.LocalDate; import org.assertj.core.api.Assertions; import org.junit.Test; import junit.framework.Assert; public class MessageServiceTest { private MessageService messageService = new MessageService(); @Test public void createMessage() throws Exception { final String inputMessage ="simple message"; final Credentials inputCredentials = new Credentials("user","pass"); Message actualMessage = messageService.createMessage(inputMessage, inputCredentials); // assertion Assert.assertEquals(inputMessage, actualMessage.getMessage()); Assertions.assertThat(actualMessage.getHeader()) .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage) .containsExactly(inputCredentials, 9, LocalDate.now(), false); } @Test public void createEncryptedMessage() throws Exception { final String inputMessage ="encryted message"; final Credentials inputCredentials = new Credentials("user","pass"); Message actualMessage = messageService.createEncryptedMessage(inputMessage, inputCredentials); // assertion Assert.assertEquals("A?4B36ddflm1Dkok49d1d9gaz", actualMessage.getMessage()); Assertions.assertThat(actualMessage.getHeader()) .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage) .containsExactly(inputCredentials, 9, LocalDate.now(), true); } @Test public void createAnonymousMessage() throws Exception { final String inputMessage ="anonymous message"; Message actualMessage = messageService.createAnonymousMessage(inputMessage); // assertion Assert.assertEquals(inputMessage, actualMessage.getMessage()); Assertions.assertThat(actualMessage.getHeader()) .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage) .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), false); } } |
我们可以看到,调用
我们还可以注意到,断言复制在方法之间很接近,但不是必需的,正如
重构步骤好的。
假设我们是在这样一种情况下,
1 2 3 | public Header createHeader(Credentials credentials, String message, boolean isEncrypted) { return new Header(credentials, message.length(), LocalDate.now(), isEncrypted); } |
我们现在可以测试单一的这种方法:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | @Test public void createHeader_with_encrypted_message() throws Exception { ... boolean isEncrypted = true; // action Header actualHeader = messageService.createHeader(credentials, message, isEncrypted); // assertion Assertions.assertThat(actualHeader) .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage) .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), true); } @Test public void createHeader_with_not_encrypted_message() throws Exception { ... boolean isEncrypted = false; // action messageService.createHeader(credentials, message, isEncrypted); // assertion Assertions.assertThat(actualHeader) .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage) .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), false); } |
但是我们之前为使用
因此,我们介绍了
1 2 3 4 5 6 7 | public class HeaderService { public Header createHeader(Credentials credentials, String message, boolean isEncrypted) { return new Header(credentials, message.length(), LocalDate.now(), isEncrypted); } } |
我们迁移了
现在,
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 | public class MessageService { private HeaderService headerService; public MessageService(HeaderService headerService) { this.headerService = headerService; } public Message createMessage(String message, Credentials credentials) { Header header = headerService.createHeader(credentials, message, false); return new Message(header, message); } public Message createEncryptedMessage(String message, Credentials credentials) { Header header = headerService.createHeader(credentials, message, true); // specific processing to encrypt // ...... return new Message(header, message); } public Message createAnonymousMessage(String message) { Header header = headerService.createHeader(Credentials.anonymous(), message, false); return new Message(header, message); } } |
在
例如,这里是新版本的
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Test public void createMessage() throws Exception { final String inputMessage ="simple message"; final Credentials inputCredentials = new Credentials("user","pass"); final Header fakeHeaderForMock = createFakeHeader(); Mockito.when(headerService.createHeader(inputCredentials, inputMessage, false)) .thenReturn(fakeHeaderForMock); // action Message actualMessage = messageService.createMessage(inputMessage, inputCredentials); // assertion Assert.assertEquals(inputMessage, actualMessage.getMessage()); Assert.assertSame(fakeHeaderForMock, actualMessage.getHeader()); } |
注意,
就个人而言,我在测试私有方法时也有同样的问题,这是因为一些测试工具是有限的。如果有限的工具不能响应你的需求,那么用它们驱动你的设计是不好的。改变工具,而不是设计。因为您对C语言的需求不能提出好的测试工具,但是对于Java来说,有两个强大的工具:TestNG和PosiMoCK,并且可以为.NET平台找到相应的测试工具。