让我们首先承认这是一场部分宗教的辩论。
我的观点是测试private
方法是你应该经常避免的,但也有例外。
为什么我不应该测试private
方法?
因为您正在测试您的应用程序是否工作,而不一定是如何工作。方法private
不是程序公开的 API 的一部分,它本质上是一个实现细节。
反对通过封装来解决对您设置的限制并仍然对其进行测试的一个常见论点是,它可能会破坏您测试该特定实现的测试。
在“正常”测试中,我们测试这些public
方法,因为它们构成了我们的程序:如果我们知道所有公共 API 都可以工作,那么我们就知道程序可以工作。如果您的应用程序结构良好,您将拥有多个 API 层,它们会不断地将测试细化为更小的单元,以便您清楚地了解可能出现问题的地方。
关于private
方法要意识到的是,它们总是会在某个时间点被public
方法调用,该方法将得到测试。方法private
是被测单元的实现细节,因此不应单独测试。
这个辅助方法不受 API 约束,因此不能保证它的效果会保持不变;在不调整整体功能的情况下更改实现细节现在会破坏您对该方法的测试。因此,即使您的程序仍然可以正常工作,但您现在的测试已损坏。这些测试是不稳定的:不改变你的功能的重构仍然可能导致你不得不删除单元测试,因为它们不再相关(这本身很危险,因为你必须非常确定测试实际上是多余的)。
为什么要测试private
方法?
只有少数东西是黑白的,这不是其中之一。我相信新代码不应该测试private
方法,但这就是我要区分的地方:新代码。
如果您继承了遗留代码库,那么架构可能不是很漂亮。您可能不得不跳过障碍,整体代码流可能会更好。
所以你要做的第一件事是编写一些单元测试,保证在你破坏功能时告诉你。到目前为止一切顺利,我们现在可以进入实际的重构过程。让我们从这个 500 行的private
方法开始?
牢记前面的评论,您现在必须查看代码流并查看哪些public
方法使用该private
方法,并在您进行更改时密切关注它们。
为了易于使用,我会在这里编写测试,仅private
针对应用程序的其余部分测试该方法的合同:只要您从该方法得到的结果没有差异,您就知道它不会影响任何其他遗留方法。有了这些知识,您现在可以轻松地重构该方法中的所有内容。当某些东西确实破裂时;您可以立即使用您的辅助方法,而不必通过公共 API 方法。
请记住,我只会为大型重构操作这样做:这些首先是临时测试,而“实现细节”实际上是 500 行代码:这是一个包含很多细节的细节。
我应该如何测试private
方法?
关于如何使这些可测试,您有几个选项:
反射
我相信这就是 WhiteBox 使用的。但是它引起了一些人的注意:没有静态类型(更改方法名意味着破坏测试),它很昂贵并且使用反射的代码往往更难阅读。
私有对象
我还没有查看确切的实现,但我很确定这在幕后使用反射。基本上它通过一个方便的接口提供反射方法。我没有看到它使用太多(部分是因为我会使用反射作为绝对的最后手段)但它就在那里。
MSDN
萃取
这将是我的第一种方法:这个辅助方法是否重要到足以让它独立存在?您可能会对此说“是”,因为显然它已经足够重要,以至于您需要对其进行显式测试。
上市
最明显的选择:简单地做public
。这遵循与提取相同的想法:“它可以独立存在吗?”。这里的不同之处在于您仍然将其视为当前类的一部分,您只是提升了它的访问权限,而将其提取到不同的类也表明您谈论的不仅仅是一个辅助方法。
内部的
此选项采用中间路线:如果我将其设为内部(C# 上下文)并使用该[InternalsVisibleTo]
属性,您可以internal
在为您的测试程序集提供测试它的可能性的同时制作它(并且仍然使其远离公共 API)。
这带来了人们仅将其视为实现细节并在破坏测试(占特定实现)的同时更改实现的风险。
此外,这也增加了您的应用程序和测试之间的耦合。
所有人都认为这取决于您自己的喜好:测试方法有多种选择,private
双方都有一些论据。就我个人而言,我会远离测试这些实现细节的麻烦,而是看看我能做些什么来改进我的架构。问题很可能会以这种方式自行解决。