Skip to main content

RESPX-like HTTP mocking for niquests

Project description

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.

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 patches niquests async send calls for the active context.

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 send 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 patches niquests.sessions.Session.send and niquests.async_session.AsyncSession.send. Compatibility depends on those methods continuing to accept a prepared request and keyword arguments in the shape used by current supported niquests versions. Regression tests cover sync and async delegation to the original send methods for pass-through and unmatched requests.

Project details


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.4.0.tar.gz (13.5 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.4.0-py3-none-any.whl (17.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: niquests_mock-0.4.0.tar.gz
  • Upload date:
  • Size: 13.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","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.4.0.tar.gz
Algorithm Hash digest
SHA256 5de1ad98d0c504e0f3dc70fcbf95fd0524f0001e6d48820d2205892cdf70a2e1
MD5 b46ee79a84442dfa1dd292b83e147c08
BLAKE2b-256 c2eef72d08790ce3c8b1a779fe780f0b1ad220304745a206f97c71e4296f324c

See more details on using hashes here.

File details

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

File metadata

  • Download URL: niquests_mock-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 17.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","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.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c1c4902f4fc6975ecd6e2dfd5ec1575515bdb465c7e696b44e3d93fd1c67ae5d
MD5 36d03db6539e2a4fef78e1729bbac4e3
BLAKE2b-256 c99ea677253e9ea85a052d730c8752a176a585d6146c5902242220ba0c97b723

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page