How to Mock API Responses for Local Development¶
OAA-runner has a built-in mock framework that lets you develop and test integrations without making real API calls. When enabled, the framework intercepts HTTP requests and returns pre-defined responses based on the request URL.
When to use this¶
Developing a new integration before the API credentials are available
Running the integration in CI without network access
Writing tests that need deterministic, repeatable responses
Debugging data-transformation logic without hitting rate limits
Overview¶
The mock framework has three pieces:
Component |
What it does |
|---|---|
|
Registers a function as the handler for a URL path |
|
Activates mocking when |
|
Context variable holding the full request URL, readable inside any mock handler |
See Mock Framework Reference for complete API details.
Step 1 — Create a mocks module¶
Keep mock code in a separate file so it is never imported into your main module:
my_integration/
modules/
myapp.py ← main fetch logic
myapp_mocks.py ← mock handlers (imported only during development)
Step 2 — Write mock handlers¶
In myapp_mocks.py, import mock_response and decorate functions that return
fake data:
from oaa.hooks.mock_responses import mock_response
from faker import Faker
faker = Faker()
# Build some realistic fake users
MOCK_USERS = {'data': {'items': [], 'total': 0}}
for _ in range(50):
MOCK_USERS['data']['items'].append({
'id': faker.uuid4(),
'username': faker.user_name(),
'email': faker.email(),
'status': 'active',
})
MOCK_USERS['data']['total'] = len(MOCK_USERS['data']['items'])
@mock_response(path='/v1/users')
def users():
return MOCK_USERS
@mock_response(path='/v1/roles')
def roles():
return {
'data': {
'items': [
{'id': 'admin', 'name': 'Administrator'},
{'id': 'viewer', 'name': 'Read-Only Viewer'},
]
}
}
Matching paths with query parameters¶
Register the full path including the query string to match a specific parameter combination:
@mock_response(path='/v1/users?status=active')
def active_users():
return {'data': {'items': ACTIVE_USERS}}
@mock_response(path='/v1/users?status=inactive')
def inactive_users():
return {'data': {'items': INACTIVE_USERS}}
When the caller sends /v1/users with no query string, the framework falls
back to any path-only mock that is registered for /v1/users.
Matching with wildcards¶
Use * to match any single path segment (does not cross /):
@mock_response(path='/v1/users/*')
def user_detail():
return {'data': {'id': '123', 'username': 'alice'}}
This matches /v1/users/123 or /v1/users/alice but not
/v1/users/groups/admins.
Matching with regex¶
Prefix the path with re: to use a full regular expression against the
path portion:
@mock_response(path='re:/v1/users/[0-9]+')
def user_by_id():
return {'data': {'id': '42'}}
@mock_response(path='re:/v1/.*/permissions')
def any_resource_permissions():
return {'data': {'permissions': ['read', 'write']}}
Reading the request URL inside a handler¶
Use current_request_url to inspect the full URL (including query string)
that triggered the mock. This lets a single handler return different data
depending on what was requested:
from oaa.hooks.mock_responses import mock_response, current_request_url
import urllib.parse
@mock_response(path='/v1/report/*')
def report_handler():
url = current_request_url.get()
parsed = urllib.parse.urlparse(url)
qs = urllib.parse.parse_qs(parsed.query)
entity_type = qs.get('entityType', ['Unknown'])[0]
return MOCK_DATA_BY_TYPE.get(entity_type, [])
No special setup is needed — current_request_url is set automatically
before your handler is called.
Step 3 — Activate mocking in your fetch function¶
In myapp.py, call maybe_mock_it after constructing the API client:
from oaa.hooks.mock_responses import maybe_mock_it
from oaa.modules.lazy_dicts import lazy_dict_table
def fetch(connection=None, source=None, **kwargs):
api = MyAppAPI(connection, source)
# Replace api.get with a mock when MOCK_RESULTS=true
maybe_mock_it(api, 'get', connection, source)
resource = source.params.get('resource', '')
results = api.fetch_paginated(resource)
return lazy_dict_table(results)
maybe_mock_it checks the MOCK_RESULTS setting and patches the named
method only when it is "true". When MOCK_RESULTS is absent or false,
the real API method is used — no code path changes required.
Important: Do not import myapp_mocks in myapp.py. Load the mocks
module separately (see Step 4).
Step 4 — Load mocks without importing into the main module¶
Because @mock_response registers handlers in a global registry at import
time, you only need to import the mocks module once — before fetch() runs.
Option A — config.yaml pre_module¶
If your runner config supports a pre_module hook, point it at your mocks
file. This keeps the import entirely out of the main module.
Option B — environment variable + importlib¶
# In a test or dev entrypoint, NOT in myapp.py:
import importlib, os
if os.environ.get('MOCK_RESULTS', '').lower() == 'true':
importlib.import_module('myapp_mocks')
Option C — direct import in tests¶
# In your pytest test file:
import myapp_mocks # registers all @mock_response handlers
from myapp import fetch
Step 5 — Enable mocking via config or environment¶
Set MOCK_RESULTS=true in your .env file or config.yaml:
# .env
MOCK_RESULTS=true
# config.yaml
connection:
params:
MOCK_RESULTS: "true"
Then run normally with uv:
uv tool run --from oaa-runner oaa app run --config config.yaml
Match priority¶
When a request arrives, the framework tries these in order and returns the first match:
Priority |
Key form |
Example |
|---|---|---|
1 |
Exact full URL |
|
2 |
Path + query string |
|
3 |
Path only |
|
4 |
Regex ( |
|
5 |
Wildcard ( |
|
Regex and wildcard keys are scanned in the order they were registered.
Complete example¶
modules/app_bw_mocks.py:
from oaa.hooks.mock_responses import mock_response, current_request_url
from faker import Faker
import urllib.parse
faker = Faker()
MOCK_USERS = [
{'id': str(i), 'username': faker.user_name(), 'email': faker.email()}
for i in range(100)
]
MOCK_ROLES = [
{'id': 'admin', 'name': 'Administrator'},
{'id': 'analyst', 'name': 'Analyst'},
{'id': 'viewer', 'name': 'Viewer'},
]
@mock_response(path='/api/v2/users')
def users():
return {'results': MOCK_USERS, 'total': len(MOCK_USERS)}
@mock_response(path='/api/v2/roles')
def roles():
return {'results': MOCK_ROLES}
@mock_response(path='re:/api/v2/users/[^/]+/roles')
def user_roles():
url = current_request_url.get()
user_id = urllib.parse.urlparse(url).path.split('/')[4]
# Return a deterministic subset based on user_id
return {'results': [MOCK_ROLES[int(user_id) % len(MOCK_ROLES)]]}
modules/app_bw.py:
from oaa.hooks.mock_responses import maybe_mock_it
from oaa.modules.lazy_dicts import lazy_dict_table
def fetch(connection=None, source=None, **kwargs):
api = BongaWongaAPI(connection, source)
maybe_mock_it(api, 'get', connection, source)
resource = source.params.get('resource', '')
results = api.fetch_paginated(resource)
return lazy_dict_table(results)