试想一下:你加入了一个不断发展、拥有数千个测试用例的项目。或者,随着时间的推移,你的项目测试用例已经增长到了数百个。
不同的开发人员在这个代码库上工作,每个人的技能和风格各异,导致最佳实践的应用缺乏一致性。
测试文件分散,命名约定不统一,测试固件(fixture)过载,conftest.py
文件也存在问题,这非但没有带来清晰,反而造成了更多的混乱。
即使调试一个测试用例的失败也需要花费数小时。整个局面一团糟,让人不知所措,你甚至不知道该从哪里入手进行重构。
但事情并非只能如此。
在这篇文章中,我回顾并研究了一些组织测试的最佳实践,旨在让测试工作简单、可扩展且高效。
我还会分享我作为一名 基于 Python提供自动化测试解决方案,在行业内16年+专业经验中所学到的知识。
我们将直面测试无序带来的挑战,并向你展示如何制定一个具有前瞻性的测试策略。
你将学习到如何:
- 避免常见的陷阱,如测试文件过载和整体式测试。
- 利用测试金字塔,有效平衡单元测试、集成测试和端到端测试。
- 构建测试结构,使其与应用程序代码相呼应,并将其清晰地分隔在专用文件夹中。
- 充分发挥 Pytest 的灵活性,通过
conftest.py
文件来组织测试固件、管理测试数据并控制作用域。 - 针对特定框架(如 Django),组织视图、模板和 URL,确保遵循最佳实践。
在本文结束时,你将掌握相关工具和技术,构建一个可扩展、可维护的测试套件,使其能随着项目的发展而发展,让新开发人员能够快速轻松地理解并融入其中。
为什么测试无序会成为问题?
在探讨解决方案之前,我们必须先谈谈问题本身。
无序的测试会给你的开发工作流程带来混乱,让即使是简单的任务也变得不必要地复杂。
- 测试文件或模块过载
在单个测试文件或模块中放入过多的测试用例会导致反馈缓慢,调试难度加大。
- 命名约定不一致 这会造成混淆,新开发人员在查找特定测试用例时会浪费大量时间。
- 全局测试固件过度使用 会导致测试之间相互依赖,使得不相关的测试意外失败。
- 测试固件职责过多 会模糊测试设置和测试逻辑之间的界限,使测试变得脆弱且难以维护。
- 整体式测试 试图一次性完成过多任务的大型、难以处理的测试,尤其难以调试,还会掩盖代码设计不佳和不良实践的问题。
- 不稳定测试 即有时通过、有时失败的测试。忽略或不解决这些问题会让你的团队感到沮丧,削弱对测试套件的信心。
如果没有适当的组织,测试就会成为一种负担,而非资产。
让我们来看看一些最佳实践,了解如何应对这些问题,让你的测试在当下和未来都易于管理。
最佳实践 #1:根据测试金字塔组织测试
开始组织测试的最简单方法之一是从测试金字塔的角度来考虑。
测试金字塔
这里不展开详细介绍,简单来说,就是你要有大量的单元测试、较少的服务或集成测试,以及最少的端到端或功能测试。
这些端到端测试也可以是使用 Selenium、Playwright 或 Cypress 等工具进行的 UI 测试。
UI 测试速度慢且开销大,而单元测试速度快且轻量级。
单元测试
快速、独立的测试,用于验证代码中的小而离散的单元,如函数或类。
- 集成测试 确保应用程序的不同组件能够按预期协同工作,例如测试 API 或数据库交互。
- 端到端测试 模拟真实世界的工作流程,从用户的角度验证整个应用程序的行为。
虽然对于这些测试类型并没有统一的定义,但我鼓励你熟悉各种类型的测试。
按类型对测试进行分组,能让开发人员立即获得一个高层次的视角,有助于确定需要测试的内容以及测试顺序。
典型的文件夹结构可能如下所示:
tests/
├── unit/
│ ├── __init__.py
│ ├── test_user.py
│ ├── test_order.py
│ ├── test_payment_service.py
│ └── test_notification_service.py
│
├── integration/
│ ├── __init__.py
│ ├── test_api_endpoints.py
│ ├── test_database_interactions.py
│ └── test_service_integration.py
│
├── e2e/
│ ├── __init__.py
│ ├── test_user_journey.py
│ ├── test_checkout_flow.py
│ └── test_admin_dashboard.py
│
│
├── conftest.py
└── pytest.ini
Pytest 还允许你使用
@pytest.mark.X
装饰器来标记测试,例如 @pytest.mark.unit
,这非常有用。
你也可以直接使用标记来运行测试,例如:
$ pytest -m unit
如果你需要复习 Pytest 标记的相关知识,可以查看这篇文章。
最佳实践 #2:测试结构应与应用程序代码相呼应
组织测试的另一个良好实践是让测试结构与应用程序的文件夹结构相呼应。
这是一种简单而有效的方法,能够确保测试的清晰性和可维护性。
当你的测试与应用程序的模块和目录相匹配时,在两者之间切换就会变得直观自然,既节省时间,又减少混淆。
例如,如果你的应用程序为模型、服务和控制器分别设置了不同的模块,那么你的测试目录也应该反映这种结构。
每个测试文件专注于对应的模块,这样在代码发生更改时,就更容易定位和更新测试。
这种结构还能帮助新团队成员快速了解测试套件与应用程序之间的关系,使新功能开发和代码重构变得更加容易。
示例结构
如果你的应用程序代码如下所示:
src/
├── models/
│ ├── user.py
│ └── order.py
├── services/
│ ├── payment_service.py
│ └── notification_service.py
└── controllers/
└── order_controller.py
那么你的测试文件夹应该与之呼应:
tests/
├── models/
│ ├── test_user.py
│ └── test_order.py
├── services/
│ ├── test_payment_service.py
│ └── test_notification_service.py
└── controllers/
└── test_order_controller.py
好处
提高可读性
测试与对应的应用程序组件直接对应。
- 便于调试 能够快速定位并修复与特定模块相关的测试。
- 可扩展性 随着应用程序的发展,测试结构自然会随之演变。
- 促进协作 这使得开发人员和 QA 团队更容易协调工作。
通过与应用程序结构相呼应,你的测试套件将成为一个组织有序、直观易用的资源,而不是一堆杂乱无章的文件。
最佳实践 #3:如何分组或组织测试固件
测试固件是可在测试中重复使用的代码片段,用于减少样板代码、提高效率,并处理测试的设置和清理工作。
有效地组织测试固件是维护一个简洁、可扩展的测试套件的关键。
管理不善的测试固件会导致不必要的复杂性、测试速度变慢以及难以追踪的依赖关系。
通过精心分组和设置测试固件的作用域,你可以简化测试流程,提高效率。
以下是对 Pytest 测试固件作用域的快速介绍。如果你已经熟悉这部分内容,可以跳过。
Pytest 的测试固件可以有不同的作用域,这决定了它们的生命周期和复用方式:
函数作用域(默认)
每个测试函数都会创建并销毁该测试固件。适用于特定测试的设置,如临时文件或隔离的数据库状态。
@pytest.fixture def temp_file(): with open("temp.txt", "w") as f: yield f
类作用域
每个测试类会创建一次该测试固件,并在其所有方法中共享。非常适合初始化类中所有测试都要使用的对象或状态。
@pytest.fixture(scope="class")def sample_data(): return {"key": "value"}
模块作用域
该测试固件会在一个模块的所有测试中共享。对于像数据库连接或 API 客户端这样开销较大的设置非常适用。
@pytest.fixture(scope="module") def db_connection(): return connect_to_db()
还有会话作用域,可将测试固件应用于整个测试会话。
集中式与本地化测试固件:找到合适的平衡点
在 Pytest 中管理测试固件时,是选择集中式的测试固件文件夹,还是本地化的 conftest.py
文件,通常取决于项目的规模和复杂程度。
这两种方法各有独特的优势,将它们结合成一种混合策略可以兼得两者之长。
集中式测试固件
使用专门的 fixtures/
文件夹非常适合多个测试目录都依赖的共享资源,如数据库连接或 API 客户端。
将测试固件分组到 fixtures_db.py
或 fixtures_api.py
等模块中,既能保持组织有序,又能促进复用。
tests/
├── fixtures/
│ ├── fixtures_db.py
│ ├── fixtures_api.py
│ └── fixtures_auth.py
├── unit/
│ ├── conftest.py
│ └── test_models.py
├── integration/
│ ├── conftest.py
│ └── test_api.py
但这些方法也有一些权衡之处。
优点
- 集中管理按功能对测试固件进行分组(如
fixtures_db.py
、fixtures_api.py
),使它们组织有序,易于定位(尤其是模拟对象)。 - 可复用性共享的测试固件可以在整个测试套件中使用,无需重复定义。
- 可扩展性适用于拥有大量测试文件和测试固件的大型项目。
- 职责分离更清晰测试固件可以拆分成多个模块,减少单个文件的规模。
- 缺点
- 需要显式导入你必须将测试固件导入到测试中,这可能会让它们的使用不那么 “便捷”。
- 可能过度使用集中式的测试固件可能会导致过度依赖,在不相关的测试之间产生隐藏的依赖关系。
在本地目录中使用 conftest.py
(本地化测试固件)
另一方面,特定测试目录中的 conftest.py
文件非常适合特定测试的设置。
这可以确保作用域较窄的逻辑不会使全局命名空间混乱,也不会影响不相关的测试。
例如:
tests/
├── unit/
│ ├── conftest.py # 特定于单元测试的测试固件
│ └── test_models.py
├── integration/
│ ├── conftest.py # 特定于集成测试的测试固件
│ └── test_api.py
优点
- 隐式发现Pytest 会自动发现
conftest.py
中定义的测试固件,因此无需手动导入。 - 局部作用域测试固件的作用域仅限于特定目录,降低了不相关测试之间产生意外依赖的风险。
- 适合小型项目将测试固件与它们支持的测试紧密关联,便于理解其使用方式。
- 缺点
- 难以扩展随着测试数量的增加,管理多个
conftest.py
文件中的测试固件会变得困难。 - 可能存在重复如果没有精心规划,测试固件可能会在不同目录中重复,导致维护困难。
两全其美的方法
在大型项目中,你可以考虑采用混合方法:
使用 fixtures/
文件夹来存放广泛共享、可复用的测试固件,如数据库连接或模拟对象。
- 使用本地的
conftest.py
文件来存放特定目录或作用域较窄的测试固件,如特定测试的数据或设置逻辑。
tests/
├── fixtures/
│ ├── fixtures_db.py
│ ├── fixtures_api.py
│ └── fixtures_auth.py
├── unit/
│ ├── conftest.py
│ └── test_models.py
├── integration/
│ ├── conftest.py
│ └── test_api.py
这种方法可以使你的测试套件易于维护和扩展。
我喜欢将测试固件放在与目录相关的conftest.py
文件中,并将公共测试固件放在 tests
目录下的 conftest.py
文件中。
了解了如何组织测试固件后,让我们来看看将测试包含在应用程序代码中是否是个好主意,以及哪些因素会影响这个决策。
最佳实践 #4:测试放在应用程序代码外部还是内部
一个关键的决策是将测试放在应用程序代码外部还是内部。
每种方法都有其优点和权衡之处,正确的选择取决于项目的规模、目的和部署策略。
测试放在应用程序代码外部
优点
- 职责分离将测试放在
tests/
文件夹中,可以确保应用程序代码干净,不包含测试相关的内容。 - 清晰和组织性专用文件夹可以实现清晰的层次结构,将单元测试、集成测试和端到端测试分开。
- 避免部署风险在生产部署中排除测试代码可以减少开销,并降低潜在的安全风险。
- 可扩展性适用于拥有大量测试套件的大型项目。
project/
├── src/
│ ├── models/
│ └── services/
├── tests/
│ ├── unit/
│ ├── integration/
│ ├── e2e/
│ └── fixtures/
这种方法最适合生产级应用程序,因为在这些应用程序中,保持测试代码和生产代码的清晰分离至关重要。
测试作为应用程序代码的一部分
优点
- 紧密耦合的测试将测试嵌入到应用程序模块中可以简化库或可复用模块的测试,因为测试与它们要验证的代码放在一起。
- 适用于小型项目对于小型项目或包来说,将所有内容放在一个地方的简单性可能比分离的好处更重要。
- 快速获取上下文开发人员可以快速访问并理解与代码相关的测试。
- 潜在风险
- 部署风险如果没有适当的过滤,测试代码可能会意外包含在生产构建中。
- 代码库混乱嵌入测试会使代码库更难导航,尤其是随着项目的增长。
src/
├── models/
│ ├── user.py
│ └── tests/
│ └── test_user.py
├── services/
│ ├── payment_service.py
│ └── tests/
│ └── test_payment_service.py
你可以通过配置部署管道或打包工具,确保生产构建中排除 tests/
文件夹。
选择正确的方法
何时将测试放在外部
- 大型应用程序,有多个开发人员参与,测试需求复杂。
- 项目中部署风险或生产性能至关重要。
- 需要强大测试套件且职责分离清晰的应用程序。
- 何时将测试嵌入内部
- 用于复用的小型库或独立模块。
- 快速原型或概念验证项目。
- 当访问便捷性和简单性比可扩展性更重要时。
最佳实践 #5:为 Django 应用程序组织测试
Django 应用程序通常涉及多个组件,如模型、视图、模板和序列化器,每个组件都需要进行全面测试。
一些建议包括按视图、模型、模板等对 Django 测试进行分组。
tests/
├── unit/
│ ├── models/
│ │ ├── test_user_model.py
│ │ ├── test_order_model.py
│ │ └── __init__.py
│ ├── serializers/
│ │ ├── test_user_serializer.py
│ │ └── test_order_serializer.py
│ ├── views/
│ │ ├── test_user_views.py
│ │ └── __init__.py
│ ├── templates/
│ │ └── test_user_templates.py
│ └── __init__.py
│
├── integration/
│ ├── test_user_workflow.py
│ ├── test_order_workflow.py
│ └── __init__.py
│
├── e2e/
│ ├── test_full_user_journey.py
│ ├── test_checkout_process.py
│ └── __init__.py
│
├── fixtures/
│ ├── fixtures_db.py
│ ├── fixtures_auth.py
│ └── __init__.py
│
└── conftest.py
这些只是建议,并非严格的规则。你可以查看一些 Django 示例项目以获取更多灵感。
有些公司更喜欢使用 Django 单元测试,并以不同的方式对测试进行分组。
最佳实践是那种对你和你的团队来说易于理解、管理和扩展的方法。
结论
在本文中,你学习了如何将测试套件从混乱无序转变为可扩展、可维护的资产。
我们涵盖了有效组织测试的关键策略,如按测试金字塔分组、与应用程序代码结构相呼应以及精心组织测试固件。
你还了解了如何决定是将测试与应用程序代码打包在一起还是分开,最后,还了解了如何在 Django 应用程序中对测试进行分组。
总体信息很明确:没有一种适用于所有情况的解决方案。
最重要的一步是花时间认真思考你的测试结构。
目标是让它易于理解、维护和扩展。不要害怕随着项目的发展调整你的方法。
现在轮到你将这些原则付诸实践了。
从小处着手 —— 重构几个文件、组织你的测试固件,或者实施测试金字塔。看看哪些方法对你和你的团队有效,然后在此基础上不断迭代。