mock-friendly-api-layering
npx machina-cli add skill shimo4228/claude-code-learned-skills/mock-friendly-api-layering --openclawMock-Friendly API Layering
Extracted: 2026-02-11
Context: AnkiConnect client (anki_connect.py) — public functions forwarding url to internal _invoke broke mock assertions.
Problem
When public functions forward internal routing parameters (like url, timeout, base_path) to a low-level helper, tests that mock the helper fail on assert_called_once_with() because of unexpected kwargs.
# BAD: ensure_deck forwards url to _invoke
def ensure_deck(name: str, *, url: str = ANKICONNECT_URL) -> None:
_invoke("createDeck", url=url, deck=name)
# Test expects:
mock.assert_called_once_with("createDeck", deck="pdf2anki::test")
# Actual call:
# _invoke("createDeck", url="http://127.0.0.1:8765", deck="pdf2anki::test")
# → AssertionError: unexpected kwarg url
Solution
Keep internal routing parameters only on the lowest-level function (_invoke). Public functions expose only business-relevant parameters.
# GOOD: public function has no url param
def ensure_deck(name: str) -> None:
_invoke("createDeck", deck=name)
# _invoke owns the url default internally
def _invoke(action: str, *, url: str = ANKICONNECT_URL, **params: Any) -> Any:
...
Design Principle
| Layer | Owns | Example |
|---|---|---|
_invoke (internal) | Transport: url, timeout, headers | _invoke("action", url=..., **params) |
| Public functions | Business params only | ensure_deck(name), push_cards(cards, deck_name=...) |
When to Use
- Wrapping HTTP APIs (AnkiConnect, REST, GraphQL) with an internal
_invoke/_requesthelper - Any module with a low-level helper + multiple public functions that delegate to it
- When
mock.assert_called_once_with()fails due to extra kwargs from defaults
Source
git clone https://github.com/shimo4228/claude-code-learned-skills/blob/main/skills/mock-friendly-api-layering/SKILL.mdView on GitHub Overview
Mock-Friendly API Layering addresses failures when tests mock a low-level helper but public functions forward transport params like url or timeout. By keeping transport concerns inside the lowest-level function and exposing only business parameters at public APIs, mock assertions stay stable across changes. This pattern clarifies responsibilities and makes tests deterministic.
How This Skill Works
Place all transport details (url, timeout, headers) in the internal _invoke (or _request) function. Public functions should not accept or forward these internal params; they call the internal helper with only the business data. Tests should assert against the public call signatures, not the transport-layer kwargs.
When to Use It
- Wrapping HTTP APIs (AnkiConnect, REST, GraphQL) with an internal _invoke or _request helper
- Any module that has a low-level helper plus multiple public functions delegating to it
- When mock.assert_called_once_with() fails due to extra kwargs from default transport parameters
- When you want the public API to expose only business parameters for clarity
- When you want transport concerns isolated to the internal function across all calls
Quick Start
- Step 1: Identify the internal transport helper (_invoke)
- Step 2: Move url/timeout/headers into the internal function and remove them from public APIs
- Step 3: Refactor public functions to pass business params only and adjust tests
Best Practices
- Put transport params (url, timeout, headers) on _invoke only
- Public functions should not declare internal transport params
- Keep a clear separation of concerns between _invoke and public wrappers
- Document the expected business params at the public API
- Update tests to assert on business params, not transport kwargs
Example Use Cases
- AnkiConnect client (anki_connect.py) public functions forwarding url to _invoke
- REST API client where a shared _invoke handles base_url and headers
- GraphQL client that routes through a _request helper
- Test suites where mock.assert_called_once_with() fails due to extra kwargs
- Public wrappers like ensure_deck(name) calling _invoke('createDeck', deck=name)