# Test Mocking Rules

> Comprehensive guidelines for when and how to use mocks, stubs, and fakes in tests

- Skill: `snoodleboot-io/test-mocking-rules` (Agent Skill)
- Install (CLI): `npx skillmds@latest add snoodleboot-io/test-mocking-rules`
- Raw SKILL.md: https://api.skillmd.com/api/skills/snoodleboot-io/test-mocking-rules/raw
- Safety review: PASS (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: snoodleboot-io (https://skillmd.com/u/snoodleboot-io)
- Updated: 2026-08-19
- Page: https://skillmd.com/skills/snoodleboot-io/test-mocking-rules

---


## Test Mocking Rules

Mocking is a testing technique that replaces real implementations with controlled substitutes. This guide defines when to mock, what to mock, and common anti-patterns to avoid.

---

## Core Principle: Mock at Boundaries, Not Internals

**The Golden Rule:** Mock only external dependencies (I/O boundaries), never internal logic.

**Why?**
- Mocking internal logic = testing implementation, not behavior
- Over-mocking = brittle tests that break when refactoring
- Under-mocking = slow, flaky tests dependent on external services

---

## When to Mock

### ✓ Mock These (Process Boundaries)

#### 1. Database Connections
**Reason:** Tests should be fast, isolated, and not require running database

**Example (Python with pytest):**
```python
from unittest.mock import Mock
import pytest

def test_get_user_by_id():
    # Mock the database connection
    mock_db = Mock()
    mock_db.query.return_value.filter.return_value.first.return_value = {
        'id': '123',
        'email': 'test@example.com'
    }
    
    user_service = UserService(db=mock_db)
    user = user_service.get_user_by_id('123')
    
    assert user['email'] == 'test@example.com'
    mock_db.query.assert_called_once()
```

**Exception:** Integration tests should use REAL database (test database or in-memory)

---

#### 2. Network Calls (HTTP, External APIs)
**Reason:** External APIs are slow, cost money, have rate limits, can be down

**Example (TypeScript with Vitest):**
```typescript
import { describe, it, expect, vi } from 'vitest'
import { PaymentService } from './payment-service'

describe('PaymentService', () => {
  it('processes payment via Stripe API', async () => {
    // Mock the HTTP client
    const mockHttpClient = {
      post: vi.fn().mockResolvedValue({
        data: { id: 'charge_123', status: 'succeeded' }
      })
    }
    
    const paymentService = new PaymentService(mockHttpClient)
    const result = await paymentService.charge(100, 'tok_visa')
    
    expect(result.status).toBe('succeeded')
    expect(mockHttpClient.post).toHaveBeenCalledWith(
      '/v1/charges',
      expect.objectContaining({ amount: 100, source: 'tok_visa' })
    )
  })
})
```

**Better Alternative:** Use a mock server library (MSW, nock, WireMock)

---

#### 3. Filesystem Operations
**Reason:** File I/O is slow, tests should not depend on disk state

**Example (Python):**
```python
from unittest.mock import mock_open, patch

def test_read_config():
    mock_file_content = "database_url=postgres://localhost"
    
    with patch('builtins.open', mock_open(read_data=mock_file_content)):
        config = ConfigLoader.load('/etc/config.txt')
        assert config['database_url'] == 'postgres://localhost'
```

**Alternative:** Use temp directories for integration tests

---

#### 4. Time/Date Functions
**Reason:** Tests should be deterministic, not dependent on current time

**Example (Python):**
```python
from unittest.mock import patch
from datetime import datetime

def test_is_expired():
    # Mock datetime.now() to return fixed time
    fixed_time = datetime(2026, 1, 1, 12, 0, 0)
    
    with patch('myapp.utils.datetime') as mock_datetime:
        mock_datetime.now.return_value = fixed_time
        
        token = Token(expires_at=datetime(2025, 12, 31))
        assert token.is_expired() is True
```

**Better Alternative:** Inject clock/time provider

---

#### 5. Random Number Generation
**Reason:** Tests should be reproducible

**Example (TypeScript):**
```typescript
import { vi } from 'vitest'

it('generates random session ID', () => {
  vi.spyOn(Math, 'random').mockReturnValue(0.5)
  
  const sessionId = generateSessionId()
  
  expect(sessionId).toBe('expected-deterministic-id')
})
```

**Better Alternative:** Inject random number generator

---

#### 6. External Services (Email, SMS, Cloud Storage)
**Reason:** Should not send real emails/SMS during tests

**Example (Python):**
```python
from unittest.mock import Mock

def test_send_welcome_email():
    mock_email_client = Mock()
    
    user_service = UserService(email_client=mock_email_client)
    user_service.register_user('test@example.com', 'password')
    
    mock_email_client.send.assert_called_once_with(
        to='test@example.com',
        subject='Welcome!',
        body=expect.any(str)
    )
```

---

## What NOT to Mock

### ✗ Never Mock These

#### 1. The Thing Under Test
**Problem:** Defeats the purpose of the test

❌ **Wrong:**
```python
def test_calculate_total():
    mock_calculator = Mock()
    mock_calculator.calculate_total.return_value = 100
    
    # Not testing anything!
    assert mock_calculator.calculate_total([10, 20, 30]) == 100
```

✓ **Correct:**
```python
def test_calculate_total():
    calculator = Calculator()
    
    # Actually tests the logic
    assert calculator.calculate_total([10, 20, 30]) == 60
```

---

#### 2. Internal Helper Functions
**Problem:** Tests implementation, not behavior

❌ **Wrong:**
```python
def test_process_order():
    mock_validate = Mock(return_value=True)
    
    with patch('myapp.order.validate_order', mock_validate):
        result = process_order(order_data)
        
    # Not testing validation logic!
    assert result.status == 'processed'
```

✓ **Correct:**
```python
def test_process_order():
    # Test through public interface - validation happens internally
    result = process_order(valid_order_data)
    assert result.status == 'processed'
    
def test_process_order_with_invalid_data():
    # Test validation by providing invalid data
    with pytest.raises(ValidationError):
        process_order(invalid_order_data)
```

**Lesson:** Test internal helpers indirectly through public API

---

#### 3. Your Own Database (in integration tests)
**Problem:** Integration tests should test real database interactions

❌ **Wrong:**
```typescript
// Integration test with mocked database
it('creates user in database', async () => {
  const mockDb = { insert: vi.fn().mockResolvedValue({ id: '123' }) }
  
  const userRepo = new UserRepository(mockDb)
  await userRepo.create({ email: 'test@example.com' })
  
  // Not actually testing database!
})
```

✓ **Correct:**
```typescript
// Integration test with real test database
it('creates user in database', async () => {
  const testDb = await setupTestDatabase()
  
  const userRepo = new UserRepository(testDb)
  const user = await userRepo.create({ email: 'test@example.com' })
  
  // Verify by querying database
  const found = await testDb.query('SELECT * FROM users WHERE id = $1', [user.id])
  expect(found.email).toBe('test@example.com')
  
  await teardownTestDatabase(testDb)
})
```

**For unit tests:** Mock database. For integration tests: Use real database.

---

#### 4. Language Standard Library
**Problem:** Standard library is well-tested, mocking adds no value

❌ **Wrong:**
```python
from unittest.mock import patch

def test_parse_json():
    with patch('json.loads', return_value={'key': 'value'}):
        result = parse_config('{"key": "value"}')
        
    # Not testing anything - json.loads is mocked!
```

✓ **Correct:**
```python
def test_parse_json():
    # Use real json.loads - it's fast and reliable
    result = parse_config('{"key": "value"}')
    assert result['key'] == 'value'
```

---

#### 5. Simple Data Structures
**Problem:** Mocking adds complexity without benefit

❌ **Wrong:**
```typescript
it('calculates cart total', () => {
  const mockCart = {
    items: vi.fn().mockReturnValue([
      { price: 10 },
      { price: 20 }
    ])
  }
  
  // Overly complex
})
```

✓ **Correct:**
```typescript
it('calculates cart total', () => {
  const cart = new Cart()
  cart.addItem({ price: 10 })
  cart.addItem({ price: 20 })
  
  expect(cart.total()).toBe(30)
})
```

---

## Mock Types: Mocks, Stubs, Fakes, Spies

### Stub
**Definition:** Returns fixed data, no behavior verification

**When to use:** Simple test cases where you only care about return value

**Example:**
```python
def test_get_user():
    # Stub: returns fixed data
    stub_db = Mock()
    stub_db.get_user.return_value = User(id='123', email='test@example.com')
    
    service = UserService(db=stub_db)
    user = service.get_user('123')
    
    assert user.email == 'test@example.com'
    # No verification of how stub was called
```

---

### Mock
**Definition:** Returns data AND verifies behavior (method calls, arguments)

**When to use:** When you need to verify interactions

**Example:**
```python
def test_send_notification():
    # Mock: verify it was called correctly
    mock_email = Mock()
    
    notifier = Notifier(email_client=mock_email)
    notifier.send_welcome('test@example.com')
    
    # Verify behavior
    mock_email.send.assert_called_once_with(
        to='test@example.com',
        subject='Welcome',
        body=expect.stringContaining('welcome')
    )
```

---

### Fake
**Definition:** Working implementation with shortcuts (in-memory database, fake API)

**When to use:** When you need realistic behavior without external dependencies

**Example:**
```typescript
// Fake in-memory database
class FakeDatabase {
  private users: Map<string, User> = new Map()
  
  async insert(user: User): Promise<User> {
    this.users.set(user.id, user)
    return user
  }
  
  async findById(id: string): Promise<User | null> {
    return this.users.get(id) || null
  }
}

it('creates and retrieves user', async () => {
  const fakeDb = new FakeDatabase()
  const repo = new UserRepository(fakeDb)
  
  await repo.create({ id: '123', email: 'test@example.com' })
  const user = await repo.findById('123')
  
  expect(user?.email).toBe('test@example.com')
})
```

**Benefits:**
- More realistic than mocks
- Tests actual logic flow
- Fast (in-memory)

---

### Spy
**Definition:** Wraps real object to record calls while preserving real behavior

**When to use:** When you want real behavior but also need to verify calls

**Example:**
```typescript
import { vi } from 'vitest'

it('logs errors when payment fails', async () => {
  const logger = new Logger()
  const loggerSpy = vi.spyOn(logger, 'error')
  
  const service = new PaymentService(logger)
  await service.charge(100, 'invalid_token')
  
  // Logger actually logs AND we can verify it was called
  expect(loggerSpy).toHaveBeenCalledWith(
    'Payment failed',
    expect.objectContaining({ token: 'invalid_token' })
  )
})
```

---

## Mocking Patterns

### Pattern 1: Dependency Injection

**Principle:** Pass dependencies as constructor/function arguments

✓ **Good (testable):**
```python
class UserService:
    def __init__(self, db, email_client):
        self.db = db
        self.email_client = email_client
    
    def register(self, email, password):
        user = self.db.insert_user(email, password)
        self.email_client.send_welcome(email)
        return user

# Easy to test - inject mocks
def test_register():
    mock_db = Mock()
    mock_email = Mock()
    
    service = UserService(db=mock_db, email_client=mock_email)
    service.register('test@example.com', 'password')
    
    mock_db.insert_user.assert_called_once()
    mock_email.send_welcome.assert_called_once()
```

❌ **Bad (hard to test):**
```python
class UserService:
    def __init__(self):
        # Hard-coded dependencies
        self.db = Database()
        self.email_client = EmailClient()
    
    def register(self, email, password):
        # Can't inject mocks!
        user = self.db.insert_user(email, password)
        self.email_client.send_welcome(email)
        return user
```

---

### Pattern 2: Interface/Protocol-Based Mocking

**Principle:** Define interfaces, mock implementations

**Example (TypeScript):**
```typescript
// Define interface
interface PaymentGateway {
  charge(amount: number, token: string): Promise<ChargeResult>
}

// Real implementation
class StripeGateway implements PaymentGateway {
  async charge(amount: number, token: string): Promise<ChargeResult> {
    // Real Stripe API call
  }
}

// Mock implementation
class MockPaymentGateway implements PaymentGateway {
  async charge(amount: number, token: string): Promise<ChargeResult> {
    return { id: 'mock_charge', status: 'succeeded' }
  }
}

// Service uses interface
class PaymentService {
  constructor(private gateway: PaymentGateway) {}
  
  async processPayment(amount: number, token: string) {
    const result = await this.gateway.charge(amount, token)
    return result
  }
}

// Test with mock
it('processes payment', async () => {
  const mockGateway = new MockPaymentGateway()
  const service = new PaymentService(mockGateway)
  
  const result = await service.processPayment(100, 'tok_visa')
  expect(result.status).toBe('succeeded')
})
```

---

### Pattern 3: Builder Pattern for Test Data

**Principle:** Use builders to create test objects with sensible defaults

**Example:**
```python
class UserBuilder:
    def __init__(self):
        self.id = 'default-id'
        self.email = 'default@example.com'
        self.is_active = True
    
    def with_id(self, id):
        self.id = id
        return self
    
    def with_email(self, email):
        self.email = email
        return self
    
    def inactive(self):
        self.is_active = False
        return self
    
    def build(self):
        return User(id=self.id, email=self.email, is_active=self.is_active)

# Clean test code
def test_send_email_to_active_users():
    active_user = UserBuilder().with_email('active@example.com').build()
    inactive_user = UserBuilder().inactive().build()
    
    email_service.send_newsletter([active_user, inactive_user])
    
    # Only active user receives email
    assert_email_sent_to('active@example.com')
    assert_email_not_sent_to('inactive@example.com')
```

---

## Anti-Patterns to Avoid

### Anti-Pattern 1: Mocking Everything

**Problem:** Tests become brittle, don't test real behavior

❌ **Wrong:**
```python
def test_process_order():
    mock_validate = Mock(return_value=True)
    mock_calculate = Mock(return_value=100)
    mock_save = Mock()
    mock_email = Mock()
    
    with patch.multiple('myapp.order',
                        validate=mock_validate,
                        calculate_total=mock_calculate,
                        save_order=mock_save,
                        send_confirmation=mock_email):
        process_order(order_data)
    
    # Not testing any real logic!
```

✓ **Better:**
```python
def test_process_order():
    # Only mock external dependencies
    mock_db = Mock()
    mock_email = Mock()
    
    service = OrderService(db=mock_db, email=mock_email)
    result = service.process_order(order_data)
    
    # Real validation and calculation happen
    assert result.total == 100
    mock_db.save.assert_called_once()
    mock_email.send.assert_called_once()
```

---

### Anti-Pattern 2: Overly Specific Mocks

**Problem:** Tests break when irrelevant details change

❌ **Brittle:**
```python
mock_api.post.assert_called_once_with(
    '/users',
    headers={'Content-Type': 'application/json', 'User-Agent': 'MyApp/1.0'},
    json={'email': 'test@example.com', 'name': 'Test', 'age': 25},
    timeout=30
)
```

✓ **Resilient:**
```python
mock_api.post.assert_called_once()
call_args = mock_api.post.call_args
assert call_args[0][0] == '/users'
assert call_args[1]['json']['email'] == 'test@example.com'
# Don't assert on headers, timeout unless critical
```

---

### Anti-Pattern 3: Testing Mock Configuration

**Problem:** Test verifies mock setup, not actual logic

❌ **Wrong:**
```python
def test_mock_returns_correct_value():
    mock_service = Mock()
    mock_service.get_user.return_value = User(id='123')
    
    # This just tests that the mock works - useless!
    assert mock_service.get_user().id == '123'
```

---

### Anti-Pattern 4: Not Resetting Mocks Between Tests

**Problem:** Test pollution - tests affect each other

❌ **Wrong:**
```python
mock_db = Mock()  # Shared across tests

def test_create_user():
    service = UserService(db=mock_db)
    service.create_user('test@example.com')
    
    assert mock_db.insert.call_count == 1

def test_create_another_user():
    service = UserService(db=mock_db)
    service.create_user('test2@example.com')
    
    # Fails! call_count is 2 because previous test's call is still tracked
    assert mock_db.insert.call_count == 1
```

✓ **Correct:**
```python
import pytest

@pytest.fixture
def mock_db():
    # Fresh mock for each test
    return Mock()

def test_create_user(mock_db):
    service = UserService(db=mock_db)
    service.create_user('test@example.com')
    
    assert mock_db.insert.call_count == 1
```

---

## Language-Specific Best Practices

### Python (pytest + unittest.mock)
```python
from unittest.mock import Mock, patch, MagicMock
import pytest

# Use fixtures for mocks
@pytest.fixture
def mock_db():
    return Mock()

# Use patch for global dependencies
def test_with_patching():
    with patch('myapp.utils.send_email') as mock_email:
        send_welcome_email('test@example.com')
        mock_email.assert_called_once()

# Use MagicMock for magic methods
def test_context_manager():
    mock_file = MagicMock()
    with mock_file as f:
        f.read.return_value = 'content'
```

---

### TypeScript/JavaScript (Vitest/Jest)
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest'

describe('UserService', () => {
  let mockDb: any
  
  beforeEach(() => {
    mockDb = {
      insert: vi.fn(),
      findById: vi.fn()
    }
  })
  
  it('creates user', async () => {
    mockDb.insert.mockResolvedValue({ id: '123' })
    
    const service = new UserService(mockDb)
    const user = await service.create({ email: 'test@example.com' })
    
    expect(user.id).toBe('123')
    expect(mockDb.insert).toHaveBeenCalledOnce()
  })
})
```

---

## Summary Checklist

Before adding a mock to your test, ask:

- [ ] Is this an external dependency (database, API, filesystem)?
- [ ] Can I use a fake instead of a mock?
- [ ] Am I mocking the thing under test? (Don't!)
- [ ] Am I mocking internal helpers? (Don't!)
- [ ] Will this make my test brittle?
- [ ] Is dependency injection set up correctly?
- [ ] Are mocks reset between tests?

**Golden Rules:**
1. Mock at boundaries (I/O), not internals
2. Prefer fakes over mocks when possible
3. Use dependency injection
4. Keep mocks simple and resilient
5. Integration tests use real dependencies

