# Testing Strategy

> Test naming conventions, fixture patterns, mocking strategies, and coverage thresholds by stage

- Skill: `navraj007in/testing-strategy` (Agent Skill)
- Install (CLI): `npx skillmds@latest add navraj007in/testing-strategy`
- Raw SKILL.md: https://api.skillmd.com/api/skills/navraj007in/testing-strategy/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: navraj007in (https://skillmd.com/u/navraj007in)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/navraj007in/testing-strategy

---


# Testing Strategy Skill

Covers unit, integration, and e2e test patterns for all supported frameworks.

## Test File Organization

### Folder Structure
- **Unit tests:** co-locate with source files
  - `src/services/user.ts` → `src/services/__tests__/user.test.ts` (Jest/Vitest)
  - `src/services/user.ts` → `src/services/test_user.py` (pytest)
- **Integration tests:** separate `tests/` directory at project root
  - `tests/integration/auth.test.ts`
  - `tests/integration/database.test.ts`
- **E2E tests:** separate `e2e/` or `tests/e2e/` directory
  - `e2e/flows/login.test.ts`
  - `e2e/flows/checkout.test.ts`

### File Naming Convention
- Unit: `<module>.test.ts` or `<module>_test.py`
- Integration: `<feature>.integration.test.ts` or `test_<feature>.py`
- E2E: `<flow>.e2e.test.ts` or `test_<flow>_e2e.py`

## Test Structure: Arrange-Act-Assert

All tests MUST follow AAA pattern:

```typescript
describe('UserService', () => {
  describe('createUser', () => {
    it('should create a user with valid email and name', () => {
      // ARRANGE: Setup test data, mocks, fixtures
      const email = 'alice@example.com';
      const name = 'Alice Chen';
      
      // ACT: Call the function under test
      const user = service.createUser(email, name);
      
      // ASSERT: Verify the result
      expect(user.email).toBe(email);
      expect(user.name).toBe(name);
      expect(user.id).toBeDefined();
    });
  });
});
```

**Golden rule:** One logical assertion per test (one reason to fail).

## Test Naming: Descriptive, Not Clever

❌ BAD:
```typescript
it('works', () => { ... });
it('user creation 1', () => { ... });
```

✅ GOOD:
```typescript
it('should create a user with valid email and return user object with id', () => { ... });
it('should reject email without @ symbol', () => { ... });
it('should hash password before storing in database', () => { ... });
```

Pattern: `should [what happens] [given conditions if not obvious]`

## Framework-Specific Setup

### Node.js / TypeScript (Jest / Vitest)
```typescript
// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src', '<rootDir>/tests'],
  testMatch: ['**/__tests__/**/*.ts', '**/*.test.ts'],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/index.ts'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

// tsconfig.test.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "jsx": "react",
    "types": ["jest", "node"]
  }
}
```

### Python (pytest)
```python
# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
addopts = --verbose --strict-markers
markers =
  unit: unit tests
  integration: integration tests
  slow: slow running tests

# conftest.py — shared fixtures
import pytest

@pytest.fixture
def sample_user():
    return {'id': 1, 'email': 'test@example.com', 'name': 'Test User'}
```

### Go (standard testing)
```go
// *_test.go convention
package user

import "testing"

func TestCreateUser(t *testing.T) {
    user := CreateUser("alice@example.com", "Alice")
    if user.Email != "alice@example.com" {
        t.Errorf("expected %q, got %q", "alice@example.com", user.Email)
    }
}
```

### .NET (xUnit)
```csharp
public class UserServiceTests
{
    [Fact]
    public void CreateUser_WithValidEmail_ReturnsUserWithId()
    {
        // Arrange
        var service = new UserService();
        
        // Act
        var user = service.CreateUser("alice@example.com", "Alice");
        
        // Assert
        Assert.NotNull(user.Id);
        Assert.Equal("alice@example.com", user.Email);
    }
}
```

## Mocking Strategy

### Database Mocks (Unit Tests)
Use **in-memory** database for unit tests when possible:

```typescript
// jest.config.js for Node backend
module.exports = {
  setupFilesAfterEnv: ['<rootDir>/tests/setup.ts']
};

// tests/setup.ts
import { PrismaClient } from '@prisma/client';

beforeAll(async () => {
  // Point to test database
  process.env.DATABASE_URL = 'file:./test.db';
  await prisma.$executeRawUnsafe('PRAGMA foreign_keys = OFF');
});

afterEach(async () => {
  // Clean up between tests
  await prisma.$queryRaw`DELETE FROM "User"`;
  await prisma.$queryRaw`DELETE FROM "Post"`;
});

afterAll(async () => {
  await prisma.$disconnect();
});
```

### External Service Mocks (Unit Tests)
Mock HTTP calls, external APIs, payment gateways:

```typescript
// jest.mock for modules
jest.mock('../lib/stripe', () => ({
  createPayment: jest.fn().mockResolvedValue({ id: 'pay_123', status: 'succeeded' })
}));

// Manual mock for fetch
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ data: [] })
  })
);
```

### Real Database (Integration Tests)
Use **actual test database** for integration tests:

```typescript
// tests/integration/setup.ts
beforeAll(async () => {
  // Spin up test PostgreSQL via docker-compose.test.yml
  process.env.DATABASE_URL = 'postgresql://test:test@localhost:5433/test_db';
  await prisma.$executeRawUnsafe('CREATE SCHEMA IF NOT EXISTS tests');
});

afterAll(async () => {
  await prisma.$executeRawUnsafe('DROP SCHEMA tests CASCADE');
});
```

**Rule:** If a test needs the database to run, it's an integration test, not a unit test. Don't mock the database for integration tests.

## Fixture Patterns

### Shared Fixtures (Reusable Test Data)

```typescript
// tests/fixtures/user.fixtures.ts
export const validUser = {
  email: 'alice@example.com',
  name: 'Alice Chen',
  password: 'SecurePass123!'
};

export const adminUser = {
  ...validUser,
  role: 'admin'
};

export const inactiveUser = {
  ...validUser,
  status: 'inactive'
};

// In test file
import { validUser, adminUser } from './fixtures/user.fixtures';

it('should allow admin to create users', () => {
  const created = service.createUser(validUser, { createdBy: adminUser });
  expect(created.createdBy).toBe(adminUser.id);
});
```

### Factory Pattern (Dynamic Test Data)

```typescript
// tests/factories/user.factory.ts
class UserFactory {
  static async create(overrides = {}) {
    const defaults = {
      email: `user-${Date.now()}@example.com`,
      name: 'Test User',
      role: 'user'
    };
    return prisma.user.create({
      data: { ...defaults, ...overrides }
    });
  }
  
  static async createMany(count, overrides = {}) {
    return Promise.all(
      Array.from({ length: count }).map(() => this.create(overrides))
    );
  }
}

// In test
const users = await UserFactory.createMany(10, { role: 'admin' });
expect(users).toHaveLength(10);
```

## Coverage Thresholds by Stage

### MVP Stage
- **Unit tests:** ≥ 70% coverage (critical paths only)
- **Integration tests:** ≥ 5 per service (authentication, CRUD, error cases)
- **E2E tests:** ≥ 2 per major flow (happy path + error case)
- **Requirement:** Tests must run in CI before merge

### Growth Stage
- **Unit tests:** ≥ 80% coverage (all public APIs)
- **Integration tests:** ≥ 10 per service (edge cases, race conditions)
- **E2E tests:** ≥ 5 per major flow (happy path, errors, edge cases)
- **Requirement:** Failed tests block merge, coverage reports published

### Enterprise Stage
- **Unit tests:** ≥ 85% coverage (all code paths including errors)
- **Integration tests:** ≥ 15 per service + contract tests
- **E2E tests:** ≥ 10 per major flow + performance tests
- **Requirement:** Coverage must improve or stay same; no regression allowed

## Test Execution

### NPM Script Convention
```json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:unit": "jest --testPathPattern=__tests__",
    "test:integration": "jest --testPathPattern=integration",
    "test:e2e": "playwright test"
  }
}
```

### Python Convention
```bash
pytest                          # all tests
pytest -m unit                  # unit tests only
pytest -m integration           # integration tests
pytest -v --cov=src            # with coverage
```

### Go Convention
```bash
go test ./...                   # all tests
go test -cover ./...           # with coverage
go test -run TestCreateUser    # specific test
```

## Async / Await Testing

### TypeScript (Jest)
```typescript
// Always return promise or use async/await
it('should fetch user by id', async () => {
  const user = await service.getUserById(1);
  expect(user.id).toBe(1);
});

// Expect promises to resolve/reject
it('should reject invalid id', async () => {
  await expect(service.getUserById(-1)).rejects.toThrow('Invalid ID');
});
```

### Python (pytest)
```python
import pytest

@pytest.mark.asyncio
async def test_fetch_user():
    user = await service.get_user(1)
    assert user.id == 1

# Or use sync wrapper
from asyncio import run

def test_sync_wrapper():
    user = run(service.get_user(1))
    assert user.id == 1
```

## Skip & Mark Patterns

### Skip Flaky Tests
```typescript
// Jest
it.skip('should handle network timeout', () => {
  // Re-enable after fixing flakiness
});

// pytest
@pytest.mark.skip(reason="flaky, TODO: fix race condition")
def test_concurrent_updates():
    pass
```

### Mark Slow Tests
```typescript
// jest.config.js
module.exports = {
  testTimeout: 30000  // 30s for slow tests
};

// In test
jest.setTimeout(5000); // override for this test
```

### Mark as WIP (Work in Progress)
```typescript
it.todo('should support bulk user import');
```

## CI/CD Integration

### GitHub Actions
```yaml
- name: Run Tests
  run: npm test -- --coverage
  
- name: Upload Coverage
  uses: codecov/codecov-action@v3
  with:
    files: ./coverage/lcov.info
```

### Fail on Coverage Drop
```yaml
- name: Check Coverage
  run: |
    COVERAGE=$(npm test -- --coverage | grep Lines | awk '{print $NF}' | sed 's/%//')
    if (( $(echo "$COVERAGE < 80" | bc -l) )); then
      echo "Coverage $COVERAGE% below threshold 80%"
      exit 1
    fi
```

## Best Practices Checklist

- ✓ Each test file tests ONE class/module
- ✓ Each test method tests ONE behavior
- ✓ Test names describe the BEHAVIOR, not the method
- ✓ AAA structure (Arrange, Act, Assert)
- ✓ No test interdependencies (tests can run in any order)
- ✓ Mock external dependencies (APIs, databases, file system)
- ✓ Use real database for integration tests only
- ✓ Clean up after each test (teardown)
- ✓ Fixtures for reusable test data
- ✓ Factories for dynamic test data generation
- ✓ Timeouts for async tests
- ✓ Descriptive assertion messages
- ✓ One assertion per test when possible (one reason to fail)
- ✓ Avoid testing private implementation details
- ✓ Test behavior from the outside (black box)

