什么是测试驱动开发?(以及如何正确实施)
测试是任何软件开发项目中最关键的部分之一。创建一套强大而全面的软件测试套件,就能检测出构建代码库时产生的错误、失误和错误假设。通过使用多种不同类型的测试,从单元测试、集成测试到用户界面测试和回归测试,您将更有可能发布更高质量的最终产品。
然而,不幸的现实是,测试往往只是事后诸葛亮,或者仅仅被当作一个需要勾选的选项。开发团队面临着截止日期和预算限制的压力,根本没有时间投入到软件应有的测试中。
为了应对这种趋势,软件开发人员 Kent Beck 重新提出了“测试驱动开发”的概念。本文将深入讨论在构建软件应用程序时使用测试驱动开发,以及这样做的优缺点。
测试驱动开发的定义
顾名思义,测试驱动开发(简称 TDD)是一种将测试放在开发过程首位的软件开发实践。要理解测试驱动开发的定义,我们首先需要定义单元测试,这是 TDD 中的一个重要概念。
什么是单元测试?
单元测试是一种软件测试方法,它将应用程序分解成多个小部分,每个部分称为单元。每个单元都通过一系列专门为该单元编写的全面测试进行单独评估。为了便于调试,单元测试应尽可能简短易懂,以便开发人员更容易发现错误。
例如,假设我们正在为一个计算器应用程序编写代码,并且想要使用单元测试来测试该程序。我们可以为计算器的每个功能编写一个单元测试:加法、减法、乘法、除法、指数运算等。我们还需要为诸如清除计算器屏幕和组合多个运算等操作编写单元测试。
单元测试的目标是在开发生命周期的早期发现错误和问题。大型软件应用程序的单元测试套件可能包含数百或数千个单元测试,每个测试都必须通过才能继续开发。
什么是测试驱动开发?
TDD 是一种将单元测试作为首要关注点的软件编写实践。TDD 是一种高度结构化、高度规范化的软件开发方法。TDD 的原则如下:
- 测试总是在代码编写之前编写,以确保代码通过。测试可以预测代码的正确行为。
- 开发过程每次进行一个测试。一旦一个测试从失败变为通过,就会编写下一个测试,然后开发就可以继续。
- 再次强调,测试应该尽可能简单,长度足以打破应用程序的当前状态。编写测试后,所有开发都必须专注于使软件通过测试。
TDD 以通常称为“红、绿、重构”的循环进行。通常,每个单元测试都会执行一次此循环。该循环包含三个阶段:
- 红色: 编写由于软件缺少功能而失败的单元测试(因为红色通常表示失败)。
- 绿色: 编写代码,使软件通过测试来解决问题(因为绿色通常表示成功)。
- 重构: 清理代码库以解释新代码的存在。
将 TDD 与撰写长篇文章的过程进行比较可能会有所帮助。撰写文章时,首先要创建一个详细的提纲,列出想要涵盖的主题。接下来,根据提纲撰写文章的一部分,并根据刚刚写好的文本对其余部分进行必要的调整。提前构思好结构,最终成品的创作就会变得容易得多。
如何进行测试驱动开发
在上一节中,我们讨论了测试驱动开发的主要思想。现在,我们将提供有关如何在软件开发项目中实施 TDD 的深入指南。
1. 编写测试
当然,TDD的第一步是创建一个单元测试来评估代码库的某个部分。单元测试中的“单元”通常是一个方法、一个类或该类的一个成员函数。
例如,假设我们正在创建一个驾驶模拟器作为教育课程的一部分,并且我们希望用一个 Car 类来表示用户正在驾驶的汽车。这个 Car 类将包含 startCar()、turnOffCar()、changeGear()、changeSpeed() 等方法。它还将包含一些变量来保存汽车的当前状态(开启或关闭)、当前档位和当前速度等信息。
我们要编写的第一个测试是创建 Car 类的实例:
汽车c = 新车();
当然,这个测试将在编译时失败,因为我们还没有编写 Car 类。
TDD 期间编写测试的灵感可以来自用例图和用户故事。
- 用例图是根据用户想要执行的操作来描述系统应如何运行的模型。
- 用户故事是项目主要利益相关者撰写的软件需求的简短文本描述。在我们的驾驶模拟器示例中,两个用户故事可能是“用户可以使用该软件练习平行停车”和“用户可以在各种天气条件下练习”。
2. 运行测试
当我们运行单元测试套件(目前为止只包含一个测试)时,我们会在编译过程中收到一个错误,提示该类不存在。这条错误信息为开发人员提供了解决问题的线索。
在某些情况下,错误会在运行时出现,而不是编译时。您可以使用断言语句来验证程序执行时给定条件是否为真或满足。您还可以抛出异常来检查一个或多个错误条件。
随着 TDD 流程的推进,单元测试套件的末尾会添加更多测试。需要注意的是,在 TDD 中,每个单元测试都应该是一个独立的实体。换句话说,任何测试都不应依赖于之前测试的行为或成功。
3.修复代码
有了相应的错误消息,开发人员就可以着手解决问题了。在这一步,你应该把重点放在编写一个满足测试条件的解决方案上,而不是编写一个完美的解决方案。
例如,在我们的 Car 示例中,修复失败测试所需的代码将是 Car 类的定义:
类汽车{
}
正如我们上面提到的,这段代码是通过我们的第一个测试所需的最少工作量。我们还没有定义任何属于这个类的成员变量或成员函数——因为不需要这样做就能通过测试。
我们编写的附加单元测试将查找 Car 类中所需的函数和变量(例如 turnOn() 和 currentState)。编写每个测试后,我们就可以添加它所测试的函数或变量。
4. 重新运行测试
编码完成后,重新运行测试套件,看看现在是否可以通过测试。例如,在我们基本的 Car 示例中,应用程序将创建对象,然后静默退出。如果一切顺利,并且您遵循 TDD 原则,那么所有测试现在都应该能够通过。
5.重构代码
在此(可选)步骤中,您将重构在步骤 3 中编写的代码,使其与现有代码库集成。这可能包括提升代码可读性、将其拆分成更具逻辑性的部分,以及重命名或移动变量和方法。
6.重复
TDD 应该逐步持续,逐步扩展软件的特性和功能。
在 TDD 测试开发过程中,你可能会发现新的测试难以通过,或者新编写的代码会破坏之前的测试。在这种情况下,TDD 的最佳实践通常是撤销已做的更改,而不是浪费时间进行冗长的调试。
请注意,如果您使用任何第三方库或框架,则无需测试这些外部资源的功能。您只需测试您计划自己编写的代码即可。此外,优秀的库和框架应该已经在其代码库中定义了自己的单元测试。
测试驱动开发的 5 个最佳工具
TDD 是一种流行的编程实践,它在项目开发过程中不乏各种工具来帮助你。以下是一些最佳的测试驱动开发工具:
-
Travis CI:Travis CI 是一款使用持续集成实践来测试和部署代码的工具,这与测试驱动开发 (TDD) 非常契合。持续集成要求开发人员每天多次将代码集成到共享存储库中,并使用自动构建工具进行验证。Travis CI 集成了 GitHub 以及许多数据库和服务,并且对于开源项目完全免费。
-
CircleCI:CircleCI 是另一款持续集成工具,高度可定制,让您可以完全掌控开发和测试流程。CircleCI 支持作业编排和缓存,并且兼容 Docker 以及在 Linux 或 Mac 上运行的任何语言、工具链或框架。
-
Squash:Squash 取代了传统的开发、预发布和 QA 服务器设置,为代码库中的每个代码分支都配备了一个专属虚拟机。您可以根据对每个分支所做的更改预览应用程序的完整运行版本。这让您能够快速轻松地尝试新的测试和功能,并根据需要自动扩展或缩减使用量。
-
Selenium:是一个免费的开源自动化测试框架,专门用于 Web 开发。您可以配置测试,使其模拟不同的桌面环境和 Web 浏览器,并自动生成测试结果报告。
-
Cypress:Cypress 是另一个基于 JavaScript 的开源前端 Web 开发测试框架。具体来说,Cypress 执行端到端测试,确保 Web 应用程序的流程从始至终都正确无误。DHL、Spotify 和 NASA 等组织的 Web 开发人员都使用 Cypress 编写、运行和记录测试。
测试驱动开发的 3 个好处
1.速度
对于能够快速行动的熟练开发人员和测试人员来说,TDD 的一大优势就是速度。通过添加失败的测试,然后修复代码使其通过,TDD 可以促进快速迭代和进步。
TDD 乍一看可能比较慢,但你最初付出的努力终将获得回报。对于软件开发项目来说,没有什么比发现应用程序逻辑存在重大缺陷,需要重构或重写代码更糟糕的了。
通过尽早投入时间提高代码库的质量,TDD 可以节省开发人员的时间和精力,并降低项目失败或延迟的风险。
2. 更容易实现自动化
速度方面的另一个相关好处是,TDD 和单元测试使得软件测试套件的自动化变得更加容易。
手动检查通常非常缓慢,尤其是在执行需要遵循一系列步骤的功能测试时。功能测试过程可能需要几秒钟甚至几分钟才能完成,并且每次对系统进行更改时都必须进行。
另一方面,单元测试速度快,而且极易实现自动化。虽然手动测试和自动化测试在成熟、强大的软件测试程序中都占有一席之地,但自动化测试可以显著加快测试速度。这使得您可以在相同的时间内运行更多测试,从而提高代码质量。
3.更高质量的代码
如果你等到开发周期的后期才测试代码,那很有可能酿成大祸。编写数百或数千行代码而不犯任何错误或拼写错误几乎是不可能的。
即使是顶尖的开发人员,也难免会犯错,犯错只是时间问题。如果不定期测试代码,就更容易出现 bug 和意外行为。
TDD 本质上是一种增量式的软件开发方法。在大多数情况下,开发人员每次只编写几行代码——刚好够当前测试通过即可。这种“慢而稳”的理念可以确保(尽管不能保证)您的软件不包含任何错误。
TDD 的另一个好处是代码本身可以作为文档。例如,如果你想演示某个函数在给定异常输入(例如空字符串或负数)时的行为,你只需编写一个使用该输入的测试即可。
测试驱动开发的 3 个常见陷阱
1. 耗时
好消息是,TDD 似乎为其从业者带来了真正的好处。一项关于 TDD 影响的多项研究调查发现,TDD 可以将缺陷数量减少 40% 到 60%,同时将工作量和执行时间增加 15% 到 35%。
虽然 TDD 通常能带来更高质量的代码,但也必须承认,额外的努力并不总是值得的。TDD 流程需要大量的单元测试开销。除了软件本身之外,创建和维护测试套件也是一项巨大的投资。
因此,即使考虑到传统开发中独立的编码和测试阶段,编写简短且/或简单的软件使用 TDD 也可能需要更长的时间。那些倾向于投入时间进行手动 QA 或缺乏技术资源来实施单元测试的企业,可能并不适合采用 TDD。
2. 灵活性较差
当开发人员想要或需要进行更改时,TDD 带来的开销往往会在项目过程中令人窒息甚至瘫痪。早期架构、设计或测试策略选择不当,在项目后期很难恢复。此时,如果不导致数十甚至数百个现有测试失败,修改代码库可能非常困难,甚至不可能。
面对必须重构代码和测试套件的局面,开发人员陷入了进退维谷的境地。即使是简单的修改也可能耗时耗力,因此您需要权衡在此阶段是否值得继续使用 TDD。
因此,TDD 很难应用于遗留代码库。如果您计划实践 TDD,拥有一支由经验丰富的测试人员和 QA 人员组成的团队至关重要,他们知道如何编写优秀的软件测试。
3. 并非完美的解决方案
TDD 并不能保证你的代码库万无一失,不会出现 bug 和错误。毕竟,TDD 中的测试不是由计算机编写的,而是由容易出错的人编写的。
这意味着测试过程中出现错误的可能性几乎与开发过程中一样高。例如,开发人员可能会忘记编写涵盖软件重要特性或功能的测试,从而导致错误未被发现。一次错误的按键或判断失误都很容易在测试过程中引发问题。
更重要的是,TDD 无法保护您免受理解错误的影响,这种错误是由于开发人员对他们试图解决的问题存在根本性的误解或错误的假设而导致的。
当然,为第一个测试套件编写另一个测试套件是一个愚蠢且不切实际的想法。你能做的最好的事情就是在编写测试时格外小心,并定期检查测试代码。
测试驱动开发与传统开发
TDD 听起来像是一个好主意,但它在软件开发中并不总是常见的做法(甚至在今天也不总是使用)。
按照传统的软件开发模型,项目应该按照一系列连续的、有序的阶段进行:需求收集、分析、设计、编码、测试和部署。这个概念通常被称为 “瀑布”模型,就像一道瀑布从一系列不同高度的岩石上倾泻而下。
事实上,瀑布的比喻非常贴切。瀑布中的水不断向下流动,不可能回到更高的高度。同样,软件开发的瀑布模型通常不鼓励回到之前的开发阶段。所有编码和实现都必须在测试开始之前完成。
发现瀑布方法的缺陷并不难。如果项目进行到一半时出现了新的需求,或者你发现假设存在严重缺陷,你可能别无选择,只能重新开始。因此,使用瀑布模型开发的软件很容易出现延期和预算超支的情况。
与传统的瀑布式开发方法不同,TDD 与目前软件开发人员中盛行的敏捷和精益方法非常契合 。敏捷方法优先考虑灵活性、适应性和客户满意度,而非严格的规章制度。具体来说,敏捷方法采用迭代开发,软件会不断以最新的可部署状态发布,以便获得宝贵的客户反馈。
精益方法论源于汽车制造业的概念。精益采用“即时生产”(JIT)开发的理念,即系统组件的制造或订购恰好及时,以便用于最终产品。JIT减少了维持过剩库存的需求,并确保员工始终致力于为产品增值。
不难看出,TDD、敏捷和精益在软件开发理念上有着相似之处。TDD 以增量方式编写代码和测试,以最少的投入获得反馈并启动新的迭代。事实上,TDD 的实践最初源自极限编程 (XP),这是一种隶属于敏捷的软件开发方法。
测试驱动开发与行为驱动开发
单元测试是 TDD 的重要组成部分,你经常会看到这两个概念被同时提及。在本节中,我们将讨论另一个与 TDD 高度相关的软件测试概念:行为驱动开发(也称为 BDD)。事实上,TDD 和 BDD 非常相似,以至于一些开发人员认为它们是同一事物的两个术语。
与 TDD 类似,BDD 在开发过程中将测试放在首位。然而,BDD 更进一步,它提出了一个问题:“这难道不是正确的测试方式吗?”
BDD 考虑的问题包括:
- 代码的哪些部分应该测试,哪些部分不应该测试
- 如何进行测试
- 如何理解测试失败或通过的原因。
BDD 鼓励使用能够用自然语言描述预期行为的单元测试名称。例如,如果我们使用 BDD 测试计算器应用程序,我们可能会说测试“当给定 1 + 1 时应该(或 does,或 shall)返回 2”。“should/does/shall”结构在 BDD 测试名称中非常常见。
BDD 和 TDD 之间的另一个区别是,BDD 中的测试评估代码的预期行为,而不是实现的细节。
例如,我们的 Car 类可能有一个 speed 变量,在创建 Car 对象时该变量被初始化为 0。我们还编写了一个单元测试,用于验证使用 increaseSpeed() 函数时速度是否正确增加。
在调用 increaseSpeed(10) 后,BDD 不会验证 Car 的速度是否为 10,而是会验证新的速度是否等于 Car 的初始速度加上 10。这有助于单元测试独立于被测代码运行。例如,我们可以在初始化对象时将 Car 的初始速度更改为 20,而不会破坏测试。
最后的想法
TDD 及其近亲 BDD 代表着对传统软件测试方法的突破,传统的软件测试方法仅在编程工作完成后才进行测试。相反,TDD 通过将测试与开发紧密结合,强调测试的价值。
这种新颖的思维方式迫使开发人员了解代码库的每个部分应该如何运行,并帮助他们在开发过程为时已晚之前发现错误。
能够描述软件的预期输出和行为有很多好处。沟通可以得到改善,错误可以减少,关键利益相关者也可以确保他们对项目的要求得到满足。
鏂囩珷鏉yu簮锛�https://dev.to/squash/what-is-test-driven-development-and-how-to-get-it-right-7mj