mocking-and-test-isolation

verified

3d6becb3-7f41-42b8-ab19-f29a5454a4f6

Use when writing unit tests that depend on external systems (HTTP, DB, clock, filesystem) — mock at the boundary, never your own internals. Dummy → Stub → Spy → Mock → Fake hierarchy.

Metadata

Skill ID
3d6becb3-7f41-42b8-ab19-f29a5454a4f6
Version
1
Owner
387274b7-2891-478b-81b8-e11d5adb9319
Tags
testingmockingunit-testsisolationpython
Signature
verified
Integrity
OK
Content hash
ac3b97d6a5c5e2b29a1bf0f03f11753cc2a7af25fee00281e3ba57193327b09a
Created
2026-08-15T05:26:58Z

Skill file

Raw skill file (markdown source)
# Mocking and Test Isolation

**Use when** writing unit tests that depend on external systems — HTTP clients, databases, clocks, filesystems, or random number generators. The rule: mock at the boundary, never your own internals.

## The Test Double Hierarchy

| Double | What it does | When to use |
|--------|-------------|-------------|
| **Dummy** | Passed but never used; just satisfies the arg | Required positional args you don't test |
| **Stub** | Returns canned answers | When you need fixed, predictable responses |
| **Spy** | Records calls for later assertion | When you need to verify *that* something was called |
| **Mock** | Pre-programmed expectations (will fail early if unexpected calls happen) | When call order and exact args matter |
| **Fake** | Working implementation, but lightweight (in-memory DB) | When you need realistic behavior without real infrastructure |

## Rule of Thumb: Mock at the Boundary

```python
# BAD: Mocking your own internal function
def test_process_order():
    with patch("app.orders.calculate_tax", return_value=5.0):
        # Now your test is coupled to the implementation — you can't
        # refactor calculate_tax without breaking this test
        assert process_order(...) == ...

# GOOD: Mock at the boundary (HTTP client, DB driver, clock)
def test_process_order():
    mock_http = Mock()
    mock_http.post.return_value = {"status": "ok"}
    # process_order calls an external service — mock THAT boundary
    result = process_order(order, http_client=mock_http)
    assert result.status == "submitted"
```

### The boundary principle in practice

```python
# DO mock: external I/O, clock, randomness, filesystem
# DON'T mock: your own domain objects, service layer, utility functions

# Acceptable mocks (boundary):
with patch("app.gateway.requests.post")       # HTTP
with patch("app.repo.sqlalchemy.Session")      # Database
with patch("app.utils.datetime.now")           # Clock
with patch("app.gateway.stripe.Charge.create") # Payment

# Smelly mocks (internal):
with patch("app.orders.calculate_tax")         # Your own function
with patch("app.models.User.is_premium")       # Domain model method
```

## Inject Dependencies for Testability

Don't monkey-patch globals. Inject:

```python
# BEFORE — untestable (global dependency)
import requests

def get_weather(city):
    resp = requests.get(f"https://api.weather.com/{city}")
    return resp.json()["temp"]

# AFTER — testable (dependency injected)
def get_weather(city, http_client=None):
    http = http_client or requests  # default to real implementation
    resp = http.get(f"https://api.weather.com/{city}")
    return resp.json()["temp"]

# Test:
def test_get_weather_stub():
    stub = Mock()
    stub.get.return_value.json.return_value = {"temp": 22}
    assert get_weather("london", http_client=stub) == 22
```

## Stub, Spy, Mock — Worked Examples

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

# STUB: fixed response
def test_send_email_stub():
    mailer = Mock()
    mailer.send.return_value = True
    result = notify_user("alice@example.com", mailer=mailer)
    assert result == True

# SPY: verify it was called
def test_send_email_spy():
    mailer = Mock()
    notify_user("alice@example.com", mailer=mailer)
    mailer.send.assert_called_once_with(
        to="alice@example.com",
        subject="Welcome!",
    )

# MOCK: strict expectations (will fail on unexpected calls)
def test_send_email_mock():
    mailer = Mock(spec=EmailService)  # only allows real methods
    mailer.send.return_value = True
    result = notify_user("alice@example.com", mailer=mailer)
    assert result == True
```

## Fake Example (In-Memory Database)

```python
# A fake that implements the same interface but in memory
class FakeUserRepo:
    def __init__(self):
        self._users = {}

    def save(self, user):
        self._users[user.id] = user
        return user

    def find_by_email(self, email):
        for u in self._users.values():
            if u.email == email:
                return u
        return None

# Test uses the Fake — no real DB, but realistic behavior
def test_register_duplicate_email():
    repo = FakeUserRepo()
    repo.save(User(id="1", email="a@b.com"))
    with pytest.raises(DuplicateError):
        register("a@b.com", repo=repo)
```

## Guardrails

- **Never mock the thing you're testing.** If you mock `process_order` in a test called `test_process_order`, the test is meaningless.
- **Mock at the I/O boundary**, not at the service layer. `patch("requests.post")` is fine; `patch("app.services.calculate")` is not.
- **Prefer fakes over mocks** when you need realistic behavior. Mocks test implementation details; fakes test behavior.
- **Tests that assert mock calls but not behavior are fragile.** If your test checks `mock.doSomething()` was called but never checks the result, it's coupling to implementation.

## Pitfalls

| Pitfall | Fix |
|---------|-----|
| Mocking the system under test | Only mock dependencies, never the function/class being tested |
| Mocks hiding integration bugs | Pair unit tests with integration tests |
| `patch` decorating the wrong path | Patch where the object is USED, not where it's DEFINED |
| Tests that only assert mock calls | Also assert the return value / side effect |
| Forgetting to reset between tests | Use `autouse=True` fixtures or `mock.reset_mock()` |

## Verify / Checklist

- [ ] Mocks are only at I/O boundaries (HTTP, DB, Clock, FS, Random)
- [ ] No internal functions are mocked
- [ ] At least one test asserts actual return values, not just mock calls
- [ ] Dependencies are injectable (constructor param or default arg)
- [ ] Integration tests exist for the real boundary (don't only mock)
- [ ] No test pollution: mocks don't leak between tests

Attached files

No attached files.