当我重构时,如何确保不破坏测试代码?

时间:2011-12-19 21:57:49

标签: unit-testing refactoring tdd

代码不断发展,如果没有修剪,它也会衰减,在这方面有点像花园。修剪意味着重构以使其实现其不断发展的目的。

如果我们有良好的单元测试覆盖率,重构会更安全。 测试驱动的开发迫使我们在生产代码之前首先编写测试代码。因此,我们无法测试实现,因为没有。这使得重构生产代码变得更加容易。

TDD周期是这样的:编写测试,测试失败,编写生产代码直到测试成功,重构代码。

但是从我所看到的,人们重构了生产代码,而不是测试代码。随着测试代码的衰减,生产代码将变得陈旧,然后一切都走下坡路。因此,我认为有必要重构测试代码。

问题在于:如何确保在重构​​时不破坏测试代码?

(我已经采用了一种方法https://thecomsci.wordpress.com/2011/12/19/double-dabble/,但我认为可能有更好的方法。)

显然有一本书http://www.amazon.com/dp/0131495054,我还没读过。

还有一个关于此的Wiki页面http://c2.com/cgi/wiki?RefactoringTestCode,它没有解决方案。

5 个答案:

答案 0 :(得分:2)

我认为你不应该改变你的测试代码。

为什么呢? 在TDD中,您可以为类定义接口。 此界面包含使用一组功能定义的方法。要求 / 设计

首先:重构生产代码时,这些要求不会改变。重构意味着:在不改变功能的情况下更改/清理代码 第二:测试检查某组功能,此设置保持不变。

结论:重构测试和重构生产代码是两回事。

提示:编写测试时,请编写干净的代码。做小测试。这真的测试了一部分功能。

但是“由于对要求的无法预料的变化,您的设计会发生变化”。 可能引导或可能不会导致界面发生变化 当您的需求发生变化时,您的测试必须更改。这是不可避免的 您必须记住,这是一个 TDD周期。首先测试新功能并删除旧功能测试。然后实施新设计。

要使其正常工作,您需要进行干净且小巧的测试。 例如:

MethodOne does: changeA and changeB

Don't put this in 1 unit test, but make a test class with 2 unit tests.
Both execute MethodOne, but they check for other results (changeA, changeB).

When the specification of changeA changes, you only need to rewrite 1 unit method.
When MethodOne gets a new specification changeC: Add a unit test.

通过上面的示例,您的测试将更灵活,更容易更改。

摘要:

  • 在重构生产代码时,不要重构您的测试。
  • 编写干净灵活的测试。

希望这会有所帮助。 祝你好运。

@disclaimer:如果这让你变得富有,我不想要你的钱。

答案 1 :(得分:2)

重构测试需要两个步骤。简单说明:首先,您必须使用正在测试的应用程序,以确保测试在重构时通过。然后,在重构的测试为绿色后,您必须确保它们会失败。但要正确地执行此操作需要一些特定的步骤。

为了正确测试重构的测试,您必须更改测试中的应用程序以使测试失败。只有那个测试条件才会失败。这样,除了传递之外,您还可以确保测试失败。您应该努力尝试单个测试失败,但在某些情况下这是不可能的(即不是单元测试)。但是,如果您正确地进行重构,则重构测试中将出现单个故障,并且其他故障将存在于与当前重构无关的测试中。需要了解您的代码库才能正确识别此类型的级联故障,此类型的故障仅适用于单元测试以外的测试。

答案 2 :(得分:1)

庵。

FOR JAVA SOLUTION!我不知道你在编写什么语言!

好吧,我刚读过一篇Martins的“清洁代码”,这本书认为重构测试代码以保持清洁和可读的想法是个好主意,确实是一个目标。因此,重构和保持代码清洁的雄心是好的,而不是像我最初想的那样愚蠢的想法。

但那不是你问的问题,所以让我们来回答吧!

无论如何,我会保留你的测试数据库 - 或最后的测试结果。 通过一些java注释,你可以做这样的事情:

@SuperTestingFramerworkCapable
public class MyFancyTest {

   @TestEntry
   @Test
   public testXEqualsYAfterConstructors(){
      @TestElement
      //create my object X

      @TestElement
      //create my object Y

      @TheTest
      AssertTrue(X.equals(Y));
   }
}

无论如何,你还需要一个反射和注释处理超类来检查这段代码。它可能只是你处理过程中的一个额外步骤 - 编写测试,通过这个超级处理器,然后,如果它通过,运行测试。 而你的超级处理器将使用架构 MyFancyTest

对于你班上的每个成员,它将使用一个新表 - 这里(唯一)表将是testXEqualsYAfterConstructors 该表将为每个标有@TestElement注释的项目提供列。它还有一个@TheTest专栏 我想你只需要调用列TestElement1,TestElement2等

然后,一旦它完成了所有这些,它只会保存变量名称和注释@TheTest的行。 所以表格是

testXEqualsYAfterConstructors
TestElement1     |  TestElement2     |  TheTest
SomeObjectType X |  SomeObjectType X |  AssertTrue(X.equals(Y));

因此,如果超级处理器去了并且发现表存在,那么它可以将已经存在的表与现在的代码进行比较,并且它可以为每个不同的条目引发警报。您可以创建一个新用户 - 一个管理员 - 谁可以获得更改,并可以检查它们,坩埚风格,以及好或不好。

然后你可以为这个问题推销这个解决方案,给你公司卖100M并给我20%

喝彩!

缓慢的一天,这是理性的: yuor解决方案在实际生产代码中使用额外开销的 lot ,最具破坏性。您的prod代码不应该与您的测试代码绑定,并且它当然不应该具有测试特定的随机变量。 我对您提供的代码的下一个建议是,您的框架不会阻止人们破坏测试。毕竟,你可以拥有:

@Test
public void equalsIfSameObject()
{
    Person expected = createPerson();
    Person actual = expected;

    check(Person.FEATURE_EQUAL_IF_SAME_OBJECT);
    boolean isEqual = actual.equals(expected);

    assertThat(isEqual).isTrue();
}

但是,如果我在测试类的某些“重构”中更改最后两行代码,那么您的框架将报告成功,但测试将不会执行任何操作。你真的需要确保提出警报,人们可以看到“差异”。

然后,您可能只想使用svn或perforce和crucible来比较和检查这些东西!

另外,当你热衷于一个新主意时,你会想要阅读有关本地注释的内容:http://stackoverflow.com/questions/3285652/how-can-i-create-an-annotation-处理器 - 即流程-A-局部变量

嗯,所以你可能需要得到那个人 - 看看上面链接中的最后一条评论 - 你可能也需要他的自定义java编译器。

@Disclaimer 如果你创建一家新公司,其代码几乎遵循上述规则,那么在我选择的时候,如果你的价值超过30M,我保留20%的公司权利

答案 3 :(得分:1)

  

如何确保在重构​​时不破坏测试代码   它?

在大多数情况下,重新运行测试就足够了。

还有其他一些策略here,但与您获得的好处相比,它们可能过度。

答案 4 :(得分:1)

大约两个月前,您的问题是我在重构方面的主要问题之一。让我解释一下我的经历:

  • 当你想重构一个方法时,你应该用单元测试(或任何其他测试)来覆盖它,以确保你在重构期间没有破坏某些东西(在我的情况下,团队知道代码运行良好因为它们已经使用它6年了,他们只需要改进它,所以我的所有单元测试都在第一步完成。)所以在第一步你有一些通过单元测试,涵盖整个场景。如果某些单元测试失败,首先应该解决问题,以确保您的方法正常工作。

  • 在通过所有测试后,您已经重构了该方法,并且您希望运行测试以确保每件事情都是正确的。测试代码有任何变化吗?

  • 您应该编写独立于方法内部结构的测试。重构后,您应该只更改一小部分代码,并且在大多数情况下不需要进行任何更改,因为重构只是改进了结构并且不会改变行为。如果你的测试代码需要改变很多,你就不会知道你是否在重构期间破坏了主代码上的一些东西。

  • 对我来说最重要的是要记住每次测试都应该考虑一种行为

我希望我能解释清楚。