Testing an adapter
Testing an adapter
Part of: Develop > Guides | Related: Testing adapters, Testing tiers, Adding an adapter
Step-by-step guide for writing and running an adapter's tests. It covers the mechanics — mocking, fixtures, commands — while Testing adapters defines what coverage is required.
When to test
Every new or changed adapter needs tests before it is merged. Write them alongside the adapter — it is the "Add tests" step of Adding an adapter — not afterward. The offline default must stay green, so tests mock the upstream system rather than calling it.
Prerequisites
- A working dev environment (
uv sync). - The adapter under test, with its
model_loaderand any incremental methods. - Familiarity with
unittest.mockand pytest fixtures. - The existing tests as templates:
tests/adapters/test_netbox_incremental.pyandtests/test_diffsync_mixin_contract.py.
Steps
Step 1: Create the test module
Add tests/adapters/test_<adapter>_*.py — for example test_mysystem_incremental.py for the
cursor methods and test_mysystem_loader.py for conversion.
Step 2: Guard the optional dependency
If the adapter hard-imports an optional SDK, skip the whole module when it is absent so test collection stays green:
import pytest
pytest.importorskip("pynetbox") # at module top, before importing the adapter
Step 3: Build a fake client
Stub the upstream client with MagicMock and feed it sample records shaped like the real API
response. Construct the adapter, then replace its client with the mock:
from unittest.mock import MagicMock
from infrahub_sync.adapters.mysystem import MysystemAdapter
def make_adapter(config):
adapter = MysystemAdapter(target="source", adapter=config.source, config=config)
adapter.client = MagicMock()
return adapter
Keep the sample records and a minimal SyncConfig (with a schema_mapping) in fixtures so
several tests share them. tests/adapters/test_netbox_incremental.py shows the concrete
construction for a real adapter — mirror it.
Step 4: Test the conversion path
Drive model_loader (or obj_to_diffsync) with the fake records and assert the resulting
models. Check that fields map to the right names, that local_id and identifiers are set,
and that reference fields resolve to peer unique_ids:
def test_loader_maps_fields(adapter, device_model):
adapter.client.get.return_value = [{"name": "rtr1", "device_type": "qfx"}]
adapter.model_loader("InfraDevice", device_model)
obj = adapter.get(device_model, "rtr1")
assert obj.type == "qfx"
Add cases for filters (kept vs dropped) and transforms (native-typed output).
Step 5: Test the incremental contract
If the adapter declares cursor support, assert each method:
from infrahub_sync.cache.cursors import CursorState, CursorTier
def test_cursor_tier(adapter):
assert adapter.cursor_tier_for("InfraDevice") == CursorTier.TIMESTAMP
assert adapter.cursor_tier_for("Unmapped") == CursorTier.NONE
def test_list_changed_since_uses_change_filter(adapter):
cursor = CursorState(tier=CursorTier.TIMESTAMP, value="2024-01-01T00:00:00Z")
list(adapter.list_changed_since("InfraDevice", cursor))
# assert the client was queried with the change filter, e.g. last_updated__gte
Also assert that declaring a non-NONE tier without implementing list_changed_since raises
NotImplementedError — the mixin defaults are covered in tests/test_diffsync_mixin_contract.py.
Step 6: Test the edge cases
Cover empty result sets, pagination, and that authentication failures (401 / 403), timeouts, and unknown model names raise clear errors rather than passing silently.
Step 7: Run the suite
uv run pytest -q tests/adapters/test_mysystem_loader.py # the module you are writing
uv run invoke tests.tests-unit # the offline default
Integration tests
Tests that talk to a live system go under tests/integration/, marked so they are opt-in:
import pytest
@pytest.mark.integration
def test_live_load(): ...
Run them explicitly and only when credentials are available:
uv run invoke tests.tests-integration
Keep them out of the default run — uv run invoke tests.tests-unit must pass with no network
and no secrets. Testing tiers covers what the integration
tier needs before it proves anything.
Verification
uv run invoke tests.tests-unitpasses offline, with no live system and no credentials.- The module skips cleanly (not errors) when the optional SDK is not installed.
uv run invoke lintis clean on the new test files.
Quality checklist
- Upstream client mocked; no network in the offline default.
- Conversion covered: field mapping,
local_id, identifiers, references (single and list). - Filters and transforms covered.
- Cursor methods covered (tier, change filter, existing ids) if the adapter is incremental.
- Edge cases covered: empty, pagination, 401/403, timeout, unknown model.
-
pytest.importorskipguards an optional-dependency import. - Live tests marked
@pytest.mark.integrationundertests/integration/.
Related resources
- Testing adapters — the required coverage and conventions.
- Testing tiers — which command runs which suite.
- Adding an adapter — where testing fits in the full procedure.
- Incremental sync and cache — the behavior to test.