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

@mock_response(path=...)

Registers a function as the handler for a URL path

maybe_mock_it(api, 'get', connection, source)

Activates mocking when MOCK_RESULTS=true

current_request_url

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

https://api.example.com/v1/users?page=2

2

Path + query string

/v1/users?page=2

3

Path only

/v1/users

4

Regex (re: prefix)

re:/v1/users/[0-9]+

5

Wildcard (* / ?)

/v1/users/*

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)