Skill: sydpz/test-design (version 1.0.0)

--- name: test-design description: > Test design and strategy skill for comprehensive testing. Use when: designing test cases, planning testing strategy, writing unit/integration/e2e tests, or improving test coverage. Triggers: test design, test strategy, unit test, integration test, e2e test, TDD, test coverage, test plan, test case design. Provides: testing pyramid guidance, test case templates, test categorization, mocking strategies, and test execution frameworks. --- # Test Design Skill A structured approach to designing tests that give confidence without excessive maintenance burden. ## Core Philosophy **Test behavior, not implementation.** Tests that couple to internal implementation details become fragile and refactor-resistant. Aim to test what the code does, not how it does it. **The best test is the one that fails when something breaks, and passes when everything works.** Perfect coverage means nothing if tests don't catch real bugs. ## Testing Pyramid ``` ┌─────────┐ │ E2E │ ← Few, slow, high confidence ┌┴─────────┴┐ │ Integration│ ← Some, medium speed ┌┴───────────┴┐ │ Unit │ ← Many, fast, granular └─────────────┘ ``` **Unit tests** form the base — fast, isolated, cover individual functions. **Integration tests** verify components work together. **E2E tests** validate complete user journeys. ## Test Case Design ### Arrange-Act-Assert (AAA) Pattern ```python def test_withdraw_amount_exceeds_balance(): # Arrange account = Account(balance=Decimal("100.00")) # Act result = account.withdraw(Decimal("150.00")) # Assert assert result.is_failed() assert account.balance == Decimal("100.00") # unchanged ``` ### Given-When-Then (GWT) Pattern ```gherkin Feature: Account Withdrawal Scenario: Withdrawal amount exceeds balance Given an account with balance $100 When I withdraw $150 Then the withdrawal is rejected And the balance remains $100 ``` ## Test Categories ### 🔴 Happy Path Tests - Verify the main success scenario works - At least one per feature/function ### 🟡 Edge Case Tests - Boundary values (0, -1, max integer) - Empty/null inputs - Rare but possible conditions ### 🟡 Error Case Tests - Invalid inputs - Missing dependencies - Permission denied scenarios ### 🟢 Edge/Extreme Cases - Very large inputs - Very small inputs - Unicode/special characters - Concurrent access ## Test Doubles (Mocks/Stubs/Fakes) | Type | Purpose | Use When | |------|---------|----------| | **Dummy** | Fill parameter lists | Never actually used | | **Stub** | Provide predetermined responses | Test requires specific data | | **Spy** | Record how something was called | Verify interactions | | **Mock** | Pre-programmed expectations | Verify behavior + interactions | | **Fake** | Working implementation (simplified) | Real impl too slow/complex | **Rule:** Prefer real objects over mocks when practical. Mocks hide integration problems. ## Test Naming Convention ``` test_<unit>_<scenario>_<expected_result> ``` Examples: - [REDACTED_SECRET] - [REDACTED_SECRET] - [REDACTED_SECRET] ## Coverage Targets | Layer | Target | Purpose | |-------|--------|---------| | Unit | 70-80% | Core business logic | | Integration | 40-60% | Key flows work together | | E2E | Critical paths only | User journeys | **Note:** Coverage is a guide, not a goal. 100% coverage with shallow tests is worse than 70% with meaningful assertions. ## Test Execution ### Unit Tests Run frequently during development (every save). ```bash # Go go test ./... # Python pytest tests/unit/ -v # TypeScript npm test -- --coverage ``` ### Integration Tests Run before PR, after unit tests pass. ```bash # Start dependencies docker-compose up -d # Run integration suite pytest tests/integration/ -v ``` ### E2E Tests Run on CI against deployed environment. ```bash # CI/CD pipeline npm run test:e2e -- --specs=critical-paths ``` ## Test Data Management ### Strategies 1. **Factories/Fixtures** — Create test data on demand 2. **Seeded Data** — Deterministic test datasets 3. **Randomized Data** — Fuzz testing with random inputs 4. **Snapshot Testing** — Verify serializable outputs ### Best Practices - Tests should be independent (no shared state) - Each test should clean up after itself - Use meaningful data, not magic values - Avoid test interdependencies ## File Structure ``` test-design/ ├── SKILL.md └── references/ ├── test-case-templates.md ├── testing-pyramid.md ├── mocking-strategies.md ├── test-naming-conventions.md └── per-type/ ├── unit-test-guide.md ├── integration-test-guide.md └── e2e-test-guide.md ```

path: references/mocking-strategies.md
content: # Mocking Strategies

## When to Mock

### Good Reasons to Mock
- External API calls (slow, unreliable, expensive)
- Database operations (isolation, speed)
- Time-dependent logic (freeze time)
- Non-deterministic systems

### Bad Reasons to Mock
- Too lazy to set up real dependency
- Speed over correctness
- Hiding a design problem

## Test Doubles

| Type | What it does | Use When |
|------|--------------|----------|
| **Dummy** | Passes values, never used | Fill parameter lists |
| **Fake** | Works implementation (simplified) | Real is too slow/complex |
| **Stub** | Provides canned responses | Need specific data |
| **Spy** | Records how it was called | Verify interactions |
| **Mock** | Pre-programmed with expectations | Verify behavior + interactions |

## Go Mocking

### Interface-based Mocking

```go
// Define interface
type UserRepo interface {
    FindByID(id int) (*User, error)
    Create(user *User) error
}

// Use in service
type UserService struct {
    repo UserRepo
}

// Test with mock
type mockUserRepo struct {
    findByIDCalled bool
    findByIDID     int
    findByIDResult *User
    findByIDError  error
}

func (m *mockUserRepo) FindByID(id int) (*User, error) {
    m.findByIDCalled = true
    m.findByIDID = id
    return m.findByIDResult, m.findByIDError
}

func TestUserService_Get(t *testing.T) {
    mock := &mockUserRepo{
        findByIDResult: &User{Name: "Test"},
    }
    svc := NewUserService(mock)
    
    user, err := svc.Get(1)
    
    if err != nil {
        t.Fatal(err)
    }
    if !mock.findByIDCalled {
        t.Error("expected FindByID to be called")
    }
    if user.Name != "Test" {
        t.Errorf("expected name Test, got %s", user.Name)
    }
}
```

### Using testify/mock

```go
import "github.com/stretchr/testify/mock"

type MockDB struct {
    mock.Mock
}

func (m *MockDB) FindUser(id int) (*User, error) {
    args := m.Called(id)
    return args.Get(0).(*User), args.Error(1)
}

func TestWithTestify(t *testing.T) {
    mockDB := new(MockDB)
    
    mockDB.On("FindUser", 1).Return(&User{Name: "Test"}, nil)
    
    svc := NewUserService(mockDB)
    user, _ := svc.Get(1)
    
    mockDB.AssertExpectations(t)
    assert.Equal(t, "Test", user.Name)
}
```

## Python Mocking

### Using unittest.mock

```python
from unittest.mock import Mock, patch, MagicMock

def test_with_mock():
    mock_repo = Mock()
    mock_repo.find_by_id.return_value = User(name="Test")
    
    svc = UserService(mock_repo)
    user = svc.get(1)
    
    mock_repo.find_by_id.assert_called_once_with(1)
    assert user.name == "Test"

def test_with_patch():
    with patch('app.external_api.Client') as mock_client:
        mock_client.return_value.get_user.return_value = {"name": "Test"}
        # ... test code
```

### Using pytest-mock

```python
def test_with_fixture(mocker):
    mock_repo = mocker.Mock()
    mock_repo.find_by_id.return_value = User(name="Test")
    
    svc = UserService(mock_repo)
    user = svc.get(1)
    
    assert user.name == "Test"
    mock_repo.find_by_id.assert_called_once()
```

## TypeScript Mocking

### Using jest.fn()

```typescript
const mockRepo = {
  findById: jest.fn(),
  create: jest.fn(),
};

mockRepo.findById.mockResolvedValue({ id: 1, name: 'Test' });

const svc = new UserService(mockRepo);
const user = await svc.get(1);

expect(mockRepo.findById).toHaveBeenCalledWith(1);
expect(user.name).toBe('Test');
```

### Using jest.spyOn()

```typescript
const db = require('./db');
jest.spyOn(db, 'findUser').mockResolvedValue({ id: 1, name: 'Test' });
```

## Best Practices

1. **Mock interfaces, not concrete classes** — Allows implementation changes
2. **Don't mock what you don't own** — Could create false confidence
3. **Verify interactions, not just return values** — Ensures correct usage
4. **Reset mocks between tests** — Prevents test pollution
5. **Use specific assertions** — `assert_called_once_with()` not just `assert_called()`

## Anti-Patterns

### Over-mocking
```python
# Bad: Mocking everything
mock_db = Mock()
mock_cache = Mock()
mock_logger = Mock()
mock_config = Mock()
```
Better: Use real objects when they don't complicate tests

### Brittle mocks
```python
# Bad: Too specific
mock.method.assert_called_with("arg1", 2, True, something_else)

# Better: What matters
mock.method.assert_called()
```

### Mocking private methods
Don't mock internal implementation details. Test behavior, not implementation.


path: references/test-case-templates.md
content: # Test Case Templates

## Unit Test Template

### Go
```go
func Test<UnitName>_<Scenario>_<Expected>(t *testing.T) {
    // Arrange
    <setup code>
    
    // Act
    <action being tested>
    
    // Assert
    <verify expected outcome>
}
```

### Python
```python
def test_<unit>_<scenario>_<expected>():
    # Arrange
    <setup code>
    
    # Act
    <action being tested>
    
    # Assert
    <verify expected outcome>
```

### TypeScript
```typescript
describe('UnitName', () => {
  describe('Scenario', () => {
    it('should Expected', () => {
      // Arrange
      const <setup>
      
      // Act
      const <result> = <action>()
      
      // Assert
      expect(<result>).to<matcher>(<expected>)
    })
  })
})
```

## Integration Test Template

```
Test Case: <Name>
Preconditions: <What must be true before>
Test Steps:
  1. <Step 1>
  2. <Step 2>
  3. <Step 3>
Expected Result: <What should happen>
Cleanup: <How to clean up>
```

## E2E Test Template (Gherkin)

```gherkin
Feature: <Feature Name>

  Scenario: <Scenario description>
    Given <precondition>
    And <additional precondition>
    When <action performed>
    And <another action>
    Then <expected outcome>
    And <another expected outcome>
```

## Test Data Factory Template

```python
# Factory for test data
class UserFactory:
    @staticmethod
    def create(email: str = "[REDACTED_SECRET]", 
               name: str = "Test User",
               **kwargs) -> User:
        return User(
            email=email,
            name=name,
            *kwargs
        )
    
    @staticmethod
    def create_batch(count: int, **kwargs) -> list[User]:
        return [UserFactory.create(**kwargs) for _ in range(count)]
```

## Property-Based Test Template (Python/pytest)

```python
from hypothesis import given, strategies as st

@given(
    name=st.text(min_size=1, max_size=100),
    email=st.email()
)
def test_user_creation_property(name, email):
    user = User(name=name, email=email)
    assert user.name == name
    assert user.email == email
```


path: references/testing-pyramid.md
content: # Testing Pyramid

## Concept

The testing pyramid suggests having many fast, focused unit tests at the base, fewer integration tests in the middle, and even fewer slow, end-to-end tests at the top.

```
        /\
       /  \
      / E2E\         Slow, Expensive, High Confidence
     /______\
    /        \
   /Integration\    Medium Speed, Medium Cost
  /____________\
 /              \
/    Unit        \   Fast, Cheap, Granular
/________________\

Many        Few
```

## Layer Definitions

### Unit Tests

**Speed:** Milliseconds
**Cost:** Very low
**Scope:** Single function/class
**Dependencies:** Mocked

**When to write:**
- Core business logic
- Complex algorithms
- Edge cases
- Error handling

**When to skip:**
- Trivial getters/setters
- Generated code
- UI bindings

### Integration Tests

**Speed:** Seconds
**Cost:** Medium
**Scope:** Multiple components
**Dependencies:** Real (or close to real)

**When to write:**
- Database interactions
- API integrations
- Message queue operations
- External service calls

**When to skip:**
- Unit-tested paths
- Simple transformations
- Already covered scenarios

### E2E Tests

**Speed:** Minutes
**Cost:** High
**Scope:** Complete user journeys
**Dependencies:** Full system

**When to write:**
- Critical user flows
- Payment/checkout
- Login/authentication
- Core product features

**When to skip:**
- Rare edge cases
- Internal workflows
- Developer tooling

## Practical Adaptation

Not every team should follow the pyramid strictly. Consider:

| Context | Pyramid Emphasis |
|---------|------------------|
| New project | Start with unit tests |
| Legacy code | Add integration tests first |
| High-risk domain (finance) | More E2E |
| Microservices | Contract tests + E2E |
| Data pipelines | Integration + validation |

## Anti-Patterns

### Inverted Pyramid
```
   Many E2E
   Few Unit
```
Problem: Slow, brittle, expensive to maintain

### Ice Cream Cone
```
   Many E2E
   Many Integration  
   Few Unit
```
Problem: Over-reliance on slow tests

### Single Layer
```
   Only Unit
```
Problem: Doesn't catch integration issues

## Test Execution Strategy

### On Every Commit (CI)
- Unit tests only
- Must complete in < 5 minutes

### On Every PR
- Unit tests
- Integration tests
- Lint + type check

### Nightly/Weekly
- Full E2E suite
- Performance tests
- Security scans

## Coverage Guidelines

| Layer | Target | Minimum |
|-------|--------|---------|
| Unit | 70-80% | 50% |
| Integration | 40-60% | 30% |
| E2E | Critical paths | - |


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)