我是否会为那些重构其他方法的方法编写测试感到困惑。
第一个问题,请考虑以下情节。
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
,在重构时,我将验证器方法转移到了辅助类PuzzleHelper
。
object PuzzleHelper {
fun validateFoo() {
...
}
fun validateBar() {
...
}
}
在这种情况下,我仍然需要测试validateFoo
和validateBar
吗?
第二个问题
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() {
...
}
}
我仍然需要为getPiece
和removePiece
编写涉及职位验证的测试吗?
我真的想精通TDD,但不知道如何开始。现在,我终于潜入水中,而不管未来如何,我想要的只是产品质量。希望很快能收到您的启迪。
答案 0 :(得分:1)
当我们进入Red -> Green -> Refactor
周期的重构阶段时,我们不应该添加任何新行为。这意味着所有代码都已经过测试,因此不需要新的测试。您可以通过更改重构代码来轻松地验证自己是否已完成此操作,并观察其未通过测试。如果没有,则添加了您不应该添加的内容。
在某些情况下,如果提取的代码在其他地方被重用,则将测试转移到重构代码的测试套件中可能是有意义的。
对于第二个问题,这取决于您的设计以及代码中未包含的某些内容。例如,如果验证失败,我不知道您要怎么做。如果每种方法的验证均失败,则必须为这些情况添加不同的测试。
我想指出的一件事是,将方法放置在静态对象(类函数,全局函数,但是您要调用它)中会使测试代码更加困难。如果您想在忽略验证时测试类方法(将其存为始终通过),则将无法执行该操作。
我更喜欢创建一个作为构造函数参数传递给类的协作者。因此,您的班级现在得到一个validator: Validator
,您可以在测试中将您想要的任何内容传递给它。存根,真实的东西,模拟的东西等等。
答案 1 :(得分:1)
在这种情况下,我仍然需要测试validateFoo和validateBar吗?
要视情况而定。
TDD的部分要点是我们应该能够对内部设计进行迭代。 aka重构。正是这种魔力使我们能够从前期设计的少量投资开始,然后逐步进行其余的设计-事实是我们可以更改事物,并且测试可以评估更改而不会造成干扰。
当系统所需的行为稳定时,效果很好。
当系统所需的行为不稳定不稳定时,当我们有大量decisions不断变化时,当我们知道所需的行为将要改变但我们没有不知道哪个...具有跨越许多不稳定行为的单个测试往往会使测试变得“脆弱”。
这在很长一段时间以来一直是自动化UI测试的祸根-因为测试UI几乎涵盖了系统每一层的每个决定,所以测试一直在维护中,以消除面对Windows时出现的误报问题。否则微不足道的行为改变。
在这种情况下,您可能想开始研究引入隔板的方法,这些隔板可以在需求发生变化时防止过度损坏。我们开始编写测试,以验证测试对象的行为与某些简单的oracle行为相同的方式,以及一个测试,该简单的oracle行为正确。
这也是TDD反馈循环的一部分-因为跨越许多不稳定行为的测试非常困难,因此我们重构为支持在孤立颗粒处测试行为的设计,并根据其更简单的元素来支持更大的成分。