Switch Between Real and Mock APIs Instantly with a Python Decorator
Learn how to use a lightweight Python @smart_mock decorator to automatically toggle between real API calls and mock responses based on an environment variable, speeding up local development, ensuring reliable CI tests, and keeping your code clean and secure.
Background
When writing automated interface tests, developers often face a trade‑off: using real APIs gives accurate results but can be slow, flaky, and dependent on external services (e.g., payment, SMS), while mock data is fast and controllable but may diverge from production behavior.
Core Goals
During development/debugging, use mock data for rapid feedback.
During pre‑release or regression testing, call the real API to verify end‑to‑end correctness.
Allow per‑function configuration so different interfaces can be mocked independently.
Make mock responses customizable to resemble real payloads.
Design Idea
The solution is a decorator @smart_mock that checks an environment variable (default MOCK_MODE). If the variable is set to 1, the decorator returns either static mock data or the result of a user‑provided mock function; otherwise it executes the original function.
Implementation of @smart_mock
import os
import functools
from typing import Any, Dict, Optional
def smart_mock(
mock_data: Optional[Dict] = None,
mock_func: Optional[callable] = None,
enabled_env_var: str = "MOCK_MODE",
):
"""Decorator that switches between real calls and mock based on an env var.
:param mock_data: static mock response (dict)
:param mock_func: dynamic mock function receiving original args
:param enabled_env_var: name of the controlling environment variable
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if os.getenv(enabled_env_var, "0") == "1":
if mock_func is not None:
return mock_func(*args, **kwargs)
elif mock_data is not None:
return mock_data
else:
raise ValueError(f"Provide mock_data or mock_func for {func.__name__}")
else:
return func(*args, **kwargs)
return wrapper
return decoratorUsage Example 1 – Static Mock
@smart_mock(mock_data={
"status": "success",
"order_id": "ORD20260124",
"amount": 99.9
})
def create_order(user_id, product_id):
"""Real logic: call the order API"""
resp = requests.post(
"https://api.example.com/orders",
json={"user_id": user_id, "product_id": product_id}
)
return resp.json()
# Test case
def test_order_creation():
result = create_order(1001, 501)
assert result["order_id"].startswith("ORD")When MOCK_MODE=1, create_order returns the predefined dictionary without any HTTP request.
Usage Example 2 – Dynamic Mock
def mock_get_user(*args, **kwargs):
user_id = kwargs.get("user_id") or args[0]
return {
"id": user_id,
"name": f"MockUser_{user_id}",
"email": f"user{user_id}@mock.com"
}
@smart_mock(mock_func=mock_get_user)
def get_user(user_id):
resp = requests.get(f"https://api.example.com/users/{user_id}")
return resp.json()
def test_user_profile():
user = get_user(888)
assert user["name"] == "MockUser_888"The dynamic mock can generate responses based on input parameters, offering more realistic behavior than static data.
Environment Switching in Practice
Local development: export MOCK_MODE=1 then run pytest test_order.py – tests finish in seconds without network.
CI regression: unset or set MOCK_MODE=0 in the CI script to hit real services.
Temporary debugging: set os.environ["MOCK_MODE"] = "1" at the top of a test file.
Security & Best Practices
Never enable mock mode in production; explicitly unset MOCK_MODE in deployment scripts.
Mock data should closely mirror real response structures to avoid false‑positive tests; generate templates from actual API traffic.
Validate mock responses with jsonschema to ensure schema compliance.
Log when a mock is used, e.g., print(f"[MOCK] Skipping real call: {func.__name__}"), for auditability.
Comparison with Other Mock Solutions
unittest.mock.patch– fine‑grained control but invasive; requires test‑code changes. responses library – designed for requests only; limited to HTTP and can be cumbersome to configure.
Our decorator – zero‑invasion, driven by environment, works with any function regardless of the underlying transport.
Conclusion
Using @smart_mock can reduce a test suite from minutes to seconds, decouple tests from flaky third‑party services, provide a single codebase that runs in both fast mock mode and real mode, and enforce safety through environment‑controlled activation.
Hands‑On Recommendation
Apply @smart_mock to the central API client module (e.g., api_client.py), prepare realistic mock payloads for each critical endpoint, and document the MOCK_MODE=1 usage in your team wiki so developers never wait for slow external callbacks again.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
