Plane API 测试体系实战:pytest 三层测试架构、认证 Fixture 与覆盖率门禁 Plane API 测试体系实战pytest 三层测试架构、认证 Fixture 与覆盖率门禁【免费下载链接】plane Open-source Jira, Linear, Monday, and ClickUp alternative. Plane is a modern project management platform to manage tasks, sprints, docs, and triage.项目地址: https://gitcode.com/GitHub_Trending/pl/planePlane开源项目管理平台Jira/Linear 的开源替代品的 API 端基于 Django Django REST Framework 构建位于apps/api目录下。本篇以 测试目录 README 为主体结合 pytest.ini、run_tests.py、conftest.py 等真实源码讲清 Plane 如何组织 unit / contract / smoke 三层测试、如何区分外部 API 与 Web 应用 API 两套认证客户端以及如何本地运行、用 Docker 跑全量套件并强制 90% 覆盖率门禁。读完本文你可以独立为 Plane API 新增符合规范的测试并完整复现其 CI 同款测试环境。一、测试目录结构与 pytest 配置Plane 的 API 测试全部放在 apps/api/plane/tests/ 下目录按测试类型划分apps/api/plane/tests/ ├── unit/ # 单元测试models、serializers、utils、middleware、views、bg_tasks、settings ├── contract/ │ ├── api/ # 契约测试外部 API/api/v1/ │ └── app/ # 契约测试Web 应用 API/api/ ├── smoke/ # 冒烟测试真实 HTTP 服务 ├── conftest.py # 认证、用户、工作区等通用 fixture ├── conftest_external.py # Redis / Elasticsearch / Celery mock fixture ├── factories.py # factory_boy 测试数据工厂 ├── TESTING_GUIDE.md # 测试编写指南 └── README.md # 测试体系总览README 将测试组织为三大类单元测试隔离测试单个函数或类、契约测试测试组件间交互、验证 API 契约再细分为 API 测试与 App 测试、冒烟测试验证应用可以正常运行的基础测试。这些分类通过 pytest marker 落地。pytest.ini 中声明了 4 个 marker 及全局运行参数[pytest] DJANGO_SETTINGS_MODULE plane.settings.test python_files test_*.py python_classes Test* python_functions test_* markers unit: Unit tests for models, serializers, and utility functions contract: Contract tests for API endpoints smoke: Smoke tests for critical functionality slow: Tests that are slow and might be skipped in some contexts addopts --strict-markers --reuse-db --nomigrations -vs几个关键点的源码依据--strict-markers意味着使用未声明的 marker 会直接报错测试必须用pytest.mark.unit等已声明标记归类--reuse-db与--nomigrations表示复用测试数据库并跳过迁移执行显著加快本地运行速度run_tests.py 在拼装命令时也会追加这两个参数测试专用的 settings 模块plane.settings.test在 plane/settings/test.py 中定义继承common配置将DEBUG置为True、邮箱后端换成locmem邮件进内存假收件箱避免真实发信并把plane.tests注册为 INSTALLED_APP。二、核心设计外部 API 与 Web 应用 API 的双端点体系这是 Plane 测试体系中最需要先理解的一点。README 明确说明 Plane 有两套 API 端点测试时必须选用对应的客户端 fixture维度外部 APIplane.apiWeb 应用 APIplane.appURL 前缀/api/v1//api/认证方式API KeyX-Api-Key请求头Session 会话认证CSRF 已禁用设计目的面向外部 API 契约与第三方接入面向 Web 前端应用测试客户端api_key_clientsession_client测试文件位置contract/api/contract/app/对应的 fixture 定义在 conftest.py 中实现非常直白pytest.fixture def api_key_client(api_client, api_token): Return an API key authenticated client for external API testing api_client.credentials(HTTP_X_API_KEYapi_token.token) return api_client pytest.fixture def session_client(api_client, create_user): Return a session authenticated API client for app API testing, which is what plane.app uses api_client.force_authenticate(usercreate_user) return api_clientapi_key_client通过credentials(HTTP_X_API_KEY...)注入 API 令牌模拟第三方携带X-Api-Key头访问/api/v1/session_client用 DRF 的force_authenticate直接绑定用户身份模拟前端 session 登录态访问/api/。URL 反向解析同样有命名空间区别README「Writing Tests」第 3 条外部 API 用reverse(api:endpoint_name)Web 应用 API 用reverse(endpoint_name)。一个真实契约测试示例可以印证这套用法。contract/api/test_issues.py 是一组针对 Issue 列表端点排序参数注入漏洞GHSA-p885-6jpg-cr2p的回归测试pytest.mark.contract class TestIssueListOrderByInjection: Regression tests for GHSA-p885-6jpg-cr2p on the work-item list endpoint: GET /api/v1/workspaces/{slug}/projects/{project_id}/issues/. def get_url(self, workspace_slug, project_id): return f/api/v1/workspaces/{workspace_slug}/projects/{project_id}/issues/ pytest.mark.django_db def test_invalid_order_by_does_not_500(self, api_key_client, workspace, project, issue): url self.get_url(workspace.slug, project.id) response api_key_client.get(url, {order_by: not_a_field}) assert response.status_code status.HTTP_200_OK可以看到完整的规范组合pytest.mark.contract归类 pytest.mark.django_db标记数据库访问 api_key_client认证 直接拼写/api/v1/URL 纯assert断言。三、Fixture 体系conftest.py、conftest_external.py 与 factories.pyREADME 的「Fixtures」与「Test Fixtures」两节列出的 fixture在源码中的定义位置如下Fixture作用定义位置api_client未认证的 DRF APIClientconftest.py#L19-L22user_data/create_user标准测试用户数据 / 创建测试用户conftest.py#L25-L46api_token为测试用户创建 API Tokenconftest.py#L49-L57api_key_client带 API Key 认证的客户端外部 API 测试用conftest.py#L60-L64session_client带会话认证的客户端App API 测试用conftest.py#L67-L71workspace创建 Test Workspace 及其 Admin 成员conftest.py#L125-L139plane_server真实运行的 Django 测试服务器冒烟测试用conftest.py#L116-L122其中plane_server是对 pytest-django 内置live_server的重命名封装用于避免命名冲突冒烟测试通过它拿到plane_server.url后用真实 HTTP 库requests发起请求走完整的网络与中间件链路。例如 smoke/test_auth_smoke.py 对登录端点做错误密码与正确密码两条路径的断言并通过reverse(sign-in)解析 Web 应用 API 的 URL。外部服务 mockconftest_external.pyREADME「External Dependencies」一节要求与外部服务交互的组件测试应使用mock_redis、mock_elasticsearch、mock_celeryfixture更全面的契约测试则可选用 Docker 测试容器。这三个 mock 的 patch 点在 conftest_external.py 中定义mock_redispatchplane.settings.redis.redis_instance返回预配置好get/set/delete/exists/ttl行为的MagicMockmock_elasticsearchpatchelasticsearch.Elasticsearch客户端预置索引、搜索、增删改的返回值mock_celerypatchcelery.app.task.Task.delay阻止后台任务真实执行并返回固定任务 ID。这种「在单测与多数契约测试中打桩、在需要时用真实容器」的两级策略让绝大多数测试不依赖任何外部基础设施。测试数据工厂factories.pyfactories.py 基于 factory_boy 提供 5 个 DjangoModelFactoryUserFactory、WorkspaceFactory、WorkspaceMemberFactory、ProjectFactory、ProjectMemberFactory。几个值得注意的实现细节UserFactory声明django_get_or_create (email,)同一邮箱重复创建时不会报错WorkspaceFactory的owner用SubFactory(UserFactory)自动级联创建所有者ProjectFactory的created_by使用SelfAttribute(workspace.owner)保证项目创建者与所属工作区所有者一致成员角色默认role 20Admin。TESTING_GUIDE.md 给出的典型用法from plane.tests.factories import UserFactory, WorkspaceFactory user UserFactory() workspace WorkspaceFactory(owneruser) users UserFactory.create_batch(5)四、运行测试本地命令、辅助脚本与 Docker 全量套件1. 直接用 pytest 运行README 给出的原始命令均可在apps/api目录下执行工作目录即 pytest.ini 所在目录# 运行全部测试 python -m pytest # 运行单元测试 python -m pytest plane/tests/unit/ # 运行 API 契约测试 python -m pytest plane/tests/contract/api/ # 运行 App 契约测试 python -m pytest plane/tests/contract/app/ # 运行冒烟测试 python -m pytest plane/tests/smoke/2. run_tests.py 辅助脚本run_tests.py 是一个 argparse 封装的命令行工具参数含义如下对照源码逐一定义参数作用生成的 pytest 行为-u/--unit只跑单元测试-m unit-c/--contract只跑契约测试-m contract-s/--smoke只跑冒烟测试-m smoke-o/--coverage生成覆盖率报告--covplane --cov-reportterm --cov-reporthtml-p/--parallel并行执行-n autopytest-xdist-v/--verbose详细输出追加-vREADME 中的示例# 运行全部测试 ./run_tests.py # 只运行单元测试 ./run_tests.py -u # 运行契约测试并输出覆盖率报告 ./run_tests.py -c -o # 并行运行测试 ./run_tests.py -p一个 README 未明说但源码里存在的质量门禁当启用-o时脚本会在 pytest 结束后额外执行python -m coverage report --fail-under90覆盖率低于 90% 会以非零码退出见 run_tests.py#L58-L65。这与 README「Best Practices」第 7 条「模型、序列化器与业务逻辑目标覆盖率 90%」互相印证。3. Docker Compose 隔离环境仓库根目录的 docker-compose-test.yml 提供了一键全量运行方案apps/api/tests/RUNNING_TESTS.md 给出了完整流程它启动 Postgres、ValkeyRedis、RabbitMQ、MinIO 四个依赖服务数据目录基于 tmpfs每次运行从干净状态开始。# 前置生成 env 文件复制 apps/api/.env.example → apps/api/.env ./setup.sh # 全量运行从仓库根目录 docker compose -f docker-compose-test.yml up \ --build \ --abort-on-container-exit \ --exit-code-from api-tests # 过滤运行只跑 unit marker docker compose -f docker-compose-test.yml run --rm --build api-tests pytest -m unit # 单文件运行 docker compose -f docker-compose-test.yml run --rm api-tests \ pytest plane/tests/unit/models/test_workspace.py -vv # 拆除 docker compose -f docker-compose-test.yml down -vapi-tests服务从 apps/api/Dockerfile.dev 构建并安装requirements/test.txt通过depends_on的service_healthy条件等待四个依赖全部就绪后才启动 pytest--exit-code-from api-tests把 pytest 退出码透传给 CI。4. 覆盖率报告README 给出的手动生成方式python -m pytest --covplane --cov-reportterm --cov-reporthtml执行后终端输出文本报告同时 HTML 报告写入htmlcov/目录。五、编写测试的规范来自 README 与 TESTING_GUIDEREADME「Writing Tests」与「Best Practices」两节合并起来的完整规范如下均可在仓库现有测试中找到对应实例按类型放入正确目录unit/、contract/api/、contract/app/、smoke/按被测 API 选择客户端/api/v1/用api_key_client/api/用session_client真实 HTTP 冒烟用plane_serverURL 反向解析使用正确命名空间外部 API 为reverse(api:endpoint_name)Web 应用 API 为reverse(endpoint_name)访问数据库的测试必须加pytest.mark.django_db加 marker 归类pytest.mark.unit/pytest.mark.contract/pytest.mark.smoke另有slow使用 pytest 的assert语法而不是 Django 的self.assert*方法用 fixture 代替 setUp/tearDown代码更干净、更可复用用 mock fixture 隔离外部依赖Redis / Elasticsearch / Celery避免外部服务依赖写聚焦的测试每个测试只验证一个行为或边界情况测试文件按组件或端点保持小而有序模型、序列化器、业务逻辑目标覆盖率 90%。三类测试的最小骨架取自 TESTING_GUIDE.md# 单元测试 pytest.mark.unit class TestMySerializer: def test_serializer_valid_data(self): data {field1: value1, field2: 42} serializer MySerializer(datadata) assert serializer.is_valid() assert serializer.validated_data[field1] value1# 契约测试 pytest.mark.contract class TestMyEndpoint: pytest.mark.django_db def test_my_endpoint_get(self, auth_client): url reverse(my-endpoint) response auth_client.get(url) assert response.status_code status.HTTP_200_OK assert data in response.data# 冒烟测试真实 HTTP pytest.mark.smoke class TestCriticalFlow: pytest.mark.django_db def test_login_flow(self, plane_server, create_user, user_data): url f{plane_server.url}/api/auth/signin/ response requests.post(url, json{ email: user_data[email], password: user_data[password], }) assert response.status_code 200 assert access_token in response.json()六、存量测试迁移说明README 最后一节「Migration from Old Tests」指出仍有一部分旧格式测试位于api/目录下对应仓库中 apps/api/tests/ 及历史目录这些测试需要逐步迁移到上文所述的contract/新结构中。从 apps/api/tests/RUNNING_TESTS.md 可以看到当前 CI 主入口是 Docker Compose 套件而plane/tests/新结构是持续演进的主体——从contract/app/下大量以_app.py结尾、聚焦项目/工作区作用域鉴权的测试文件如test_workspace_app.py、test_project_app.py也能看出契约测试目前主要承担的是权限作用域与回归防护职责。参考文件索引文件内容apps/api/plane/tests/README.md测试体系总览本文主体apps/api/plane/tests/TESTING_GUIDE.md三类测试编写示例与最佳实践apps/api/pytest.inimarker 声明与全局 pytest 参数apps/api/run_tests.py测试运行辅助脚本与 90% 覆盖率门禁apps/api/plane/tests/conftest.py认证与数据 fixture 定义apps/api/plane/tests/conftest_external.pyRedis / Elasticsearch / Celery mockapps/api/plane/tests/factories.pyfactory_boy 数据工厂apps/api/plane/settings/test.py测试专用 Django settingsdocker-compose-test.ymlDocker 全量测试编排apps/api/tests/RUNNING_TESTS.mdDocker 环境运行与排障指南【免费下载链接】plane Open-source Jira, Linear, Monday, and ClickUp alternative. Plane is a modern project management platform to manage tasks, sprints, docs, and triage.项目地址: https://gitcode.com/GitHub_Trending/pl/plane创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考