TDD 单元测试子方法

TDD Unit Test sub-methods

我在是否要为重构另一种方法的产物的方法编写测试时进退两难。

第一个问题,考虑这个情节。

class Puzzle(
    val foo: List<Pieces>,
    val bar: List<Pieces>
) {
    init {
       // code to validate foo
       // code to validate bar
    }
}

我在这里验证构造对象时的参数。此代码是 TDD 的结果。但是对于 TDD,我们编写 fail_test -> pass test -> refactor,在重构时,我将验证器方法转移到助手 class PuzzleHelper.

object PuzzleHelper {

    fun validateFoo() {
         ...
    }

    fun validateBar() {
         ...
    }
}

在这种情况下,我还需要测试 validateFoovalidateBar 吗?

第二个问题

class Puzzle(
    val foo: List<Pieces>,
    val bar: List<Pieces>
) {
    ...

    fun getPiece(inPosition: Position) {
        validatePosition()
        // return piece at position
    }

    fun removePiece(inPosition: Position) {
        validatePosition()
        // remove piece at position
    }
}

object PuzzleHelper {

    ...

    fun validatePosition() {
         ...
    }
}

我还需要为 getPieceremovePiece 编写涉及位置验证的测试吗?

我真的很想熟练使用 TDD,但不知道如何开始。现在我终于一头扎进去了,不在乎前面是什么,我只想要产品质量。希望早日得到您的启迪。

当我们进入 Red -> Green -> Refactor 周期的重构阶段时,我们不应该添加任何新行为。这意味着所有代码都已经过测试,因此不需要新的测试。您可以通过更改重构代码轻松地验证您是否已完成此操作,并观察它是否未通过测试。如果没有,则说明您添加了不应该添加的内容。

在某些情况下,如果在其他地方重用提取的代码,将测试转移到重构代码的测试套件可能是有意义的。

至于第二个问题,这取决于你的设计,以及一些你的代码中没有的东西。例如,我不知道如果验证失败你想做什么。您必须为这些情况添加不同的测试,以防每种方法的验证失败。

我想指出的一件事是,将方法放在静态对象中(class 函数、全局函数,无论您想如何调用它)都会使测试代码变得更加困难。如果您想在忽略验证时测试您的 class 方法(将其存根以使其始终通过),您将无法做到这一点。
我更喜欢创建一个作为构造函数参数传递给 class 的协作者。所以你的 class 现在得到一个 validator: Validator 并且你可以在测试中将任何你想要的传递给它。存根、真品、模拟品等

Do I still need to test validateFoo and validateBar in this case?

视情况而定。

TDD 的部分要点是我们应该能够迭代内部设计;又名重构。这就是让我们从对前期设计的小投资开始并在我们进行时解决其余部分的魔力——事实上我们可以改变事物,并且测试评估变化 而不会妨碍.

当您的系统所需的行为稳定时,这非常有效。

当系统所需的行为不稳定时,当我们有很多decisions在变化时,当我们知道所需的行为正在发生时进行更改,但我们不知道是哪一个...具有跨越许多不稳定行为的单个测试往往会使测试 "brittle".

长期以来,这是自动化 UI 测试的祸根——因为测试 UI 几乎涵盖了系统每一层的每一个决定,测试一直在维护中以消除面对其他微不足道的行为变化时出现的级联误报。

在这种情况下,您可能希望开始研究引入隔板的方法,以防止需求发生变化时造成过度损坏。我们开始编写测试来验证测试对象的行为 与一些更简单的 oracle 的行为相同 ,以及更简单的 oracle 做正确事情的测试。

这也是 TDD 反馈循环的一部分——因为跨越许多不稳定行为的测试很困难,我们重构了支持在孤立粒度上测试行为的设计,以及更简单的更大组合元素。