我赞同鲍勃叔叔的观点,即问题出在设计上。我还会后退一步检查您的合同设计。
简而言之
不要说“x==0 时返回 -1”或“x==y 时抛出 CannotCalculateException”,而是在 niftyCalcuatorThingy(x,y)
适当的情况下用前置条件x!=y && x!=0
指定不足(见下文)。因此,您的存根在这些情况下可能表现得任意,您的单元测试必须反映这一点,并且您具有最大的模块化,即对于所有未指定的情况,可以随意更改被测系统的行为 - 而无需更改合同或测试。
适当的规格不足
您可以根据以下标准区分您的陈述“-1 当它因某种原因失败时”:
- 实现可以检查的异常行为?
- 在方法的域/职责范围内?
- 调用者(或调用堆栈中较早的人)可以通过其他方式恢复/处理的异常?
当且仅当 1) 到 3) 成立时,在合约中指定场景(例如,EmptyStackException
在空堆栈上调用 pop() 时抛出的场景)。
没有 1),实现不能保证在例外情况下的特定行为。例如,当不满足自反性、对称性、传递性和一致性的条件时,Object.equals() 不会指定任何行为。
没有 2),不满足 SingleResponsibilityPrinciple,模块化被破坏,代码的用户/读者会感到困惑。例如,Graph transform(Graph original)
不应该指定MissingResourceException
可能会被抛出,因为在内心深处,通过序列化进行了一些克隆。
没有 3),调用者不能使用指定的行为(某些返回值/异常)。例如,如果 JVM 抛出 UnknownError。
优点和缺点
如果您确实指定了 1)、2) 或 3) 不成立的情况,您会遇到一些困难:
- (设计)合同的主要目的是模块化。如果您真正分离职责,这是最好的实现:当不满足前提条件(调用者的职责)时,不指定实现的行为会导致最大的模块化 - 正如您的示例所示。
- 您将来无权更改,甚至无法更改在较少情况下引发异常的方法的更通用功能
- 异常行为可能变得相当复杂,因此涵盖它们的合约变得复杂、容易出错且难以理解。例如:是否涵盖了所有情况?如果多个异常前提条件成立,哪种行为是正确的?
规范不足的缺点是(测试)稳健性,即实现对异常情况做出适当反应的能力,更难。
作为妥协,我喜欢尽可能使用以下合约模式:
<(半)正式的前置条件和后置条件,包括 1) 到 3) 成立的异常行为>
如果不满足 PRE,则当前实现将抛出 RTE A、B 或 C。