- TDD 意味着首先编写失败的测试,然后编写足够的代码来通过,然后进行整理。
- BDD 用共享语言描述行为,通常为“Given”、“When”、“Then”。
- TDD主要指导代码设计。 BDD 主要建立共享理解。
- 他们不是竞争对手,许多团队使用 BDD 来描述功能,使用 TDD 来描述下面的细节。
TDD 与 BDD 的争论常常被认为是选边站。事实上,他们回答不同的问题。测试驱动开发询问代码是否符合开发人员的预期,而行为驱动开发询问团队是否同意软件应该做什么。
为了使差异具体化,我们为每种方法构建了一个小的折扣功能。我们使用标准库在 Python 3.13 上运行代码,下面的结果来自该运行。 BDD 部分使用我们自己的小型运行程序来展示该机制,因此实际项目将使用 Cucumber 等工具或Behavior 来代替。
简短的回答
测试驱动开发(TDD) 是一种编码实践。您编写一个小的失败测试,用最简单的代码使其通过,然后改进设计。测试主要由开发人员用代码编写。肯特·贝克推广了这种做法。
行为驱动开发 (BDD) 是一种源自 TDD 的协作实践,由 Dan North 引入了该术语。开发人员、测试人员和业务人员就系统应如何运行的示例达成一致,用简单的语言编写,然后可以将这些示例自动化。
TDD 与 BDD 对比
| 方面 | 时分双工 | BDD |
|---|---|---|
| 主要焦点 | 正确、设计良好的代码 | 对行为的共同理解 |
| 写于 | 编程语言 | 简单的语言,例如小黄瓜 |
| 撰写者 | 开发商 | 业务、QA 和开发人员一起 |
| 可读者 | 开发商 | 团队中的每个人 |
| 典型水平 | 单位 | 特征或接受 |
| 起点 | 失败的测试 | 举例对话 |
| 循环 | 红、绿、重构 | 发现、制定、自动化 |
| 常用工具 | pytest、JUnit、玩笑 | 黄瓜、行为、SpecFlow、Reqnroll |
特点
以下是我们将实施的业务规则: 1000 件或以上的订单可享受 10% 的折扣。会员订购这些订单可享受额外 5% 的折扣。
这听起来似乎很清楚,但实际上已经很模糊了。会员消费满500元可以享受5折吗?该句子说“那些命令”,所以我们假设不是。请记住这一点,因为这正是 BDD 旨在尽早提出的问题。
使用 TDD 构建它
TDD 遵循一个称为 红色、绿色、重构 的短循环:
| 步骤 | 你做什么 |
|---|---|
| 红色 | 为尚不存在的行为编写一个测试,然后观察它失败 |
| 绿色 | 编写最简单的代码使其通过 |
| 重构 | 改进代码,保持测试绿色 |
这些是我们的测试,每条规则都有一个测试:
class DiscountTests(unittest.TestCase):
def test_no_discount_below_1000(self):
self.assertEqual(discount(999, False), 0)
def test_ten_percent_from_1000(self):
self.assertEqual(discount(1000, False), 100)
def test_member_gets_extra_five_percent(self):
self.assertEqual(discount(1000, True), 150)
然后,我们分三个阶段编写代码,并在每个阶段之后运行测试。严格的 TDD 会话一次添加一个测试,这显示了相同的进展:
第一阶段:最简单的事情
def discount(total, is_member):
return 0
# 3 tests, 2 failed
# test_member_gets_extra_five_percent
# test_ten_percent_from_1000
返回零看起来很愚蠢,但它证明测试实际上可能会失败,并且它通过的一个测试是“无折扣”规则。
第二阶段:添加阈值
def discount(total, is_member):
if total >= 1000:
return total * 0.1
return 0
# 3 tests, 1 failed
# test_member_gets_extra_five_percent
第 3 阶段:添加成员并清理
def discount(total, is_member):
rate = 0.1 if total >= 1000 else 0
if is_member:
rate += 0.05
return round(total * rate, 2)
# 3 tests, 0 failed
三人均通过。但仔细阅读这个版本。它给消费满 500 美元的会员提供 5% 的折扣,这不是我们假设的规则的含义。没有测试涵盖该案例,因此没有任何标记。 TDD 仅保护您想要测试的行为。 这是一个真正的限制,也是关于示例的对话如此重要的原因。
为什么周期很重要
- 红色证明测试有效。 从未失败的测试可能没有测试任何内容。
- 绿色让您保持专注。 您只编写需要的代码,这有助于防止过度设计。
- 重构保持高品质。测试让您有信心安全地更改设计。
- 它影响设计。 难以测试的代码通常耦合得太紧密,您会立即注意到。
我们的 pytest 教程 展示了一种更方便的方法来编写此类测试,边界值分析 解释了为什么我们选择 999 和 1000 作为值。
使用 BDD 构建它
BDD 从对话开始,而不是从工具开始。通常这是一个被称为三人组的简短会议,产品人员、开发人员和测试人员讨论具体示例:
Tester: What about a member spending 500?
Product: Good question. No discount. It has to reach 1000 first.
Dev: And exactly 1000 counts?
Product: Yes, 1000 or more.
那个交换会发现我们的 TDD 代码中的问题。然后,他们以一种名为 Gherkin 的结构化格式记录示例:
Feature: Order discounts
Scenario: Member reaches the discount threshold
Given a member with a cart worth 1000
When they view the amount to pay
Then the amount to pay is 850
Scenario: Member below the threshold gets no discount
Given a member with a cart worth 500
When they view the amount to pay
Then the amount to pay is 500
| 关键词 | 目的 |
|---|---|
| 给定 | 起始情况 |
| 当 | 动作或事件 |
| 然后 | 预期结果 |
团队中的任何人都可以阅读该内容并说“是的,这是正确的”或“不,这是错误的”。即使您从未将其自动化,该场景也已经完成了有用的工作。
将步骤连接到代码
工具将每个简单语言行连接到一个函数,称为步骤定义。为了展示这个想法,我们自己编写了一个小版本并运行了第一个场景:
@step("a member with a cart worth 1000")
def given(ctx):
ctx["cart"] = Cart(is_member=True)
ctx["cart"].add(1000)
@step("they view the amount to pay")
def when(ctx):
ctx["amount"] = ctx["cart"].payable()
@step("the amount to pay is 850")
def then(ctx):
assert ctx["amount"] == 850
# ok Given a member with a cart worth 1000
# ok When they view the amount to pay
# ok Then the amount to pay is 850
真正的工具会添加参数,以便单个步骤可以匹配“1000”和“500”,以及报告。结果是活生生的文档:对系统的简单描述,如果不再真实,就会大声失败。
提示:BDD的价值在于对话。在代码完成后由一个人单独编写 Gherkin 的团队将获得该工具的成本,而几乎没有任何好处。
写出好的场景
| 弱 | 更好 |
|---|---|
当我单击 ID 为“btn-77”的按钮时 |
当我应用优惠券时 |
那么结果是正确的 |
那么支付的金额是850 |
| 一个场景二十个步骤 | 每个场景一种行为 |
| 描述屏幕 | 描述商业意图 |
描述点击和字段 ID 的场景在每次重新设计时都会中断,商业读者不再阅读它们。将用户界面详细信息保留在步骤代码中。这些场景与测试用例与测试场景中的示例非常相似,并且它们也经常用于用户验收测试。
两者一起使用
两者自然地适合“由外向内”的工作流程:
- 团队就行为达成一致,并编写了 BDD 场景。它失败了,因为什么都不存在。
- 开发人员使用 TDD 来构建所需的小型单元。
- 单元测试通过了,最终场景也通过了。
- 一切都在您的 CI/CD 管道 中运行。
将每一个都保持在正确的水平。您应该为重要行为提供一些场景,并为细节提供许多快速单元测试。我们的单元测试与集成测试指南涵盖了平衡。
工具
| 语言 | TDD(单元测试) | BDD |
|---|---|---|
| 蟒蛇 | py测试、单元测试 | 行为,pytest-bdd |
| JavaScript | 开玩笑,维泰斯特 | Cucumber.js |
| 爪哇 | JUnit、测试NG | Cucumber-JVM |
| .NET | xUnit、NUnit | Reqnroll、SpecFlow |
何时使用每个
TDD 非常适合以下情况:
- 您正在编写复杂的逻辑,例如定价、解析或权限。
- 您正在修复一个错误,因为您可以先通过失败的测试来重现它。
- 您正在编写经常更改的代码。
BDD 非常适合以下情况:
- 业务规则很复杂,误解的代价也很高。
- 非技术人员确实会阅读并讨论这些场景。
- 多个团队需要对某些行为的行为有一个共同的定义。
在以下情况下,这两种方法都可能太过分了:您正在编写一次性原型或探索不明确的技术想法。许多开发人员首先进行峰值,然后通过测试重写。如果工程之外没有人会关注这些场景,那么 BDD 工具也是一项糟糕的投资。在这种情况下,具有良好名称的简单测试会更简单。
常见错误
- 将BDD视为测试工具。没有协作的Cucumber只是一种冗长的测试编写方式。
- 测试 TDD 中的实现。与私有细节相关的测试在每次重构期间都会中断。
- 跳过重构步骤。您可以在混乱的代码上通过测试。
- 在代码之后编写测试并称之为 TDD。这仍然有用,但你会失去设计反馈。
- 编写了太多 UI 场景。 这些场景变得缓慢且不稳定。请参阅片状测试。
- 追逐覆盖率数字。 良好的覆盖率仍然会遗漏没有人考虑到的行为,正如我们的示例所示。
最后的话
TDD 帮助您构建正确的事物,而 BDD 帮助您构建正确的事物。我们的折扣示例展示了双方:代码通过了所有测试,但简单的对话揭示了错误的假设。在你的下一个小功能上尝试一下。在编写任何代码之前,与同事一起编写三个具体示例,然后一次构建一个失败的测试。
常见问题
TDD 和 BDD 之间的主要区别是什么?
TDD 专注于开发人员在实施之前编写失败的代码测试。 BDD 专注于整个团队使用通俗易懂的语言示例就行为达成一致。
TDD 和 BDD 可以一起使用吗?
是的,很多团队都这样做。 BDD 场景从外部定义功能,TDD 驱动其下方的较小代码单元。
BDD 比 TDD 更好吗?
总体而言,两者都不是更好,因为它们解决了不同的问题。 BDD 有助于共享理解,TDD 有助于代码设计和快速反馈。
小黄瓜是什么?
Gherkin 是一种用于编写场景的结构化简单语言格式,使用诸如“Given”、“When”和“Then”等关键字。像 Cucumber 这样的工具可以运行它。
TDD 会减慢开发速度吗?
一开始可能会感觉较慢,尤其是在学习时。团队通常可以通过更少的错误和更安全的更改来重新获得时间,尽管效果因项目而异。
继续学习
需要测试、WordPress 或 SEO 方面的帮助?
我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。
更多指南 测试、人工智能和现代开发
喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。
人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它
人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。
阅读文章 →DevOps环境变量解释:.env 文件、秘密和配置正确完成
使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。
阅读文章 →软件开发API 速率限制解释:算法、标头和处理 429 错误
API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。
阅读文章 →测试自动化Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误
使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。
阅读文章 →软件测试代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义
用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。
阅读文章 →数据库与 SQLSQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们
DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。
阅读文章 →数据库与 SQLSQL 视图解释:创建、使用和避免陷阱(附示例)
SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。
阅读文章 →软件开发TypeScript 接口与类型:真正的差异,使用编译器进行测试
TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。
阅读文章 →软件开发Python *args 和 **kwargs 解释:灵活的函数和清晰的示例
Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。
阅读文章 →软件开发Python Lambda 函数:语法、实际用途以及何时不使用它们
Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。
阅读文章 →人工智能什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环
什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。
阅读文章 →DevOps监控与可观察性:真正的区别,包括日志、指标和跟踪
监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。
阅读文章 →DevOpsDocker Volumes 与 Bind Mounts:以正确的方式保留容器数据
Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。
阅读文章 →测试自动化TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式
TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。
阅读文章 →软件开发Git merge 与 rebase 的区别:示例及如何选择
用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。
阅读文章 →软件开发Python 列表推导式:语法、示例与何时避免使用
Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。
阅读文章 →数据库与 SQLSQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)
通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。
阅读文章 →