重构-只要所有测试都通过,简单地重写代码是否合适?


9

我最近看了RailsConf 2014的“ All Little Little”。在这次演讲中,Sandi Metz重构了一个包含大型嵌套if语句的函数:

def tick
    if @name != 'Aged Brie' && @name != 'Backstage passes to a TAFKAL80ETC concert'
        if @quality > 0
            if @name != 'Sulfuras, Hand of Ragnaros'
                @quality -= 1
            end
        end
    else
        ...
    end
    ...
end

第一步是将函数分解为几个较小的函数:

def tick
    case name
    when 'Aged Brie'
        return brie_tick
    ...
    end
end

def brie_tick
    @days_remaining -= 1
    return if quality >= 50

    @quality += 1
    @quality += 1 if @days_remaining <= 0
end

我发现有趣的是这些较小的函数的编写方式。brie_tick例如,并不是通过提取原始tick功能的相关部分来编写的,而是通过参考test_brie_*单元测试从头开始的。一旦所有这些单元测试都通过,brie_tick就认为已经完成。一旦完成所有小功能,原始的整体tick功能即被删除。

不幸的是,演示者似乎没有意识到这种方法导致四个*_tick功能中的三个出现错误(另一个是空的!)。在某些极端情况下,*_tick功能的行为与原始tick功能不同。例如,@days_remaining <= 0in brie_tick应该是< 0-所以brie_tick当用days_remaining == 1和调用时不能正常工作 quality < 50

这里出了什么问题?这是否是测试失败-因为没有针对这些特殊情况的测试?还是重构失败-因为代码应该已经逐步进行了转换,而不是从头开始重写?


2
我不确定我是否知道这个问题。当然可以重写代码。我不确定您的具体含义是“可以简单地重写代码”。如果您问“无需过多考虑即可重写代码是否可以”,答案是否定的,就像用这种方式编写代码并不可行。
John Wu,

发生这种情况的原因通常是测试计划主要侧重于测试成功的用例,而很少(或根本没有)涵盖错误用例或子用例。因此,这主要是覆盖范围的泄漏。测试泄漏。
Laiv

@JohnWu-我的印象是,重构通常是对源代码进行一系列小的转换(“提取方法”等),而不是简单地重写代码(我的意思是从头再写一次,甚至没有查看现有代码,如链接的演示文稿所示)。
user200783 '18

@JohnWu-从头开始重写是可以接受的重构技术吗?如果不是这样,那么看到这样一个备受赞誉的有关重构的演示采用这种方法,将令人失望。OTOH如果可以接受,那么可以将行为的意外更改归咎于缺失的测试-但是有什么办法可以确保测试覆盖所有可能的极端情况?
user200783

@ User200783嗯,这是一个更大的问题,不是吗(我如何确保测试是全面的?)务实地,我可能会在进行任何更改之前运行一个代码覆盖率报告,并仔细检查所有不涉及代码的区域锻炼身体,确保开发团队在重写逻辑时谨记他们。
John Wu,

Answers:


11

这是否是测试失败-因为没有针对这些特殊情况的测试?还是重构失败-因为代码应该已经逐步进行了转换,而不是从头开始重写?

都。仅使用Fowlers原始书中的标准步骤进行重构绝对比重写更容易出错,因此通常更可取的是仅使用此类婴儿步骤。即使没有针对每种极端情况的单元测试,并且即使环境不提供自动重构,单个代码更改(例如“引入解释变量”或“提取函数”)也都具有较小的机会来更改行为的细节。现有代码比完整重写一个功能。

但是,有时候,重写一段代码是您需要或想要做的。如果是这样,则需要更好的测试。

请注意,即使使用重构工具,在更改代码时也始终存在一定的引入错误的风险,而不管应用较小或较大的步骤。这就是重构始终需要测试的原因。还要注意,测试只能减少错误的可能性,而永远不能证明没有错误-尽管如此,使用诸如查看代码和分支覆盖率之类的技术可以使您具有很高的置信度,并且在重写代码节的情况下,通常值得应用这样的技术。


1
谢谢,这很有意义。因此,如果要对行为的不良改变进行最终的解决,那就是进行全面的测试,那么,是否有办法使测试覆盖所有可能的极端情况?例如,可能有100%的覆盖率,brie_tick而仍然从未@days_remaining == 1通过例如@days_remaining设置为10和的测试来测试有问题的案例-10
user200783 '18

2
您永远不能绝对确定测试是否涵盖了所有可能的边缘情况,因为使用所有可能的输入进行测试是不可行的。但是,有很多方法可以使测试更加自如。您可以研究突变测试,这是测试测试有效性的一种方法。
bdsl

1
在这种情况下,开发测试时可能已经用代码覆盖工具捕获了遗漏的分支。
cbojar

2

这里出了什么问题?这是否是测试失败-因为没有针对这些特殊情况的测试?还是重构失败-因为代码应该已经逐步进行了转换,而不是从头开始重写?

使用遗留代码真正困难的一件事是:全面了解当前行为。

没有约束所有行为的测试的遗留代码是常见的模式。这让您有一个猜测:这是否意味着不受约束的行为是自由变量?或未指定的要求?

从谈话中

现在,根据重构的定义,这就是真正的重构。我将重构此代码。我将在不改变其行为的情况下更改其排列。

这是比较保守的方法。如果要求可能没有得到充分说明,或者测试没有涵盖所有现有逻辑,那么您必须非常谨慎地进行操作。

可以肯定地说,您可以断言,如果测试不足以描述系统的行为,则说明您存在“测试失败”。我认为这很公平-但实际上没有用;这是一个普遍存在的普遍问题

还是重构失败-因为代码应该已经逐步进行了转换,而不是从头开始重写?

这个问题是不是相当的变革应该是一步一步的; 但是,由于较高的错误率,重构工具的选择(人为键盘操作员?而不是引导式自动化)与测试范围并不一致。

这可以通过使用具有更高可靠性的重构工具来解决,也可以通过引入更广泛的测试来改善对系统的约束来解决。

因此,我认为您的连词选择不当;ANDOR


2

重构不应更改代码的外部可见行为。这就是目标。

如果单元测试失败,则表明您更改了行为。但是通过单元测试绝不是目标。它或多或少地帮助您实现目标。如果重构改变了外部可见的行为,并且所有单元测试都通过了,那么重构将失败。

在这种情况下,工作单元测试只会给您错误的成功感。但是出了什么问题?两件事:重构是粗心的,并且单元测试不是很好。


1

如果您将“正确”定义为“测试通过”,那么根据定义,改变未测试的行为就不会错。

如果定义特定的边缘行为,请为其添加测试,否则,无需理会会发生什么。如果您真的很学究,则可以编写一个测试来检查true这种情况下的何时情况,以证明您不在乎行为是什么。

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.