Skip to main content

niquests-mock

CI PyPI Python License

RESPX-style HTTP mocking for niquests.

Installation

uv add niquests-mock

or

pip install niquests-mock

Usage

Fixture Style

This is the closest workflow to respx_mock in pytest.

import niquests


def test_fixture_style(niquests_mock):
    route = niquests_mock.get("https://example.org/")
    route.respond(status_code=200)

    response = niquests.get("https://example.org/")

    assert route.called
    assert response.status_code == 200

The plugin also exposes respx_mock as a compatibility alias for easier migration from respx.

Development

uv sync --dev
just check

Decorator Style

Useful when you want a familiar respx-style test shape.

import niquests
import niquests_mock as nmock


@nmock.mock
def test_decorator_style():
    route = nmock.get("https://example.org/", name="homepage").respond(status_code=200)
    response = niquests.get("https://example.org/")

    route.assert_called_once()
    assert nmock.lookup("homepage") is route
    assert response.status_code == 200

Decorator factory arguments work too:

import niquests
import niquests_mock as nmock


@nmock.mock(assert_all_called=True, base_url="https://api.example.test")
def test_strict_routes():
    nmock.get("/health", name="health").respond(status_code=200)

    response = niquests.get("https://api.example.test/health")

    assert response.status_code == 200

Decorator calls use a fresh router for every decorated function invocation. The fresh router copies router configuration such as assert_all_mocked, assert_all_called, and base_url, but it does not reuse routes registered on the decorator object itself. Register routes inside the decorated function via nmock.get(...), nmock.route(...), or other top-level helpers.

Context Manager Style

Best when you want explicit router lifetime inside the test body.

import niquests
from niquests_mock import MockRouter


def test_context_manager():
    with MockRouter(base_url="https://api.example.test") as router:
        users = router.get("/users", name="users.list").respond(
            status_code=200,
            json=[{"id": 1, "name": "Ada"}],
        )

        response = niquests.get("https://api.example.test/users")

    assert router["users.list"] is users
    users.assert_called_once()
    assert response.json() == [{"id": 1, "name": "Ada"}]

MockRouter can be nested. The innermost active router handles requests while it is active, and the outer router is restored after the inner context exits. Patch cleanup runs when a context exits, including when the test body raises an exception. Repeated start() / stop() calls on the same router are idempotent.

Strict Mode and Pass-through

By default, assert_all_mocked=True: unmatched requests raise NoMockAddress. Set assert_all_mocked=False to allow unmatched requests to use the original niquests transport.

A route can opt into pass-through even in strict mode. The request then uses the original niquests transport, so use this only for URLs that your test environment intentionally allows:

with MockRouter(assert_all_mocked=True) as router:
    router.get(live_url).pass_through()
    response = niquests.get(live_url)

Use assert_all_called=True when every registered route must be exercised. On a normal router exit, unused routes raise AllMockedAssertionError. If the test body already raised another exception, assert_all_called is skipped so the original error is preserved.

Pytest Marker Configuration

The pytest plugin accepts these marker keyword arguments:

  • assert_all_mocked
  • assert_all_called
  • base_url

Unknown marker keyword arguments raise pytest.UsageError so typos fail early.

Query Parameter Matchers

Use ANY as an individual query parameter value when the parameter must be present but its value does not matter:

import niquests
import niquests_mock as nmock


with nmock.MockRouter() as router:
    route = router.get(
        "https://api.example.test/lookup",
        params={"user_id": nmock.ANY, "page": 5},
    ).respond(json={"found": True})

    response = niquests.get(
        "https://api.example.test/lookup?user_id=user-42&page=5",
    )

route.assert_called_once()
assert response.json() == {"found": True}

unittest.mock.ANY and other objects with compatible equality semantics work the same way; niquests-mock does not depend on unittest.mock. ANY is supported for individual values inside the params mapping, not as params=ANY.

Matching Order and Precedence

Routes are matched by the active router only. Nested routers use the innermost active router first; outer routers are restored when inner contexts exit.

Within one router:

  1. Exact method + URL routes are checked first.
  2. If multiple exact routes share the same key, the most recently registered route wins.
  3. Non-exact routes, including regex/callable/pattern routes, are checked in reverse registration order. Exact-route precedence is higher than fallback route recency.
  4. If no route matches, assert_all_mocked=True raises NoMockAddress; with assert_all_mocked=False, the request uses the original niquests transport.

Diagnostics intentionally show request method/URL and route summaries, but avoid printing header values or body contents by default.

Side Effects

Use side_effect when a route needs custom logic. The callable receives the niquests.models.PreparedRequest and must return a niquests.Response or raise an exception.

import niquests
from niquests_mock import MockRouter, build_response


with MockRouter() as router:

    def create_job(request):
        return build_response(request, status_code=201, json={"id": 1})

    router.post("https://api.example.test/jobs").mock(side_effect=create_job)

    response = niquests.post("https://api.example.test/jobs", json={"name": "build"})

Exceptions are recorded on Call.exception before being re-raised.

with MockRouter() as router:
    route = router.get("https://api.example.test/fails").mock(
        side_effect=RuntimeError("boom"),
    )

For async requests, side effects may be async def callables or return awaitables.

import niquests
from niquests_mock import MockRouter, build_response


async def test_async_side_effect():
    async with MockRouter() as router:

        async def get_status(request):
            return build_response(request, status_code=200, json={"ok": True})

        router.get("https://api.example.test/status").mock(side_effect=get_status)
        response = await niquests.arequest("GET", "https://api.example.test/status")

    assert response.json() == {"ok": True}

Async Usage

MockRouter supports async context-manager use and intercepts niquests async adapter send calls for the active context. With stream=True, mocked requests return a real niquests.AsyncResponse supporting awaited content/text/JSON, asynchronous iteration, and await response.close(). Async response context managers require a niquests version with an asynchronous AsyncResponse.__aenter__: verified with 3.21.1; 3.17.0 has an upstream limitation.

async def test_async_context_manager():
    async with MockRouter(base_url="https://api.example.test") as router:
        router.get("/health").respond(json={"ok": True})
        response = await niquests.arequest("GET", "https://api.example.test/health")

    assert response.json() == {"ok": True}

Concurrency Notes

The active router is stored in a Python ContextVar.

  • Async tasks created inside an active MockRouter context inherit that active router context and can use the same registered routes.
  • Nested routers are task-local: the innermost active router handles requests for the current context, then the previous router is restored when the inner context exits.
  • New threads do not automatically inherit the active router context. If code under test performs HTTP calls in another thread, create or start a MockRouter in that thread, or explicitly propagate the Python context yourself.
  • The niquests transport methods are patched process-wide while at least one router is active, but route selection still depends on the current context. A patched send call with no active router in its context falls back to the original transport.

Compatibility Notes vs RESPX

niquests-mock is RESPX-like for common pytest workflows, but it is not a full RESPX clone. This package targets niquests, not httpx.

Supported workflows:

  • pytest fixture style via niquests_mock;
  • respx_mock fixture alias for easier migration from RESPX-shaped tests;
  • decorator style via @niquests_mock.mock;
  • context-manager style via MockRouter;
  • named routes and lookup();
  • sync and async niquests requests;
  • strict unmatched-request failures via NoMockAddress;
  • route-level pass-through;
  • exact URL matching and fallback pattern matching;
  • method, URL, scheme, host, path, headers, query params, content, and JSON matchers;
  • route call assertions and router assert_all_called.

Unsupported RESPX behavior should be treated as out of contract unless it is documented in this README or covered by tests.

Current non-goals:

  • full RESPX API parity;
  • httpx transport mocking;
  • automatic propagation of active routers into newly created threads;
  • advanced route indexing for very large fallback route sets;
  • a plugin system for custom matcher classes;
  • stable internals for MockRouter patch lifecycle or route storage;
  • supporting niquests versions below the package requirement in pyproject.toml.

niquests-mock wraps adapters selected by Session.get_adapter and AsyncSession.get_adapter, below the Session pipeline, preserving response hooks, redirects, and session cookies (including Set-Cookie and respond(cookies=...)). A small Session.send wrapper preserves callback TypeError exceptions without triggering niquests' legacy-adapter retry. Compatibility is tested from niquests 3.17.0 onward and depends on its adapter and response contracts.

Both built-in and custom mounted adapters are intercepted without replacing the session's adapter registrations. Direct adapter calls outside a Session are not intercepted. Connection-level hooks, wire-level streaming, and HTTP/2 or HTTP/3 multiplexing are not simulated; mocked responses are resolved immediately. Pass-through and unmatched non-strict requests delegate to the original adapter.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

niquests_mock-0.5.1.tar.gz (17.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

niquests_mock-0.5.1-py3-none-any.whl (20.7 kB view details)

Uploaded Python 3

File details

Details for the file niquests_mock-0.5.1.tar.gz.

File metadata

  • Download URL: niquests_mock-0.5.1.tar.gz
  • Upload date:
  • Size: 17.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for niquests_mock-0.5.1.tar.gz
Algorithm Hash digest
SHA256 c6f6fe0496dea34a2035d914922e7dee6291cc773484495b6b3daff924580f07
MD5 12c06216193ac1b60848b0374b20f3a1
BLAKE2b-256 cba9cab1751dc3f42162dfaf53fafbc4fc1fd89f882f0f22d047e66ba22100a8

See more details on using hashes here.

File details

Details for the file niquests_mock-0.5.1-py3-none-any.whl.

File metadata

  • Download URL: niquests_mock-0.5.1-py3-none-any.whl
  • Upload date:
  • Size: 20.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for niquests_mock-0.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 76d7d79b4c917783f139b8b64befd2a2bd133bbf39ec8980d12cfe904968c1ae
MD5 04cdafa31094b423000b0206869a8590
BLAKE2b-256 7137682284753a571151b71ba1aea631db71950a0abea575b4e70459cd8adb9a

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.5.1 This release

2 files

0.5.0

2 files

0.4.0

2 files

0.3.1

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page