Skip to main content

Socket Mock Framework - for all kinds of socket animals, web-clients included - with gevent/asyncio/SSL support

Project description

https://github.com/mindflayer/python-mocket/actions/workflows/main.yml/badge.svg?branch=main https://codecov.io/github/mindflayer/python-mocket/graph/badge.svg?token=htRySebRBt https://app.codacy.com/project/badge/Grade/6327640518ce42adaf59368217028f14 https://img.shields.io/pypi/dm/mocket https://deepwiki.com/badge.svg Mocket logo

A socket mock framework

for all kinds of socket animals, web-clients included - with gevent/asyncio/SSL support

…and then MicroPython’s urequests (mocket >= 3.9.1)

What is it about?

In a nutshell, Mocket is monkey-patching on steroids for the socket and ssl modules.

It’s designed to serve two main purposes:

  • As a low-level framework — for example, if you’re building a client for a new database or protocol.

  • As a ready-to-use mock — perfect for testing HTTP or HTTPS calls from any client library.

To demonstrate that Mocket is more than just a web client mocking tool, it even includes a simple Redis mock.

The main goal of Mocket is to make it easier to test Python clients that communicate using the socket protocol.

Outside GitHub

Mocket packages are available for openSUSE, NixOS, ALT Linux, NetBSD, AUR Arch Linux, and of course from PyPI.

Versioning

Starting from 3.7.0, Mocket major version will follow the same numbering pattern as Python’s and therefore indicate the most recent Python version that is supported.

FYI: the last version compatible with Python 2.7 is 3.9.4, bugfixing or backporting of features introduced after that release will only be available as commercial support.

Support it

Star the project on GitHub, Buy Me a Coffee clicking the button below or, even better, contribute with patches or documentation.

Buy Me A Coffee

How to use it

Read the following blog posts if you want to have a big picture of what Mocket is capable of:

The starting point to understand how to use Mocket to write a custom mock is the following example:

As next step, you are invited to have a look at the implementation of both the mocks it provides:

Please also have a look at the huge test suite:

Installation

Using pip:

$ pip install mocket

Speedups

Mocket uses xxhash when available instead of hashlib.md5 for creating hashes, you can install it as follows:

$ pip install mocket[speedups]

Issues

When opening an Issue, please add few lines of code as failing test, or -better- open its relative Pull request adding this test to our test suite.

Example of how to mock an HTTP[S] call

Let’s create a new virtualenv with all we need:

$ python3 -m venv example
$ source example/bin/activate
$ pip install pytest requests mocket

As second step, we create an example.py file as the following one:

import json

from mocket import mocketize
from mocket.mocks.mockhttp import Entry
import requests
import pytest


@pytest.fixture
def response():
    return {
        "integer": 1,
        "string": "asd",
        "boolean": False,
    }


@mocketize  # use its decorator
def test_json(response):
    url_to_mock = 'https://testme.org/json'

    Entry.single_register(
        Entry.GET,
        url_to_mock,
        body=json.dumps(response),
        headers={'content-type': 'application/json'}
    )

    mocked_response = requests.get(url_to_mock).json()

    assert response == mocked_response

# OR use its context manager
from mocket import Mocketizer

def test_json_with_context_manager(response):
    url_to_mock = 'https://testme.org/json'

    Entry.single_register(
        Entry.GET,
        url_to_mock,
        body=json.dumps(response),
        headers={'content-type': 'application/json'}
    )

    with Mocketizer():
        mocked_response = requests.get(url_to_mock).json()

    assert response == mocked_response

Let’s fire our example test:

$ py.test example.py

How to make Mocket fail when it tries to write to a real socket?

NEW!!! Sometimes you just want your tests to fail when they attempt to use the network.

with Mocketizer(strict_mode=True):
    with pytest.raises(StrictMocketException):
        requests.get("https://duckduckgo.com/")

# OR

@mocketize(strict_mode=True)
def test_get():
    with pytest.raises(StrictMocketException):
        requests.get("https://duckduckgo.com/")

You can specify exceptions as a list of hosts or host-port pairs.

with Mocketizer(strict_mode=True, strict_mode_allowed=["localhost", ("intake.ourmetrics.net", 443)]):
    ...

# OR

@mocketize(strict_mode=True, strict_mode_allowed=["localhost", ("intake.ourmetrics.net", 443)])
def test_get():
    ...

How to be sure that all the Entry instances have been served?

Add this instruction at the end of the test execution:

Mocket.assert_fail_if_entries_not_served()

Example of how to fake socket errors

It’s very important that we test non-happy paths.

@mocketize
def test_raise_exception(self):
    url = "http://github.com/fluidicon.png"
    Entry.single_register(Entry.GET, url, exception=socket.error())
    with self.assertRaises(requests.exceptions.ConnectionError):
        requests.get(url)

Example of how to mock a call with a custom request matching logic

can_handle_fun lets you define matching logic beyond the default path + querystring behavior.

The callback receives:

  • path: request path (for example /ip)

  • qs_dict: parsed query string as returned by urllib.parse.parse_qs(..., keep_blank_values=True)

import json

from mocket import mocketize
from mocket.mocks.mockhttp import Entry
import requests


@mocketize
def test_can_handle():
    url = "https://httpbin.org"

    Entry.single_register(
        Entry.GET,
        url,
        body=json.dumps({"message": "Nope... not this time!"}),
        headers={"content-type": "application/json"},
        can_handle_fun=lambda path, qs_dict: path == "/ip" and qs_dict,
    )
    Entry.single_register(
        Entry.GET,
        url,
        body=json.dumps({"message": "There you go!"}),
        headers={"content-type": "application/json"},
        can_handle_fun=lambda path, qs_dict: path == "/ip" and not qs_dict,
    )

    resp = requests.get("https://httpbin.org/ip")
    assert resp.status_code == 200
    assert resp.json() == {"message": "There you go!"}

Useful patterns

Regex path matching:

import re

Entry.single_register(
    Entry.GET,
    "https://api.example.com",
    body="ok",
    can_handle_fun=lambda path, qs_dict: bool(re.match(r"^/users/\d+$", path)),
)

Query parameter checks:

Entry.single_register(
    Entry.GET,
    "https://api.example.com",
    body="ok",
    can_handle_fun=lambda path, qs_dict: (
        path == "/search"
        and qs_dict.get("q") == ["mocket"]
        and qs_dict.get("limit", ["10"])[0].isdigit()
    ),
)

Case-insensitive path checks:

Entry.single_register(
    Entry.GET,
    "https://api.example.com",
    body="ok",
    can_handle_fun=lambda path, qs_dict: path.lower() == "/healthz",
)

Troubleshooting tips

  • parse_qs values are lists, so compare against ["value"].

  • Use qs_dict.get("key") instead of qs_dict["key"] when parameters are optional.

  • Keep callbacks side-effect free; they may run multiple times during request processing.

Example of how to record real socket traffic

You probably know what VCRpy is capable of, that’s the mocket’s way of achieving it:

@mocketize(truesocket_recording_dir=tempfile.mkdtemp())
def test_truesendall_with_recording_https():
    url = 'https://httpbin.org/ip'

    requests.get(url, headers={"Accept": "application/json"})
    resp = requests.get(url, headers={"Accept": "application/json"})
    assert resp.status_code == 200

    dump_filename = os.path.join(
        Mocket.get_truesocket_recording_dir(),
        Mocket.get_namespace() + '.json',
    )
    with io.open(dump_filename) as f:
        response = json.load(f)

    assert len(response['httpbin.org']['443'].keys()) == 1

HTTPretty compatibility layer

Mocket HTTP mock can work as HTTPretty replacement for many different use cases. Two main features are missing, or better said, are implemented differently:

  • URL entries containing regular expressions, Mocket implements can_handle_fun which is way simpler to use and more powerful;

  • response body from functions (used mostly to fake errors, Mocket accepts an exception instead).

Both features are documented above.

Two features which are against the Zen of Python, at least imho (mindflayer), but of course I am open to call it into question.

Example:

import json

import aiohttp
import asyncio
from unittest import TestCase

from mocket.plugins.httpretty import httpretty, httprettified


class AioHttpEntryTestCase(TestCase):
    @httprettified
    def test_https_session(self):
        url = 'https://httpbin.org/ip'
        httpretty.register_uri(
            httpretty.GET,
            url,
            body=json.dumps(dict(origin='127.0.0.1')),
        )

        async def main(l):
            async with aiohttp.ClientSession(
                loop=l, timeout=aiohttp.ClientTimeout(total=3)
            ) as session:
                async with session.get(url) as get_response:
                    assert get_response.status == 200
                    assert await get_response.text() == '{"origin": "127.0.0.1"}'

        loop = asyncio.new_event_loop()
        loop.set_debug(True)
        loop.run_until_complete(main(loop))

What about the other socket animals?

Using Mocket with asyncio based clients:

$ pip install aiohttp

Example:

# `aiohttp` creates SSLContext instances at import-time
# that's why Mocket would get stuck when dealing with HTTPS
# Importing the module while Mocket is in control (inside a
# decorated test function or using its context manager would
# be enough for making it work), the alternative is using a
# custom TCPConnector which always returns a FakeSSLContext
# from Mocket like this example is showing.
import aiohttp
import pytest

from mocket import async_mocketize
from mocket.mocks.mockhttp import Entry
from mocket.plugins.aiohttp_connector import MocketTCPConnector


@pytest.mark.asyncio
@async_mocketize
async def test_aiohttp():
    """
    The alternative to using the custom `connector` would be importing
    `aiohttp` when Mocket is already in control (inside the decorated test).
    """

    url = "https://bar.foo/"
    data = {"message": "Hello"}

    Entry.single_register(
        Entry.GET,
        url,
        body=json.dumps(data),
        headers={"content-type": "application/json"},
    )

    async with aiohttp.ClientSession(
        timeout=aiohttp.ClientTimeout(total=3), connector=MocketTCPConnector()
    ) as session, session.get(url) as response:
        response = await response.json()
        assert response == data

Works well with others

Using Mocket as pook engine:

$ pip install mocket[pook]

Example:

import pook
from mocket.plugins.pook_mock_engine import MocketEngine

pook.set_mock_engine(MocketEngine)

pook.on()

url = 'http://twitter.com/api/1/foobar'
status = 404
response_json = {'error': 'foo'}

mock = pook.get(
    url,
    headers={'content-type': 'application/json'},
    reply=status,
    response_json=response_json,
)
mock.persist()

requests.get(url)
assert mock.calls == 1

resp = requests.get(url)
assert resp.status_code == status
assert resp.json() == response_json
assert mock.calls == 2

First appearance

EuroPython 2013, Florence

Project details


Release history Release notifications | RSS feed

Download files

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

Source Distribution

mocket-3.14.2.tar.gz (85.1 kB view details)

Uploaded Source

Built Distribution

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

mocket-3.14.2-py3-none-any.whl (37.0 kB view details)

Uploaded Python 3

File details

Details for the file mocket-3.14.2.tar.gz.

File metadata

  • Download URL: mocket-3.14.2.tar.gz
  • Upload date:
  • Size: 85.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"25.10","id":"questing","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for mocket-3.14.2.tar.gz
Algorithm Hash digest
SHA256 9fc4906cae3907e9a28c46e773f3adabef0d5f4088c6e390ef61440219ce09a7
MD5 7866762a9bd996889dea6f0611b114ac
BLAKE2b-256 0655bebee943fd1136fd2b90cb0cf7565593cb29a82be8345a49cd7b486a3198

See more details on using hashes here.

File details

Details for the file mocket-3.14.2-py3-none-any.whl.

File metadata

  • Download URL: mocket-3.14.2-py3-none-any.whl
  • Upload date:
  • Size: 37.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"25.10","id":"questing","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for mocket-3.14.2-py3-none-any.whl
Algorithm Hash digest
SHA256 661c2882ba231f833a74ae994984173342a4391cf3caaab527ace160f6d2c0b1
MD5 80daed5f0a3cb16eb36f003efdcdfba5
BLAKE2b-256 2a4f76b80484d88abff1cbd2647f70e2bf88d0c0acaeaa118e4d939534dc7ef2

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