mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-21 18:58:04 +00:00
feat(ui): setup vitest with react testing library and TDD workflow (#9925)
This commit is contained in:
@@ -53,6 +53,8 @@ Reusable patterns for common technologies:
|
||||
| `nextjs-15` | App Router, Server Actions, streaming |
|
||||
| `tailwind-4` | cn() utility, Tailwind 4 patterns |
|
||||
| `playwright` | Page Object Model, selectors |
|
||||
| `vitest` | Unit testing, React Testing Library |
|
||||
| `tdd` | Test-Driven Development workflow |
|
||||
| `pytest` | Fixtures, mocking, markers |
|
||||
| `django-drf` | ViewSets, Serializers, Filters |
|
||||
| `zod-4` | Zod 4 API patterns |
|
||||
|
||||
371
skills/tdd/SKILL.md
Normal file
371
skills/tdd/SKILL.md
Normal file
@@ -0,0 +1,371 @@
|
||||
---
|
||||
name: tdd
|
||||
description: >
|
||||
Test-Driven Development workflow for ALL Prowler components (UI, SDK, API).
|
||||
Trigger: ALWAYS when implementing features, fixing bugs, or refactoring - regardless of component.
|
||||
This is a MANDATORY workflow, not optional.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
version: "2.0"
|
||||
scope: [root, ui, api, prowler]
|
||||
auto_invoke:
|
||||
- "Implementing feature"
|
||||
- "Fixing bug"
|
||||
- "Refactoring code"
|
||||
- "Working on task"
|
||||
- "Modifying component"
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task
|
||||
---
|
||||
|
||||
## TDD Cycle (MANDATORY)
|
||||
|
||||
```
|
||||
+-----------------------------------------+
|
||||
| RED -> GREEN -> REFACTOR |
|
||||
| ^ | |
|
||||
| +------------------------+ |
|
||||
+-----------------------------------------+
|
||||
```
|
||||
|
||||
**The question is NOT "should I write tests?" but "what tests do I need?"**
|
||||
|
||||
---
|
||||
|
||||
## The Three Laws of TDD
|
||||
|
||||
1. **No production code** until you have a failing test
|
||||
2. **No more test** than necessary to fail
|
||||
3. **No more code** than necessary to pass
|
||||
|
||||
---
|
||||
|
||||
## Detect Your Stack
|
||||
|
||||
Before starting, identify which component you're working on:
|
||||
|
||||
| Working in | Stack | Runner | Test pattern | Details |
|
||||
|------------|-------|--------|-------------|---------|
|
||||
| `ui/` | TypeScript / React | Vitest + RTL | `*.test.{ts,tsx}` (co-located) | See `vitest` skill |
|
||||
| `prowler/` | Python | pytest + moto | `*_test.py` (suffix) in `tests/` | See `prowler-test-sdk` skill |
|
||||
| `api/` | Python / Django | pytest + django | `test_*.py` (prefix) in `api/src/backend/**/tests/` | See `prowler-test-api` skill |
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Assessment (ALWAYS FIRST)
|
||||
|
||||
Before writing ANY code:
|
||||
|
||||
### UI (`ui/`)
|
||||
|
||||
```bash
|
||||
# 1. Find existing tests
|
||||
fd "*.test.tsx" ui/components/feature/
|
||||
|
||||
# 2. Check coverage
|
||||
pnpm test:coverage -- components/feature/
|
||||
|
||||
# 3. Read existing tests
|
||||
```
|
||||
|
||||
### SDK (`prowler/`)
|
||||
|
||||
```bash
|
||||
# 1. Find existing tests
|
||||
fd "*_test.py" tests/providers/aws/services/ec2/
|
||||
|
||||
# 2. Run specific test
|
||||
poetry run pytest tests/providers/aws/services/ec2/ec2_ami_public/ -v
|
||||
|
||||
# 3. Read existing tests
|
||||
```
|
||||
|
||||
### API (`api/`)
|
||||
|
||||
```bash
|
||||
# 1. Find existing tests
|
||||
fd "test_*.py" api/src/backend/api/tests/
|
||||
|
||||
# 2. Run specific test
|
||||
poetry run pytest api/src/backend/api/tests/test_models.py -v
|
||||
|
||||
# 3. Read existing tests
|
||||
```
|
||||
|
||||
### Decision Tree (All Stacks)
|
||||
|
||||
```
|
||||
+------------------------------------------+
|
||||
| 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 |
|
||||
+-----------+ +-----------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: RED - Write Failing Tests
|
||||
|
||||
### For NEW Functionality
|
||||
|
||||
**UI (Vitest)**
|
||||
|
||||
```typescript
|
||||
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)**
|
||||
|
||||
```python
|
||||
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)**
|
||||
|
||||
```python
|
||||
@pytest.mark.django_db
|
||||
class TestResourceModel:
|
||||
def test_create_resource_with_tags(self, providers_fixture):
|
||||
# Given
|
||||
provider, *_ = providers_fixture
|
||||
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"
|
||||
```
|
||||
|
||||
**Run -> MUST fail:** Test references code that doesn't exist yet.
|
||||
|
||||
### For BUG FIXES
|
||||
|
||||
Write a test that **reproduces the bug** first:
|
||||
|
||||
**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`
|
||||
|
||||
**Run -> Should FAIL (reproducing the bug)**
|
||||
|
||||
### For REFACTORING
|
||||
|
||||
Capture ALL current behavior BEFORE refactoring:
|
||||
|
||||
```
|
||||
# Any stack: run ALL existing tests, they should PASS
|
||||
# This is your safety net - if any fail after refactoring, you broke something
|
||||
```
|
||||
|
||||
**Run -> All should PASS (baseline)**
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: GREEN - Minimum Code
|
||||
|
||||
Write the MINIMUM code to make the test pass. Hardcoding is valid for the first test.
|
||||
|
||||
**UI:**
|
||||
|
||||
```typescript
|
||||
// Test expects calculateDiscount(100, 10) === 10
|
||||
function calculateDiscount() {
|
||||
return 10; // FAKE IT - hardcoded is valid for first test
|
||||
}
|
||||
```
|
||||
|
||||
**Python (SDK/API):**
|
||||
|
||||
```python
|
||||
# Test expects check.execute() returns 0 results
|
||||
def execute(self):
|
||||
return [] # FAKE IT - hardcoded is valid for first test
|
||||
```
|
||||
|
||||
**This passes. But we're not done...**
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Triangulation (CRITICAL)
|
||||
|
||||
**One test allows faking. Multiple tests FORCE real logic.**
|
||||
|
||||
Add tests with different inputs that break the hardcoded value:
|
||||
|
||||
| Scenario | Required? |
|
||||
|----------|-----------|
|
||||
| Happy path | YES |
|
||||
| Zero/empty values | YES |
|
||||
| Boundary values | YES |
|
||||
| Different valid inputs | YES (breaks fake) |
|
||||
| Error conditions | YES |
|
||||
|
||||
**UI:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```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"
|
||||
```
|
||||
|
||||
**Now fake BREAKS -> Real implementation required.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: REFACTOR
|
||||
|
||||
Tests GREEN -> Improve code quality WITHOUT changing behavior.
|
||||
|
||||
- Extract functions/methods
|
||||
- Improve naming
|
||||
- Add types/validation
|
||||
- Reduce duplication
|
||||
|
||||
**Run tests after EACH change -> Must stay GREEN**
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```
|
||||
+------------------------------------------------+
|
||||
| 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 |
|
||||
+------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (NEVER DO)
|
||||
|
||||
```
|
||||
# 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands by Stack
|
||||
|
||||
### UI (`ui/`)
|
||||
|
||||
```bash
|
||||
pnpm test # Watch mode
|
||||
pnpm test:run # Single run (CI)
|
||||
pnpm test:coverage # Coverage report
|
||||
pnpm test ComponentName # Filter by name
|
||||
```
|
||||
|
||||
### SDK (`prowler/`)
|
||||
|
||||
```bash
|
||||
poetry run pytest tests/path/ -v # Run specific tests
|
||||
poetry run pytest tests/path/ -v -k "test_name" # Filter by name
|
||||
poetry run pytest -n auto tests/ # Parallel run
|
||||
poetry run pytest --cov=./prowler tests/ # Coverage
|
||||
```
|
||||
|
||||
### API (`api/`)
|
||||
|
||||
```bash
|
||||
poetry run pytest -x --tb=short # Run all (stop on first fail)
|
||||
poetry run pytest api/src/backend/api/tests/test_file.py # Specific file
|
||||
poetry run pytest -k "test_name" -v # Filter by name
|
||||
```
|
||||
201
skills/vitest/SKILL.md
Normal file
201
skills/vitest/SKILL.md
Normal file
@@ -0,0 +1,201 @@
|
||||
---
|
||||
name: vitest
|
||||
description: >
|
||||
Vitest unit testing patterns with React Testing Library.
|
||||
Trigger: When writing unit tests for React components, hooks, or utilities.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: prowler-cloud
|
||||
version: "1.0"
|
||||
scope: [root, ui]
|
||||
auto_invoke:
|
||||
- "Writing Vitest tests"
|
||||
- "Writing React component tests"
|
||||
- "Writing unit tests for UI"
|
||||
- "Testing hooks or utilities"
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task
|
||||
---
|
||||
|
||||
> **For E2E tests**: Use `prowler-test-ui` skill (Playwright).
|
||||
> This skill covers **unit/integration tests** with Vitest + React Testing Library.
|
||||
|
||||
## Test Structure (REQUIRED)
|
||||
|
||||
Use **Given/When/Then** (AAA) pattern with comments:
|
||||
|
||||
```typescript
|
||||
it("should update user name when form is submitted", async () => {
|
||||
// Given - Arrange
|
||||
const user = userEvent.setup();
|
||||
const onSubmit = vi.fn();
|
||||
render(<UserForm onSubmit={onSubmit} />);
|
||||
|
||||
// When - Act
|
||||
await user.type(screen.getByLabelText(/name/i), "John");
|
||||
await user.click(screen.getByRole("button", { name: /submit/i }));
|
||||
|
||||
// Then - Assert
|
||||
expect(onSubmit).toHaveBeenCalledWith({ name: "John" });
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Describe Block Organization
|
||||
|
||||
```typescript
|
||||
describe("ComponentName", () => {
|
||||
describe("when [condition]", () => {
|
||||
it("should [expected behavior]", () => {});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Group by behavior, NOT by method.**
|
||||
|
||||
---
|
||||
|
||||
## Query Priority (REQUIRED)
|
||||
|
||||
| Priority | Query | Use Case |
|
||||
|----------|-------|----------|
|
||||
| 1 | `getByRole` | Buttons, inputs, headings |
|
||||
| 2 | `getByLabelText` | Form fields |
|
||||
| 3 | `getByPlaceholderText` | Inputs without label |
|
||||
| 4 | `getByText` | Static text |
|
||||
| 5 | `getByTestId` | Last resort only |
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD
|
||||
screen.getByRole("button", { name: /submit/i });
|
||||
screen.getByLabelText(/email/i);
|
||||
|
||||
// ❌ BAD
|
||||
container.querySelector(".btn-primary");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## userEvent over fireEvent (REQUIRED)
|
||||
|
||||
```typescript
|
||||
// ✅ ALWAYS use userEvent
|
||||
const user = userEvent.setup();
|
||||
await user.click(button);
|
||||
await user.type(input, "hello");
|
||||
|
||||
// ❌ NEVER use fireEvent for interactions
|
||||
fireEvent.click(button);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Testing Patterns
|
||||
|
||||
```typescript
|
||||
// ✅ findBy for elements that appear async
|
||||
const element = await screen.findByText(/loaded/i);
|
||||
|
||||
// ✅ waitFor for assertions
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/success/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// ✅ ONE assertion per waitFor
|
||||
await waitFor(() => expect(mockFn).toHaveBeenCalled());
|
||||
await waitFor(() => expect(screen.getByText(/done/i)).toBeVisible());
|
||||
|
||||
// ❌ NEVER multiple assertions in waitFor
|
||||
await waitFor(() => {
|
||||
expect(mockFn).toHaveBeenCalled();
|
||||
expect(screen.getByText(/done/i)).toBeVisible(); // Slower failures
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mocking
|
||||
|
||||
```typescript
|
||||
// Basic mock
|
||||
const handleClick = vi.fn();
|
||||
|
||||
// Mock with return value
|
||||
const fetchUser = vi.fn().mockResolvedValue({ name: "John" });
|
||||
|
||||
// Always clean up
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
```
|
||||
|
||||
### vi.spyOn vs vi.mock
|
||||
|
||||
| Method | When to Use |
|
||||
|--------|-------------|
|
||||
| `vi.spyOn` | Observe without replacing (PREFERRED) |
|
||||
| `vi.mock` | Replace entire module (use sparingly) |
|
||||
|
||||
---
|
||||
|
||||
## Common Matchers
|
||||
|
||||
```typescript
|
||||
// Presence
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element).toBeVisible();
|
||||
|
||||
// State
|
||||
expect(button).toBeDisabled();
|
||||
expect(input).toHaveValue("text");
|
||||
expect(checkbox).toBeChecked();
|
||||
|
||||
// Content
|
||||
expect(element).toHaveTextContent(/hello/i);
|
||||
expect(element).toHaveAttribute("href", "/home");
|
||||
|
||||
// Functions
|
||||
expect(fn).toHaveBeenCalledWith(arg1, arg2);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Test
|
||||
|
||||
```typescript
|
||||
// ❌ Internal state
|
||||
expect(component.state.isLoading).toBe(true);
|
||||
|
||||
// ❌ Third-party libraries
|
||||
expect(axios.get).toHaveBeenCalled();
|
||||
|
||||
// ❌ Static content (unless conditional)
|
||||
expect(screen.getByText("Welcome")).toBeInTheDocument();
|
||||
|
||||
// ✅ User-visible behavior
|
||||
expect(screen.getByRole("button")).toBeDisabled();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Organization
|
||||
|
||||
```
|
||||
components/
|
||||
├── Button/
|
||||
│ ├── Button.tsx
|
||||
│ ├── Button.test.tsx # Co-located
|
||||
│ └── index.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
pnpm test # Watch mode
|
||||
pnpm test:run # Single run
|
||||
pnpm test:coverage # With coverage
|
||||
pnpm test Button # Filter by name
|
||||
```
|
||||
Reference in New Issue
Block a user