单元测试和TDD的目标:找到/最小化错误或改进设计?

时间:2011-07-09 19:02:43

标签: unit-testing debugging tdd

我对单元测试和TDD相当绿色,所以请问我,因为我问一些人可能会考虑新手问题,或者之前是否有过这个问题。如果这被认为是一个“不好的问题”(过于主观和公开辩论),我会高兴地关闭它。但是,我搜索了几天,并没有得到明确的答案,我需要更好地理解这一点,所以我知道没有更好的方法来获取更多信息而不是发布在这里。

我已经开始阅读older book单元测试(因为一位同事手头有它),其开篇章节讨论了为什么要进行单元测试。其中一点是,从长远来看,您的代码更可靠,更清晰,更不容易出错。它还指出,有效的单元测试将使跟踪和修复错误变得更加容易。所以它似乎关注整体预防/减少代码中的错误。

另一方面,我还发现an article关于编写出色的单元测试,并指出单元测试的目标是使您的设计更加健壮,相反,查找错误是手动测试的目标,而不是单元测试。

因此,作为TDD的新手,我对于我应该进入TDD并构建我的单元测试的心态感到有些困惑。我承认,我现在使用我最近开始的项目的部分原因是因为我厌倦了我的更改破坏了以前的现有代码。诚然,上面的链接文章至少指出这是TDD的一个优势。但我希望通过返回并将单元测试添加到我现有的代码(然后从这一点继续TDD)来帮助防止这些错误。

本书和本文是否真的用不同的语气说同样的话,或者在这个主题上有一些主观性,而我所看到的只是两个人对如何接近TDD有不同看法?

提前致谢。

7 个答案:

答案 0 :(得分:5)

单元测试和自动化测试通常用于更好的设计和验证的代码。

单元测试应该在一些非常小的单元中测试一些执行路径。此单元通常是公共方法或在对象上公开的内部方法。该方法本身仍然可以使用来自同一对象实例的许多其他受保护或私有方法。您可以使用单个方法和多个单元测试此方法来测试不同的执行路径。 (通过执行路径,我的意思是由ifswitch等控制的东西。)以这种方式编写单元测试将验证您的代码是否真的符合您的期望。这在某些极端情况下尤其重要,在这些情况下,您希望在某些罕见的情况下抛出异常等。您还可以测试方法在传递不同参数时的行为方式 - 例如null而不是对象实例,整数为负值用于索引等。这对公共API特别有用。

现在假设您测试的方法也使用其他类的实例。怎么处理呢?您是否仍应测试您的单一方法并认为该课程有效?如果课程尚未实施怎么办?如果类内部有一些复杂的逻辑怎么办?您是否应该在当前方法上测试这些执行路径?有两种方法可以解决这个问题:

  • 在某些情况下,您只需让真实的类实例与您的方法一起进行测试。例如,这在日志记录的情况下非常常见(也可以将日志用于测试)。
  • 对于其他方案,您希望从您的方法中获取此依赖项,但如何执行此操作?解决方案是依赖注入和实现反对抽象而不是实现。这是什么意思?这意味着您的方法/类不会创建这些依赖项的实例,而是通过方法参数,类构造函数或类属性来获取它们。它还意味着您不会期望具体实现,而是抽象基类或接口。这将允许您将伪造,虚拟或模拟实现传递给测试对象。这些特殊类型的实现根本不进行任何处理,它们获取一些数据并返回预期结果。这将允许您在没有依赖性的情况下测试您的方法,并导致更好,更可扩展的设计。

缺点是什么?一旦你开始使用fakes / mocks你正在测试单个方法/类,但你没有一个测试会抓住所有真正的实现并将它们放在一起测试整个系统是否真的有效=你可以有数千个单元测试并验证你的每个方法都有效但并不意味着它们会一起工作。这是更复杂测试的场景 - 集成或端到端测试。

单元测试通常应该很容易编写 - 如果它们不是这意味着你的设计可能很复杂,你应该考虑重构。它们也应该非常快速地执行,因此您可以经常运行它们。其他类型的测试可能更复杂,也很慢,它们应该主要在构建服务器上运行。

它如何适应软件开发过程?开发过程中最糟糕的部分是稳定和错误修复,因为这部分很难估计。为了能够估计bug修复的时间,你必须知道导致bug的原因。但这项调查无法估计。您可能有一个小时需要修复的错误,但您将花费两周时间调试应用程序并搜索此错误。使用良好的代码覆盖时,您很可能会在开发过程中尽早发现此类错误。

自动测试并不是说SW不包含错误。它只是说你在开发过程中尽力找到并解决它们,因此你的稳定性可能会更少痛苦,也更短。它也没有说你的SW做了它应该做的事情 - 更多的是关于应用程序逻辑本身,必须通过每个用例/用户故事的一些单独的测试 - 验证测试(它们也可以自动化)。

这如何适合TDD? TDD将其推向极致,因为在TDD中,您将首先编写测试以提高质量,代码覆盖率和设计。

答案 1 :(得分:4)

这是一个错误的选择。 “查找/最小化错误”改进设计。

特别是TDD(与“仅仅”单元测试相反)就是为您提供更好的设计。

当你的设计更好时,后果是什么?

  • 您的代码更易于阅读
  • 您的代码更容易理解
  • 您的代码更容易测试
  • 您的代码更易于重复使用
  • 您的代码更易于调试
  • 您的代码首先出现的错误较少

使用精心设计的代码,您可以减少查找和修复错误的时间,并且可以花更多时间添加功能和润色。因此,通过为您提供更好的设计,TDD可以节省您的错误和寻找bug。这些东西不是分开的;他们是依赖和相互关联的。

答案 2 :(得分:2)

您可能需要测试代码的原因有很多种。我个人测试的原因有很多:

我通常使用常规设计模式(自上而下)和测试驱动开发(TDD;自下而上)的组合来设计API,以确保从最佳实践的角度来看我都有一个合理的API以及从实际使用的角度来看。测试的重点既在于API的主要用例,也在于API的完整性和行为 - 因此它们是主要的“黑盒子”测试。开发顺序通常是:

  • 基于设计模式和“直觉”的主要API
  • 根据API的高级规范对主要用例进行TDD测试 - 主要用于确保API“自然”且易于使用
  • 充实了API和行为
  • 确保完整性和正确行为所需的所有测试用例

每当我在代码中修复错误时,我都会尝试编写测试以确保其保持不变。不知何故,错误进入了我的原始设计并通过了我对代码的原始测试,所以它可能不是那么简单。我注意到许多测试测试都是“写盒子”测试。

为了能够对代码进行任何重大的重新分解,您需要一组广泛的API测试,以确保在重新分解后代码的行为保持不变。对于任何非平凡的API,我希望测试套件到位并在重新分解之前工作很长时间,以确保所有主要用例都以一种好的方式被覆盖。通常情况下,你被迫抛弃大部分“白盒子”测试,因为他们 - 根据定义 - 对内部构件做了太多假设。我通常会尝试尽可能多地“翻译”这些测试,因为相同的非平凡问题往往会在代码重新分解后继续存在。

为了在开发人员之间传输任何代码,我通常也想要一个关注API和主要用例的好的测试套件。所以基本上是初始TDD的测试...

答案 3 :(得分:2)

我认为你的问题的答案是:两者。

你将改进设计,因为有一个关于TDD的特殊事情是很棒的:当你编写测试时,你将自己置于将使用被测系统的客户端代码的位置 - 而这仅仅让你想到确定设计选择。

例如:UI。当你开始编写测试时,你会看到那些God-Forms无法测试,所以你将屏幕背后的逻辑分离到演示者/控制器,你得到MVP / MVC /无论如何。

具有单元测试类和模拟依赖关系的概念将带您进入单一责任原则。每一项SOLID原则都有一点意义。

至于bug,好吧,如果你对你编写的每个类的每个方法进行单元测试(除了属性,非常简单的方法等),你将在开始时捕获大多数错误。编写集成测试,几乎涵盖所有这些测试。

答案 4 :(得分:1)

我会用我之前编写的answer的混音来刺激我。简而言之,我不认为这是驱动良好设计和最小化错误之间的二分法。我认为它更像是一个(好的设计)导致另一个(最小化错误)。

我倾向于说TDD是一个恰好涉及单元测试的设计过程。这是一个设计过程,因为在每个Red-Green-Refactor迭代中,您首先为不存在的代码编写测试。你正在设计,因为你要去。

TDD的第一个美妙之处在于保证代码的设计是可测试的。可测试代码往往具有松散耦合和高内聚力。松散耦合和高内聚非常重要,因为它们使代码在需求变化时易于更改。 TDD的第二个优点是,在您完成系统实现之后,您碰巧有一个巨大的回归套件来捕获任何错误和假设的变化。因此,TDD使您的代码易于更改,因为它创建的设计,并且由于它创建的测试工具,它使您的代码可以安全地更改。

答案 5 :(得分:0)

尝试回顾性地添加单元测试可能非常痛苦且昂贵。如果代码不支持单元测试,您可能更好地查看集成测试来测试代码。

答案 6 :(得分:0)

不要将单元测试与TDD混合使用。 单元测试就是“测试”代码以确保质量和可维护性的事实。

TDD是一个完整的开发方法,您首先编写测试(基于要求),然后您编写所需的代码(并且只需要代码)使测试通过。这意味着您只需编写代码来修复损坏的测试

完成后,您编写另一个测试,并使代码通过。顺便说一下,您可能被迫对代码进行“重构”,以允许新的测试运行而不会制动另一个。这样,“设计”就来自于测试。

此方法的目的当然是减少错误并改进设计,但其主要目标是提高生产力,因为您可以准确编写所需的代码。而且你不写文档:测试是文档。如果需求发生变化,那么之后您将更改测试和代码。如果出现新要求,只需添加新测试。