Claude
Skills
Sign in
Back

testing-strategies

Included with Lifetime
$97 forever

Use when writing tests, setting up test frameworks, implementing mocking strategies, or establishing testing best practices (unit, integration, E2E) across any technology stack.

Writing & Docs

What this skill does


# Universal Testing Strategies

## Overview

Language-agnostic testing patterns and strategies applicable across all technology stacks.

## Testing Pyramid

```
          ┌───────┐
          │  E2E  │  Few, slow, expensive
         ─┴───────┴─
        ┌───────────┐
        │Integration│  Some, medium speed
       ─┴───────────┴─
      ┌───────────────┐
      │     Unit      │  Many, fast, cheap
     ─┴───────────────┴─
```

### Unit Tests (70-80%)

- Test individual functions/methods in isolation
- Mock external dependencies
- Fast execution (< 10ms per test)
- High volume, run on every save

### Integration Tests (15-20%)

- Test component interactions
- Real database/API calls (often with test containers)
- Medium speed (100ms - 1s per test)
- Run on commit/PR

### End-to-End Tests (5-10%)

- Test complete user flows
- Real browser/environment
- Slow (seconds to minutes)
- Run before deployment

## Test Structure: AAA Pattern

```
// Arrange - Set up test data and conditions
const user = createTestUser({ email: '[email protected]' });
const service = new UserService(mockDb);

// Act - Execute the code under test
const result = await service.createUser(user);

// Assert - Verify the expected outcome
expect(result.id).toBeDefined();
expect(result.email).toBe('[email protected]');
```

## Naming Conventions

### Test File Names

```
component.test.ts       # Unit tests
component.spec.ts       # Alternative convention
component.integration.ts # Integration tests
component.e2e.ts        # End-to-end tests
```

### Test Case Names

Use descriptive names that explain the scenario:

```
// Pattern: should [expected behavior] when [condition]
describe('UserService', () => {
  describe('createUser', () => {
    it('should create user when valid data provided', () => {});
    it('should throw ValidationError when email is invalid', () => {});
    it('should throw DuplicateError when email already exists', () => {});
  });
});
```

### BDD Style (Given-When-Then)

```
describe('User Registration', () => {
  describe('given a valid email and password', () => {
    describe('when the user submits the form', () => {
      it('then creates a new account', () => {});
      it('then sends a welcome email', () => {});
    });
  });
});
```

## Mocking Strategies

### Test Doubles Overview

| Type | Purpose | Example |
|------|---------|---------|
| **Stub** | Returns predetermined values | `stub.returns(42)` |
| **Mock** | Verifies interactions | `expect(mock).toHaveBeenCalled()` |
| **Spy** | Wraps real implementation | `spy(realFunction)` |
| **Fake** | Working implementation (simplified) | In-memory database |
| **Dummy** | Placeholder (not used) | Required parameter |

### When to Mock

**DO Mock:**
- External services (APIs, databases)
- Time-dependent functions
- Random number generators
- File system operations
- Network requests

**DON'T Mock:**
- The code under test
- Simple value objects
- Pure functions with no side effects

### Mock Example

```typescript
// Mock external API
const mockApi = {
  getUser: jest.fn().mockResolvedValue({ id: '1', name: 'Test' })
};

// Inject mock
const service = new UserService(mockApi);
const result = await service.getUser('1');

// Verify interaction
expect(mockApi.getUser).toHaveBeenCalledWith('1');
expect(result.name).toBe('Test');
```

## Test Data Management

### Test Factories

Create reusable factory functions for test data:

```typescript
// factories/user.ts
export function createTestUser(overrides = {}) {
  return {
    id: randomUUID(),
    email: `test-${Date.now()}@example.com`,
    name: 'Test User',
    createdAt: new Date(),
    ...overrides
  };
}

// In tests
const user = createTestUser({ name: 'Custom Name' });
```

### Fixtures

Static test data for consistent testing:

```typescript
// fixtures/users.ts
export const validUser = {
  email: '[email protected]',
  password: 'SecurePass123!'
};

export const invalidEmails = [
  'no-at-sign',
  '@no-local.com',
  'no-domain@',
  'spaces [email protected]'
];
```

## Assertion Best Practices

### Be Specific

```typescript
// BAD - vague assertion
expect(result).toBeTruthy();

// GOOD - specific assertion
expect(result.success).toBe(true);
expect(result.data.id).toBe('expected-id');
```

### One Logical Assertion Per Test

```typescript
// BAD - multiple unrelated assertions
it('should process order', () => {
  expect(order.id).toBeDefined();
  expect(order.total).toBe(100);
  expect(emailService.send).toHaveBeenCalled();
  expect(inventory.reduce).toHaveBeenCalled();
});

// GOOD - focused tests
it('should assign an order ID', () => {
  expect(order.id).toBeDefined();
});

it('should calculate correct total', () => {
  expect(order.total).toBe(100);
});

it('should send confirmation email', () => {
  expect(emailService.send).toHaveBeenCalled();
});
```

### Custom Matchers

Create domain-specific matchers for readability:

```typescript
expect.extend({
  toBeValidEmail(received) {
    const pass = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(received);
    return {
      pass,
      message: () => `expected ${received} to be a valid email`
    };
  }
});

// Usage
expect('[email protected]').toBeValidEmail();
```

## Edge Cases to Test

### Input Validation

- Empty/null/undefined inputs
- Boundary values (0, -1, MAX_INT)
- Invalid types
- Malformed data

### State Transitions

- Initial state
- After single operation
- After multiple operations
- After error and recovery

### Async Operations

- Successful completion
- Timeout
- Network errors
- Concurrent operations
- Race conditions

### Error Handling

- Expected errors (validation)
- Unexpected errors (system)
- Error recovery
- Error propagation

## Code Coverage Guidelines

### Coverage Targets

| Type | Target | Notes |
|------|--------|-------|
| Line Coverage | 80% | Minimum acceptable |
| Branch Coverage | 75% | Test all conditionals |
| Function Coverage | 90% | All public APIs |
| Critical Paths | 100% | Auth, payments, data integrity |

### Coverage Pitfalls

**High coverage ≠ good tests**

```typescript
// 100% coverage but useless test
it('should run without error', () => {
  processOrder(order); // No assertions!
});
```

**Focus on:**
- Business-critical paths
- Edge cases and error handling
- Integration points

## Test Performance

### Keep Tests Fast

| Test Type | Target Time |
|-----------|-------------|
| Unit test | < 10ms |
| Integration test | < 1s |
| E2E test | < 30s |
| Full suite | < 5min |

### Parallelization

```bash
# Run tests in parallel
vitest --pool=threads
jest --maxWorkers=4
pytest -n auto
go test -parallel 4
```

### Test Isolation

Tests should not depend on each other:

```typescript
// BAD - shared state
let counter = 0;
it('test 1', () => { counter++; });
it('test 2', () => { expect(counter).toBe(1); }); // Fragile!

// GOOD - isolated state
beforeEach(() => { counter = 0; });
it('test 1', () => { counter++; expect(counter).toBe(1); });
it('test 2', () => { counter++; expect(counter).toBe(1); });
```

## CI/CD Integration

### Pre-commit

- Lint checks
- Type checks
- Unit tests (fast)

### Pull Request

- Full unit test suite
- Integration tests
- Coverage report

### Pre-deployment

- E2E tests
- Performance tests
- Security scans

## Test Maintenance

### Avoid Flaky Tests

- Don't depend on timing
- Don't depend on external services
- Use deterministic data
- Retry with care (hide real issues)

### Keep Tests Readable

- Clear naming
- Minimal setup
- Obvious assertions
- Helpful failure messages

### Review Test Code

- Test code is production code
- Apply same quality standards
- Refactor when needed

---

*Testing strategies applicable to all technology stacks*

Related in Writing & Docs