Mock vs Live Testing Trade-offs

A decision framework for when to mock dependencies and when to test against real infrastructure

Tags

Mock vs Live Testing Trade-offs

The Lesson

Mocks prove your code calls the right functions in the right order; live tests prove your system actually works. Neither is universally better — the decision depends on what failure class you're trying to catch, how stable the dependency is, and whether the dependency is available in your test environment.

Context

Lessons Hub V2 has a backend with multiple adapter layers: LLM adapters (Ollama, AWS Bedrock, Azure OpenAI, GCP Vertex), vector adapters (ChromaDB, OpenSearch, Azure AI Search, GCP Vector Search), and service integrations (GitHub API for discovery, file system for gap storage). Each adapter has a different availability profile in CI vs local dev vs production. The testing strategy needed a principled way to decide: mock it or run it live?

What Happened

  1. The backend started with 100% mocked tests — every adapter interaction used unittest.mock.patch or injected fake implementations. This gave fast, deterministic tests that ran anywhere.
  2. When the Ollama adapter was tested live for the first time, it revealed that the prompt template produced responses the citation extractor couldn't parse. The mock had been returning hand-crafted "ideal" responses that never existed in practice.
  3. A team elsewhere lost a quarter to a similar issue: mocked database tests passed, but the actual migration failed in production. The mock perfectly simulated the old schema while the real database had moved on.
  4. Cloud SDK adapters (Bedrock, Azure OpenAI, Vertex AI) presented the opposite problem — live testing required cloud credentials, network access, and billable API calls. Mocking was the only practical option for CI.
  5. A hybrid pattern emerged using sys.modules.setdefault(): cloud SDKs are stubbed at the module level so adapter initialization code runs without the actual SDK, while local services (Ollama, ChromaDB) are tested live when available and skipped otherwise.

Key Insights

Examples

When to Mock

When to Go Live

The Hybrid Pattern

class TestBedrockAdapter:
    """Tests run without boto3 installed — adapter init still works."""
    
    @pytest.fixture(autouse=True)
    def stub_boto3(self):
        fake = type(sys)('boto3')
        fake.client = lambda service, **kw: self.mock_client
        sys.modules.setdefault('boto3', fake)
        yield
        # Don't remove — other tests may have imported it
    
    def test_adapter_initializes(self):
        from backend.adapters.aws import BedrockAdapter
        adapter = BedrockAdapter(model_id="anthropic.claude-3-sonnet")
        assert adapter.model_id == "anthropic.claude-3-sonnet"
    
    def test_generate_calls_invoke_model(self):
        # Mock at the method level, not the module level
        self.mock_client.invoke_model.return_value = {...}
        ...

Applicability

This framework applies whenever you have:

It does NOT apply to:

Related Lessons

Related Lessons