[译]我们仍没有对私有方法是否需要进行测试达成共识

这篇文档来自于阮一峰老师的周刊,主题是私有方法是否需要测试,罗列了多方的观点和意见,如果对单元测试的范围也不太确定的话,强烈推荐看看这一篇文章。原文:https://jesseduffield.com/Testing-Private-Methods/

昨天,当我在一个Rust工作会议上,我不假思索的说:我想我们都同意在编写单元测试时,除非有特殊情况,否组都不应该直接测试私有方法。一个小型的辩论就展开了,许多人争论着互不相容的观点。我们很快结束了这场辩论,但是我还是有一点尴尬,我有点错判了开发人员的信条。

毫无疑问,在开发人员这个职业中,至少有一种观点大家现在都是都是同意的,对吧?再猜一次。如果你想知道这个讨论上的共识有多少,你可以通读一下Stack Overflow上的帖子: Unit testing private methods in C#, How to unit test this private method?, Should Private/Protected methods be under unit test? 。有人说我们应该总是直接测试私有方法,有的人说我们永远不应该直接测试私有方法。这些看法不可能都是对的!有没有最适合当前软件开发状况的观点?

关于测试私有方法的讨论,有五种流行的观点:

  • 首先就不使用私有方法

  • 总是测试私有方法

  • 永远不要测试私有方法

  • 有时可以测试私有方法

  • 将私有方法提取到类中

在这篇文章中,我将讨论每一个观点,然后总结提炼成我自己的经验法则,希望大多数人都能赞同它。注意,虽然我们将讨论类和方法,但是相同的观点也适用于函数式语言中的匿名函数。

观点1:首先就不使用私有方法

我会把这个观点展示出来,是因为大多数人都会认为它有点极端,如果它是对的,它会这个辩论的其他部分无效化。

这种观点与其说是对私有方法的攻击,不如说是对试图预测未来的攻击,这个想法是,在编写库代码的时候,你不可能事先知道你的使用者想使用什么方法,并且默认使用私有方法将为你和你的客户带来更多的问题相比于默认为公共方法(或者受保护方法)。这似乎是一种库开发人员独一无二的想法(见The case against private methods, )因为应用开发者可以用一下键盘就很容易让方法变成空的,而库的使用者需要分叉代码库或者提出问题等待回应。

这种也有不利的一面:将一个私有方法转变成公开方法很容易,但是从公开方法降级到私有方法是一个破坏的改动。此外,你的公共方法表明了你希望他们如何使用你的库。为了假设的使用场景,使原有的私有API膨胀你的公共API,你会使你的所有使用者更加难受,他们只想知道如何满足已知的使用场景。这些缺点是交织在一起的:使用者使用错误的方法与你的库交互,这反过来也使重构变得更加困难

观点2:总是测试使用私有方法

虽然这是一个不受欢迎的观点,但是仍然有一些支持者,主要有三论点:

  • 当使用测试驱动开发(TDD)时,你需要在编写代码前编写测试,所以你最好在每个方法的基础上编写测试,而不管你的方法是共有的还是私有的。
  • 通过隔离测试每一个方法,(不管访问修饰符是什么)可以让读者清楚得了解每个方法的预期行为,这样他们就可以更好地理解每个方法在全局中扮演的角色。
  • 直接测试私有方法的一个显而易见的替代方法是通过公共方法测试它们,但是这需要花更长的时间去编写代码,并且可能导致测试需要更长的时间去运行。如果你首要任务是节省开发时间,并且你相信测试公共方法的前期成本高于重构时重写私有方法的持续成本,那么首先测试私有方法并在代价出现时去解决是有意义的。

有些语言比其它语言更容易测试私有方法,如果你的语言使你需要破解各种障碍来测试一个私有方法,那么你或许不适合这种观点。

观点3:永远不要测试私有方法

与先前的观点截然相反,这个观点的主要观点是,你的类的使用者只能通过它的公共方法(即类上的公共方法集)交互所以为什么你的测试会与众不同呢?如果私有方法不能直接被公共方法访问,那么它就是死代码。并且应该被删除、如果它可以通过公共访问被访问,那么你应该通过公共方法测试私有方法,因为如果不模拟即将适应的代码的用户,那么测试的目的是什么呢?

这是哲学上的观点,但是实际的论点更容易被接受:如果你的测试仅仅依赖于类的公共接口,那么你可以重构这个类的内部实现,而不需要改变任何测试。如果你不需要更新测试,那么你能够确切的知道一个失败的测试以为着你破坏了某些东西,而一个完全绿色的测试套件意味着你已经成功的保留了类的原始行为。

相反,如果类的测试依赖于私有方法,并且你的重构删除或更改了任何一个方法的签名,那么你需要重写这些测试来处理新的内部结构,但是现在你已经对你的测试失去了信心,因为测试重写和代码重写一样容易出错!

其次,即使你能够小心地重写测试,捕获与之前完全相同的行为,这仍然是一个费事实力的过程,因此阻止了可以改代码库健康状况的重构。先验观点更强调通过公共方法测试私有方法的前期成本,而这个观点更关心重构的持续成本。

观点4:有时测试私有方法

先前的观点非常关注“公共接口”,但是这个新的观点质疑什么是真正的公共的,什么是真正的单元。如果你正在编写一个应用(运行二进制文件),而不是一个库(代码被导出用于其他代码库),那么它只有一个真正的公共接口,也就是它自己,例如有用户的按键和点击组成的接口。如果你想像前面的观点所倡导的那样有最大化的重构能力,那么最好的方法是让每个测试都打开应用并模仿用户的点击和按键。这样,对任何内部代码都没有依赖性,你可以自信地重构代码,而不必重写任何测试。

在很少的情况下,端到端测试是最明智选择,例如,当你继承了一个几乎不可能进行单元测试的系统,你将要重构整个代码库,或者当你正在构建一个AB测试,并且想要针对两个实现运行测试以获得特性、BUG兼容性。然而,在大多数情况下,放弃所有的单元测试,代之以编写数以万计的端到端测试,而这些测试实际上是在模仿真实的用户,这是荒谬的。只包含端到端测试的测试套件存在问题的原因有一下几个:

  • 运行一个给定的测试需要太长的时间
  • 编写一个给定的测试需要太长的时间
  • 每个测试的复杂性掩盖了它的意图,降低了测试作为文档的能力
  • 更改一个特性可能会破坏另一个不相关的测试

正是由于这些原因,单元测试才首先存在,作为开发人员,我们通过更深入地侵入应用程序的代码并选择我们认为值得单独测试的“单元”来妥协。我们这样做是因为知道,如果重构导致这样一个单元消失,那么我们需要在其他地方重写它的测试,并承担上述所有成本。

一旦我们开始测试相对于我们的其它代码是公开的,但是相对于最终用户是私有的代码,我们必须承认我们的“单元”选择过程固有的随意性。在类中测试私有方法和在应用程序中测试类之间的区别只是程度上的不同,而不是种类上的不同。

这为我们提供了一个封装范围,从应用程序本身开始,向下通过模块、类,最后到私有方法,我们将封装级别降低到越来越小的片。封装级别越低,越难测试, 但封装级别越低,越难重构。

这种观点认为,如果一个私有方法足够自成一体。并且通过公共接口测试让人头疼,那么它就可以直接测试,而不必感到羞愧或内疚,否则就是双重标准。

观点5:将私有方法单独提取到一个单独的类中

这个观点建立在前一个观点的基础之上 ,即如果你发现自己想要测试一个私有方法,那么这是一个迹象,表明你的类可能有大多的职责,违反单一职责原则(SRP)。

在《修改代码的艺术》一书中,作者Michael Feathers说:

如果我们需要测试一个私有方法,我们应该将其公共化。如果公开它让我们感到困扰,在大多数情况下,这意味着我们的类做的太多了,我们应该修正它。

(就我个人而言, 就我个人而言,我发现想象完全为了测试而公开一个方法不会感到困扰,但是你明白我的意思)

在Ruby中的实用面向对象设计中,,Sandi Metz 还指出,渴望被测试的私有方法是违反单一职责的坏味道。

前面的观点认为“单位”的选择是任意的,这个观点不同意。如果你想测试一些私有代码,那就意味着您偶尔发现了一个抽象边界,而这个抽象边界中并没有明确表示出来。也许你想要测试一些直接与问题相关联的算法,这种情况下,它应该被提取到它自己的抽象实现中。

通过将一个私有方法提取到一个单独的类中,我们现在可以通过它的公共接口来测试这个类,而且我们还有一个额外的好处,那就是将一个新类作为一个依赖项注入到原始类中,使我们能够很容易地模拟构造类的行为,这样代码和测试都可以保持职责分离。

如果在类中包装单个函数有点极端,而且你的语言允许函数存在于类之外,那么大概率将私有方法提取到它自己的独立函数中没有问题,前提是你可以切断它对任何实例变量的依赖关系。

讨论

我们从一个提出激进的观点开始,即任何方法都不应该是私有的。当然可以简化测试过程,但是缺乏封装也使测试过程变得很糟糕。

然后我们考虑了两个完全矛盾的观点,一个不想测试私有方法,另一个想测试所有的公有和私有方法。然后第三种观点出现了,提出无论你在封装的范围内处于什么位置,在更高级别(例如类)或更低级别(例如私有方法)进行测试都有利有弊,如果利大于弊,那么编写对应的测试并不可耻。

再之后,第四种观点出现了,它给这种测试方式泼了一盆冷水,需要测试私有方法本身就是类有太多职责的坏味道。

一个强调坚持测试类的公共API的观点3支持者可能会对观点5的支持者说:等一下,到目前为止,我们一直在讨论重构和封装,但是你已经将目标转移到了单一职责上!将私有方法移动到私有的类中并不能减轻重构时的负担:我们很可能需要像处理私有方法一样处理、更改私有类,这意味着在任何情况下,测试仍然需要重写。这里假设你的语言支持私有类,如果你支持的话,你就在刚才扩展了你的公共API,包含了一个你实际上不希望客户端使用的类!当一个私有方法只被一个类使用时将它移动到一个完全独立的文件中真的有意义吗》这对可读性有什么帮助?

我的建议

下面是我建议的方法:通过将每个方法默认为private,在类中尽可能使用精简的公共接口。如果你发现你自己想要直接测试一组私有方法,那么请考虑提取一个类(或独立函数),但前提是它与你的测试需求无关。如果你想测试一个私有方法,并且看不到将其从类中提取出来的意义,那么将其转换为一个存函数,并测试该方法。这样,如果以后你决定将其函数移动到其他地方,那么移动测试就像复制+ 粘贴一样简单。

在这场辩论中,我是否忽略或歪曲了任何观点?你不同意我的观点?我是不是太过于笼统了?有消息通知我。下次见!

链接