tdd について
tdd は Prowler の UI、SDK、API 開発向けのテスト駆動開発スキルです。RED、GREEN、REFACTOR の手順に沿い、各スタックのテスト方法を示します。
主な機能
- 01RED、GREEN、REFACTOR のサイクル
- 02既存テストとカバレッジの確認
- 03新機能とバグ修正のテスト
- 04実ロジックを検証するテストの追加
- 05UI、SDK、API のテストコマンド
活用シーン
- 01UI 機能に Vitest テストを追加
- 02SDK の不具合を再現して修正
- 03API のリファクタリング前に基準を確立
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ prowler-cloud/ prowler --skill tddインストール後、Claude Code で/tddを入力して読み込まれたか確認します。
詳細
TDD サイクル(必須)
+-----------------------------------------+
| RED -> GREEN -> REFACTOR |
| ^ | |
| +------------------------+ |
+-----------------------------------------+
問題は「テストを書くべきか?」ではなく、「どのテストが必要か?」です。
TDD の 3 つの法則
- 失敗するテストを書くまでは、本番コードを書かない
- 失敗させるために必要な範囲を超えて、テストを増やさない
- テストを成功させるために必要な範囲を超えて、コードを書かない
使用するスタックを特定する
始める前に、どのコンポーネントで作業するかを確認します。
| 作業場所 | スタック | ランナー | テストパターン | 詳細 |
|---|---|---|---|---|
ui/ | TypeScript / React | Vitest + RTL | *.test.{ts,tsx}(同じディレクトリに配置) | vitest スキルを参照 |
prowler/ | Python | pytest + moto | tests/ 内の *_test.py(サフィックス形式) | prowler-test-sdk スキルを参照 |
api/ | Python / Django | pytest + django | api/src/backend/**/tests/ 内の test_*.py(プレフィックス形式) | prowler-test-api スキルを参照 |
フェーズ 0:評価(必ず最初に実施)
コードを書く前に、必ず以下を行います。
UI(ui/)
# 1. Find existing tests
fd "*.test.tsx" ui/components/feature/
# 2. Check coverage
pnpm test:coverage -- components/feature/
# 3. Read existing tests
SDK(prowler/)
# 1. Find existing tests
fd "*_test.py" tests/providers/aws/services/ec2/
# 2. Run specific test
uv run pytest tests/providers/aws/services/ec2/ec2_ami_public/ -v
# 3. Read existing tests
API(api/)
# 1. Find existing tests
fd "test_*.py" api/src/backend/api/tests/
# 2. Run specific test
uv run pytest api/src/backend/api/tests/test_models.py -v
# 3. Read existing tests
判断フロー(すべてのスタック)
+------------------------------------------+
| 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 |
+-----------+ +-----------+
フェーズ 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"
実行 -> 必ず失敗すること: テストでは、まだ存在しないコードを参照します。
バグ修正の場合
まず、バグを再現するテストを書きます。
UI: expect(() => render(<DatePicker value={null} />)).not.toThrow();
SDK: assert result[0].status == "FAIL" # Currently returns PASS incorrectly
API: assert response.status_code == 403 # Currently returns 200
実行 -> 失敗するはずです(バグを再現します)。
リファクタリングの場合
リファクタリングの前に、現在の動作をすべて記録します。
# Any stack: run ALL existing tests, they should PASS
# This is your safety net - if any fail after refactoring, you broke something
実行 -> すべて成功するはずです(ベースライン)。
フェーズ 2:GREEN - 最小限のコード
テストを成功させるために、最小限のコードを書きます。最初のテストでは、ハードコードしてもかまいません。
UI:
// Test expects calculateDiscount(100, 10) === 10
function calculateDiscount() {
return 10; // FAKE IT - hardcoded is valid for first test
}
Python(SDK/API):
# Test expects check.execute() returns 0 results
def execute(self):
return [] # FAKE IT - hardcoded is valid for first test
これで成功します。しかし、まだ終わりではありません……
フェーズ 3:三角測量(重要)
テストが 1 つだけでは、偽装が可能です。テストを複数用意すれば、実際のロジックが必要になります。
ハードコードした値では対応できないような、異なる入力値を使ったテストを追加します。
| シナリオ | 必須か |
|---|---|
| 正常系 | はい |
| ゼロ/空の値 | はい |
| 境界値 | はい |
| 異なる有効な入力(偽装を破綻させる) | はい |
| エラー条件 | はい |
UI:
it("should calculate 10% discount", () => {
expect(calculateDiscount(100, 10)).toBe(10);
});
// ADD - breaks the fake:
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):
# Different input -> breaks hardcoded empty list
assert len(result) == 1
assert result[0].status == "FAIL"
def test_private_ami(self):
assert result[0].status == "PASS"
これで偽装は破綻します -> 実際の実装が必要です。
フェーズ 4:リファクタリング
テストが GREEN -> 動作を変えずに、コードの品質を改善します。
- 関数/メソッドを抽出する
- 命名を改善する
- 型/バリデーションを追加する
- 重複を減らす
変更のたびにテストを実行します -> 必ず GREEN のままにします。
クイックリファレンス
+------------------------------------------------+
| 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 |
+------------------------------------------------+
アンチパターン(絶対にしないこと)
# ANY language:
# 1. Code first, tests after
def new_feature(): ... # Then writing tests = USELESS
# 2. Skip triangulation
# Single test allows faking forever
# 3. Test implementation details
assert component.state.is_loading == True # BAD - test behavior, not internals
assert mock_service.call_count == 3 # BAD - brittle coupling
# 4. All tests at once before any code
# Write ONE test, make it pass, THEN write the next
# 5. Giant test methods
# Each test should verify ONE behavior
スタック別コマンド
UI(ui/)
pnpm test # Watch mode
pnpm test:run # Single run (CI)
pnpm test:coverage # Coverage report
pnpm test ComponentName # Filter by name
SDK(prowler/)
uv run pytest tests/path/ -v # Run specific tests
uv run pytest tests/path/ -v -k "test_name" # Filter by name
uv run pytest -n auto tests/ # Parallel run
uv run pytest --cov=./prowler tests/ # Coverage
API(api/)
uv run pytest -x --tb=short # Run all (stop on first fail)
uv run pytest api/src/backend/api/tests/test_file.py # Specific file
uv run pytest -k "test_name" -v # Filter by name