Prowler 全组件 TDD 工作流实战指南:在 UI、SDK、API 三栈中落地 RED-GREEN-REFACTOR
2026/9/15 1:37:37 网站建设 项目流程

Prowler 全组件 TDD 工作流实战指南:在 UI、SDK、API 三栈中落地 RED-GREEN-REFACTOR

【免费下载链接】prowlerProwler is the world’s most widely used open-source cloud security platform that automates security and compliance across any cloud environment.项目地址: https://gitcode.com/GitHub_Trending/pr/prowler

本篇指南完整解析 Prowler 仓库内置的 TDD 技能文档:它以 "RED → GREEN → REFACTOR" 为骨架,规定了一套面向ui/(TypeScript/React + Vitest)、prowler/(Python + pytest + moto)、api/(Python/Django + pytest)三大组件的强制式测试驱动开发流程。读完本文,你将掌握在每个组件中按 "Phase 0 评估 → Phase 1 RED → Phase 2 GREEN → Phase 3 三角验证 → Phase 4 REFACTOR" 的完整节奏编写测试与实现,并能在仓库真实源码与测试用例中找到每一步的落点。

为什么 Prowler 需要一套"强制"的 TDD 工作流

Prowler 是开源的云安全与合规评估平台,代码库横跨三种技术栈:

工作目录技术栈测试运行器测试文件模式配套技能
ui/TypeScript / ReactVitest + Testing Library (RTL)*.test.{ts,tsx}(与被测文件同目录)vitest技能
prowler/Pythonpytest + moto*_test.py(后缀),位于tests/prowler-test-sdk技能
api/Python / Djangopytest + djangotest_*.py(前缀),位于api/src/backend/**/tests/prowler-test-api技能

skills/tdd/SKILL.md在 front-matter 中声明:该工作流在"实现功能、修复 Bug、重构、处理任务、修改组件"时始终触发auto_invoke),适用范围覆盖root, ui, api, prowler四个 scope,属于 mandatory(强制)流程而非可选建议。它的核心立场是:

关键问题不是"我是否应该写测试",而是"我需要哪些测试"。

这意味着每写一行生产代码之前,都必须先有测试来"驱动"它。

核心原则:TDD 循环与三条定律

整个流程围绕一个闭环展开:

+-----------------------------------------+ | RED -> GREEN -> REFACTOR | | ^ | | | +------------------------+ | +-----------------------------------------+
  • RED:先写一个失败的测试;
  • GREEN:写最小代码让它通过;
  • REFACTOR:在不改变行为的前提下改善代码质量,测试全程保持绿色。

支撑这个循环的是经典的 TDD 三定律:

  1. 在拿到失败的测试之前,不写任何生产代码
  2. 不多写一行测试——只写足以让测试失败的那部分;
  3. 不多写一行生产代码——只写足以让测试通过的那部分。

Phase 0:动手之前,先评估现状(永远第一步)

无论从哪个组件开始,写任何代码之前都要先回答三个问题:已有哪些测试?覆盖是否足够?缺什么?

UI(ui/

# 1. 查找已有测试(fd 工具,按组件目录过滤) fd "*.test.tsx" ui/components/feature/ # 2. 查看覆盖率 pnpm test:coverage -- components/feature/ # 3. 阅读已有测试,理解既有风格

SDK(prowler/

# 1. 查找已有测试 fd "*_test.py" tests/providers/aws/services/ec2/ # 2. 单独运行某个测试目录 uv run pytest tests/providers/aws/services/ec2/ec2_ami_public/ -v # 3. 阅读已有测试

API(api/

# 1. 查找已有测试 fd "test_*.py" api/src/backend/api/tests/ # 2. 单独运行某个测试文件 uv run pytest api/src/backend/api/tests/test_models.py -v # 3. 阅读已有测试

评估结果会导向同一棵决策树:

+------------------------------------------+ | Does test file exist for this code? | +----------+-----------------------+-------+ | NO | YES v v +------------------+ +------------------+ | CREATE test file | | Check coverage | | -> Phase 1: RED | | for your change | +------------------+ +--------+---------+ | +--------+--------+ | Missing cases? | +---+---------+---+ | YES | NO v v +-----------+ +-----------+ | ADD tests | | Proceed | | Phase 1 | | Phase 2 | +-----------+ +-----------+

值得注意的是:UI 的测试采用**同目录(co-located)**放置,而 SDK 测试统一放在tests/目录、API 测试放在api/src/backend/api/tests/目录。从仓库实际内容看,UI 组件测试、SDK 服务测试、API 认证测试 都严格遵循了这一布局约定。

Phase 1:RED —— 先写一个注定失败的测试

新功能:先定义期望行为

UI(Vitest)
describe("PriceCalculator", () => { it("should return 0 for quantities below threshold", () => { // Given const quantity = 3; // When const result = calculateDiscount(quantity); // Then expect(result).toBe(0); }); });
SDK(pytest)
class Test_ec2_ami_public: @mock_aws def test_no_public_amis(self): # Given - No AMIs exist aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) with mock.patch("prowler...ec2_service", new=EC2(aws_provider)): from prowler...ec2_ami_public import ec2_ami_public # When check = ec2_ami_public() result = check.execute() # Then assert len(result) == 0
API(pytest-django)
@pytest.mark.django_db class TestResourceModel: def test_create_resource_with_tags(self, aws_provider): # Given provider = aws_provider tenant_id = provider.tenant_id # When resource = Resource.objects.create( tenant_id=tenant_id, provider=provider, uid="arn:aws:ec2:us-east-1:123456789:instance/i-1234", name="test", region="us-east-1", service="ec2", type="instance", ) # Then assert resource.uid == "arn:aws:ec2:us-east-1:123456789:instance/i-1234"

运行结果必须是 FAIL——因为测试引用了尚不存在的代码。这一步的价值在于:先锁定"行为契约",再让实现去满足它。

Bug 修复:先写一个能复现 Bug 的测试

写测试复现缺陷,而不是直接改代码:

  • UIexpect(() => render(<DatePicker value={null} />)).not.toThrow();
  • SDKassert result[0].status == "FAIL" # 当前错误地返回 PASS
  • APIassert response.status_code == 403 # 当前错误地返回 200

运行 → 应当失败(成功复现了 Bug)。

重构:先锁定行为基线

重构没有"新行为"可测试,所以先把当前全部行为固化下来:

# 任何技术栈:先运行全部已有测试,它们必须全部 PASS # 这是你的安全网——如果重构后有任何测试变红,说明你破坏了某些行为

运行 → 全部 PASS(基线)。之后每一次重构改动后都要回到这条基线上验证。

Phase 2:GREEN —— 只写最小代码

让测试通过的最小实现。对第一个测试而言,硬编码(Fake It)是合法的

UI:

// 测试期望 calculateDiscount(100, 10) === 10 function calculateDiscount() { return 10; // FAKE IT - 第一个测试硬编码是合法的 }

Python(SDK/API):

# 测试期望 check.execute() 返回 0 条结果 def execute(self): return [] # FAKE IT - 第一个测试硬编码是合法的

测试通过了,但工作尚未结束——单一测试可以被硬编码轻易骗过,真正的逻辑还没有被驱动出来。

Phase 3:三角验证(Triangulation,关键环节)

一个测试允许作弊,多个测试才能逼出真实逻辑。

新增不同输入的测试来打破硬编码值:

场景是否必需
Happy path(正常路径)YES
零值 / 空值YES
边界值YES
不同的合法输入YES(打破 fake)
错误条件YES

UI:

it("should calculate 10% discount", () => { expect(calculateDiscount(100, 10)).toBe(10); }); // 新增 —— 打破硬编码: it("should calculate 15% on 200", () => { expect(calculateDiscount(200, 15)).toBe(30); }); it("should return 0 for 0% rate", () => { expect(calculateDiscount(100, 0)).toBe(0); });

Python:

def test_single_public_ami(self): # 不同输入 -> 打破硬编码的空列表 assert len(result) == 1 assert result[0].status == "FAIL" def test_private_ami(self): assert result[0].status == "PASS"

现在 fake 必然失败 → 必须写出真实实现。

仓库中的 ec2_ami_public 测试 就是三角验证在 SDK 侧的教科书案例:同一文件内test_no_amis(0 条结果)、test_one_private_ami(1 条、状态 PASS、校验resource_id/resource_arn/region/resource_tags等细节)、test_one_public_ami(通过modify_attribute将 AMI 的launchPermission授权给all后断言状态 FAIL)三个用例分别覆盖了空值、合法输入、边界场景,任何一个被硬编码的返回值都无法同时骗过它们。

Phase 4:REFACTOR —— 在绿灯下放心改善

测试全绿后,开始优化代码质量,不改变任何行为

  • 提取函数 / 方法
  • 改善命名
  • 补充类型与校验
  • 消除重复

每次改动后立即运行测试——必须保持 GREEN。有了 Phase 1 的重构基线做对照,任何回归都会立刻暴露。

快速参考卡

+------------------------------------------------+ | TDD WORKFLOW | +------------------------------------------------+ | 0. ASSESS: What tests exist? What's missing? | | | | 1. RED: Write ONE failing test | | +-- Run -> Must fail with clear error | | | | 2. GREEN: Write MINIMUM code to pass | | +-- Fake It is valid for first test | | | | 3. TRIANGULATE: Add tests that break the fake | | +-- Different inputs, edge cases | | | | 4. REFACTOR: Improve with confidence | | +-- Tests stay green throughout | | | | 5. REPEAT: Next behavior/requirement | +------------------------------------------------+

反模式清单(NEVER DO)

# 任何语言: # 1. 先写代码,后补测试 def new_feature(): ... # 事后补测试 = 毫无价值 # 2. 跳过三角验证 # 单个测试会被硬编码永久骗过 # 3. 测试实现细节(而不是行为) assert component.state.is_loading == True # 坏 - 应测试行为,而非内部状态 assert mock_service.call_count == 3 # 坏 - 脆弱的耦合断言 # 4. 在写任何代码前一次性写完所有测试 # 一次只写一个测试,让它通过,再写下一个 # 5. 巨型测试方法 # 每个测试只验证一个行为

第 3 条尤其值得注意:测试应当面向可观察行为(渲染结果、返回状态、HTTP 响应码),而不是锁定组件内部状态或 mock 调用次数,否则实现一重构测试就碎。

各栈命令速查:以仓库实际配置为准

UI(ui/

ui/package.json 中实际注册的测试脚本如下(文档中的pnpm test意图为 watch 模式,但仓库以test:watch承载该语义,test本体是 CI 单次运行,请以 package.json 为准):

pnpm test # vitest run —— 单次运行(CI 场景) pnpm test:watch # vitest —— 监听模式(开发场景) pnpm test:coverage # vitest run --coverage —— 覆盖率报告 pnpm test:unit # 仅运行 unit 项目(jsdom 环境) pnpm test:integration # 仅运行 integration 项目(浏览器/Playwright 环境) pnpm test ComponentName # 按名称过滤

在 ui/vitest.config.ts 中可以看到这套脚本背后的完整配置:测试被拆分为unit(jsdom 环境,*.test.{ts,tsx})与integration(通过@vitest/browser-playwright驱动真实 chromium,viewport 1280×800,覆盖*.integration.test.{ts,tsx})两个 project;覆盖率使用 v8 provider 并排除node_modules.next与测试文件本身。而 ui/vitest.setup.ts 则为 jsdom 环境补齐了localStorage/sessionStorage的 Mock 实现以及空实现的ResizeObserver,保证组件在"无浏览器"环境下也能稳定渲染。

SDK(prowler/

uv run pytest tests/path/ -v # 运行指定测试 uv run pytest tests/path/ -v -k "test_name" # 按名称过滤 uv run pytest -n auto tests/ # 并行运行(pytest-xdist) uv run pytest --cov=./prowler tests/ # 覆盖率

这些命令背后有 pyproject.toml 的完整支撑:dev 依赖组固定了pytest==9.0.3pytest-xdist==3.6.1pytest-cov==6.0.0moto[all]==5.1.11freezegunmock等工具;[tool.pytest_env]会在测试时自动注入AWS_ACCESS_KEY_IDAWS_DEFAULT_REGION等环境变量,配合 moto 的@mock_aws装饰器即可在本地模拟整个 AWS 服务层,无需真实云资源。

API(api/

uv run pytest -x --tb=short # 运行全部(遇首个失败即停) uv run pytest api/src/backend/api/tests/test_file.py # 运行指定文件 uv run pytest -k "test_name" -v # 按名称过滤

api/src/backend/api/tests/ 目录下已有大量test_*.py用例,例如 test_authentication.py 中@pytest.mark.django_db标记配合 fixture 工厂,直接覆盖多租户 API Key 认证、数据库路由等后端行为——正是 Phase 1/3 中 pytest-django 写法的真实范本。

总结:把 TDD 变成习惯,而不是额外负担

skills/tdd/SKILL.md的价值不在于发明新方法论,而在于把标准的 TDD 纪律工程化到了 Prowler 三个技术栈的日常开发中:Phase 0 消灭"凭感觉开写",Phase 1 用失败测试锁定契约,Phase 2 用最小代码保持节奏,Phase 3 用三角验证逼出真实逻辑,Phase 4 在安全网内放心重构。无论是给 UI 组件补一个交互测试、为 SDK 新增一条云安全检查,还是为 API 加一个鉴权端点,都可以严格套用这套五阶段流程。结合仓库内已经存在的 SDK 测试、UI 组件测试 与 API 测试 作为参照模板,任何人都能快速上手。

【免费下载链接】prowlerProwler is the world’s most widely used open-source cloud security platform that automates security and compliance across any cloud environment.项目地址: https://gitcode.com/GitHub_Trending/pr/prowler

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询