在TDD中重新设计后该方法变为私有后,方法的测试会如何?


29

假设我开始开发角色扮演游戏,角色扮演者会攻击其他角色以及类似的东西。

应用TDD,我做了一些测试用例来测试Character.receiveAttack(Int)方法内部的逻辑。像这样:

@Test
fun healthIsReducedWhenCharacterIsAttacked() {
    val c = Character(100) //arg is the health
    c.receiveAttack(50) //arg is the suffered attack damage
    assertThat(c.health, is(50));
}

说我有10种方法测试receiveAttack方法。现在,我添加一个方法Character.attack(Character)(调用receiveAttackmethod),并在经过一些TDD循环测试之后,我做出一个决定:Character.receiveAttack(Int)应该是private

前10个测试用例会发生什么?我应该删除它们吗?我应该保留方法public(我不这样认为)吗?

这个问题不是关于如何测试私有方法的,而是关于在应用TDD时重新设计后如何处理它们的问题。



10
如果它是私有的,您无需测试,就很简单。删除并进行重构舞蹈
kayess

6
我可能在这里反对。但是,我通常不惜一切代价避免使用私有方法。我喜欢更多的测试,而不是更少的测试。我知道人们在想什么:“那么,您永远不会拥有不想暴露给消费者的任何功能吗?”。是的,我不想透露很多。相反,当我有一个私有方法时,我将其重构为自己的类,并使用原始类中的所述类。可以将新类标记为internal或与您的语言等效,以防止其被暴露。实际上,凯文·克莱恩(Kevin Cline)的答案就是这种方法。
user9993 '10 -10-3

3
@ user9993您似乎已经倒退了。如果对您来说更多的测试很重要,那么确保您没有错过任何重要事项的唯一方法是运行覆盖率分析。对于覆盖率工具,该方法是私有的还是公共的或其他任何方法都没有关系。希望做的东西公开会以某种方式弥补缺乏覆盖分析的给人一种虚假的感觉恐怕
蚊蚋

2
@gnat但是我从没说过“没有报道”吗?我对“我更喜欢测试而不是更少测试”的评论应该很明显。不确定您要得到的是什么,当然,我还要测试提取的代码。这就是重点。
user9993 '10 -10-3

Answers:


52

在TDD中,测试充当设计的可执行文档。您的设计已更改,因此很显然,您的文档也必须更改!

请注意,在TDD中,该attack方法可能出现的唯一方法是测试通过失败的结果。这意味着,attack正在通过其他测试进行测试。这意味着的测试间接 receiveAttack覆盖了该attack测试。理想情况下,对的任何更改receiveAttack都应至少破坏的一项attack测试。

如果没有,那么其中的功能receiveAttack就不再需要,也不应该再存在!

因此,由于receiveAttack已经通过进行了测试attack,因此您是否保留测试也没关系。如果您的测试框架使测试私有方法变得容易,并且如果决定测试私有方法,则可以保留它们。但是您也可以删除它们,而不会失去测试的覆盖范围和信心。


14
这是一个很好的答案,除了“如果您的测试框架可以轻松测试私有方法,并且如果您决定测试私有方法,则可以保留它们。” 私有方法是实现细节,永远不要直接对其进行测试。
David Arno

20
@DavidArno:我不同意,因此不应对模块的内部进行测试。但是,模块的内部结构可能非常复杂,因此对每个单独的内部功能进行单元测试可能很有价值。单元测试用于检查功能性的不变性,如果私有方法具有不变性(前提条件/后置条件),则单元测试可能很有价值。
Matthieu M.

8
通过这种推理,绝不应该测试模块的内部 ”。这些内部绝对不能直接测试。所有测试应仅测试公共API。如果内部元素无法通过公共API访问,请删除它,因为它什么也不做。
David Arno

28
@DavidArno按照这种逻辑,如果您要构建可执行文件(而不是库),则应该根本没有单元测试。-“函数调用不是公共API的一部分!只有命令行参数才是!如果您的程序的内部函数无法通过命令行参数访问,请删除它,因为它什么也不做。” -尽管私有函数不是该类的公共API的一部分,但它们是该类的内部API的一部分。而且,虽然不一定需要测试类的内部API,但是可以使用相同的逻辑来测试可执行文件的内部API。
RM

7
@RM,如果我要以非模块化方式创建可执行文件,那么我将不得不在内部的脆弱测试之间进行选择,或者仅使用通过可执行文件和运行时I / O进行的集成测试。因此,根据我的实际逻辑,而不是您的草编版本,我将以模块化方式(例如,通过一组库)创建它。然后可以以非易碎的方式测试这些模块的公共API。
David Arno

23

如果该方法足够复杂以至于需要测试,则应在某个类中公开。因此,您可以根据以下内容进行重构:

public class X {
  private int complexity(...) {
    ...
  }
  public void somethingElse() {
    int c = complexity(...);
  }
}

至:

public class Complexity {
  public int calculate(...) {
    ...
  }
}

public class X {
  private Complexity complexity;
  public X(Complexity complexity) { // dependency injection happiness
    this.complexity = complexity;
  }

  public void something() {
    int c = complexity.calculate(...);
  }
}

将X.complexity的当前测试移到ComplexityTest。然后通过模拟复杂性向X.something发送文本。

以我的经验,重构为较小的类和较短的方法会带来巨大的好处。它们更易于理解,更易于测试,并且最终被重用,超出了人们的预期。


您的回答更清楚地说明了我在对OP问题的评论中试图解释的想法。好答案。
user9993 '10 -10-3

3
感谢您的回答。实际上,receiveAttack方法非常简单(this.health = this.health - attackDamage)。目前,也许将其提取到另一个类是一个过度设计的解决方案。
埃克托

1
对于OP来说,这绝对是大材小用-他想开车去商店,而不是飞向月球-但在一般情况下是一个很好的解决方案。

如果函数是如此简单,那么可能一开始就将其定义为函数可能是过度设计。
David K

1
今天可能有些矫over过正但是在6个月的时间里,如果对该代码进行大量更改,好处将显而易见。而且,如今在任何体面的IDE中,肯定地将一些代码提取到一个单独的类中应该只是几次击键,而考虑到在运行时二进制文件中无论如何它们都将归结为相同的东西,因此这几乎绝不是一个过度设计的解决方案。
斯蒂芬·伯恩

6

说我有10种测试receiveAttack方法的方法。现在,我添加了一个Character.attack(Character)方法(调用receiveAttack方法),经过一些TDD循环测试之后,我做出一个决定:Character.receiveAttack(Int)应该是私有的。

这里要记住的一点是,您所做的决定是从API中删除方法。向后兼容的礼貌暗示

  1. 如果您不需要删除它,则将其保留在API中
  2. 如果你不需要删除它尚未,然后将其标记为过时,如果可能的话文档时的生活到底会发生
  3. 如果确实需要删除它,那么您将有一个主要版本更改

当您的API不再支持该方法时,将删除/或替换测试。那时,私有方法是一个实现细节,您应该可以重构它。

到那时,您回到了标准问题,即您的测试套件是否应直接访问实现,而不是纯粹通过公共API进行交互。私有方法是我们应该能够替换的,而不会妨碍测试套件。因此,我希望与之相伴的测试会消失—要么退休,要么将实现与单独的可测试组件一起使用。


3
弃用并不总是一个问题。从以下问题开始:“假设我开始开发……”,如果该软件尚未发布,则不建议弃用。此外:“角色游戏”意味着这不是可重用的库,而是针对最终用户的二进制软件。虽然某些最终用户软件确实具有公共API(例如MS Office),但大多数没有。即使具有公共API 的软件,只有一部分暴露给插件,脚本(例如具有LUA扩展的游戏)或其他功能。尽管如此,还是值得提出OP描述的一般情况的想法。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.