Testing providers
July 30, 2026 · View on GitHub
Provider tests must be offline and deterministic: no live network in the suite. The whole resolver is silent-failure, so a test that quietly hits the network would pass even when the provider is broken. Patch the transport and feed it a known payload.
Running the suite
pip install .[test]
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest -q
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 keeps third-party pytest plugins from
loading, which is what CI uses.
Two offline patterns
1. Mock the HTTP session (JSON APIs)
Patch the provider's HTTP entry point and return a fake response. This is the
TMDB/TVDB cassette style: see tests/test_tmdb_cassette.py.
import os
from unittest.mock import MagicMock, patch
from mediavocab.models.signals import Signals
from metadatarr.resolve.providers.tmdb import TMDBProvider
def _response(payload, status=200):
resp = MagicMock()
resp.status_code = status
resp.json.return_value = payload
resp.raise_for_status = MagicMock()
return resp
def test_movie_match():
p = TMDBProvider()
payload = {"results": [{"id": 27205, "title": "Inception",
"release_date": "2010-07-16"}]}
sess = MagicMock()
sess.get.return_value = _response(payload)
with patch.dict(os.environ, {"TMDB_API_KEY": "fake-key"}):
with patch("metadatarr.resolve.providers.tmdb._http", return_value=sess):
m = p.lookup(Signals(title="Inception", year=2010))
assert m.external_ids.tmdb_movie == 27205
Always patch at the seam the provider actually calls (its _http factory, or
the upstream client method): patch what the provider imports, not where the
symbol is defined.
2. Committed response fixtures (HTML scrapers)
For HTML scrapers, capture a real page once, commit it under
tests/fixtures/<area>/, and serve it from disk. See
tests/test_physical_clients.py and
tests/fixtures/physical/.
from pathlib import Path
FIXTURES = Path(__file__).parent / "fixtures" / "physical"
def test_bluray_parse():
html = (FIXTURES / "bluray_com_moon_17549.html").read_text(encoding="utf-8")
# patch the client's fetch to return `html`, then assert on the parse
The per-provider smoke contract
Every registered provider is covered by a parametrized smoke test
(tests/test_providers_smoke.py) that asserts the universal contract with no
network:
is_available()returns abooland never raises.lookup(signals)returnsProviderMatch | Noneand never raises: even when the upstream transport blows up. The test injects a failing session to prove the swallow-log-return-None contract holds.
When you add a provider it is picked up automatically (the test parametrizes over the live registry), so there is nothing to wire up: but a provider that raises on a transport error, or returns the wrong type, will fail it.
Tips
- Test the unavailable path too (missing key/dep →
is_available()isFalse), the no-results path (lookup→None), and the error path (transport raises →lookup→None, with a warning logged). - Use
match_qualityin assertions sparingly, prefer asserting on the emittedexternal_ids/signalsfields, which are the contract callers depend on. - Genre/field emission is guarded centrally
(
tests/test_provider_genre_emission.py), if your provider emitscontent_genres, make sure they come fromGENRE_*constants.