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 / React | Vitest + Testing Library (RTL) | *.test.{ts,tsx}(与被测文件同目录) | vitest技能 |
prowler/ | Python | pytest + moto | *_test.py(后缀),位于tests/ | prowler-test-sdk技能 |
api/ | Python / Django | pytest + django | test_*.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 三定律:
- 在拿到失败的测试之前,不写任何生产代码;
- 不多写一行测试——只写足以让测试失败的那部分;
- 不多写一行生产代码——只写足以让测试通过的那部分。
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) == 0API(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 的测试
写测试复现缺陷,而不是直接改代码:
- UI:
expect(() => render(<DatePicker value={null} />)).not.toThrow(); - SDK:
assert result[0].status == "FAIL" # 当前错误地返回 PASS - API:
assert 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.3、pytest-xdist==3.6.1、pytest-cov==6.0.0、moto[all]==5.1.11、freezegun、mock等工具;[tool.pytest_env]会在测试时自动注入AWS_ACCESS_KEY_ID、AWS_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),仅供参考