Skip to main content

mockable

Библиотека подставляет заранее подготовленные данные, чтобы вы могли проверить работу приложения без обращения к серверу. Например, можно проверить ситуацию «свободных мест нет», даже когда настоящий сервер возвращает свободные места.

Для интеграции в start.py нужны два декоратора:

  • @mockable_class(config_factory=build_mock_config) над классом.
  • @http_mockable над общим методом _http_request.

При self.cfg.is_mock=True моки включаются автоматически на время вызова метода. Внешние файлы вызывают методы как обычно. Не нужно добавлять with, изменять handler или вручную вызывать set_mock_config.

1. Установите библиотеку

Для опубликованной версии установка выглядит так:

pip install mockable

Текущее расширение находится в исходниках этого проекта и пока не опубликовано отдельным релизом. До публикации устанавливайте эту локальную версию из папки с pyproject.toml, иначе из PyPI может установиться версия без нового API:

python -m pip install -e .

Нужен Python 3.10 или новее. Зависимость requests устанавливается вместе с библиотекой. -e позволяет использовать изменения исходников без переустановки.

2. Добавьте настройки и функцию конфигурации в start.py

Полный учебный файл находится в examples/start_like.py. В нём всё в одном месте: настройки, моки, класс и обычный handler. Ниже показан вариант с настоящей отправкой requests при выключенных моках. Адрес example.test вымышленный; замените его адресом своего API.

Добавьте в начало своего start.py:

from dataclasses import dataclass
import requests

from mockable import (
    MockConfig, MethodMockConfig, MockFixture, TestCaseType,
    HttpKey, HttpMockConfig, HttpFixture, mockable_class, http_mockable,
)


# Шаг 1. Если MedicineConfig уже существует, используйте его.
@dataclass
class MedicineConfig:
    is_mock: bool = False
    base_url: str = "https://example.test"


# Шаг 2. Функция создаёт конфиг. Сами словари с ответами находятся внутри неё.
# api — объект MedicineApi, для которого библиотека включает моки.
def build_mock_config(api):
    queue = HttpKey(
        url=api.cfg.base_url + "/queues/get/info",
        name="queue_info",
    )
    return MockConfig(
        # Здесь выбирается кейс для подмены функции целиком.
        active_cases={"get_patient": TestCaseType.POSITIVE},
        methods={
            "get_patient": MethodMockConfig(
                positive=MockFixture(result={"name": "Тестовый пациент"}),
                negative=MockFixture(result={"name": "Пациент не найден"}),
            ),
        },
        # Здесь выбирается кейс для ответа на HTTP-запрос.
        http_active_cases={queue: TestCaseType.POSITIVE},
        http_requests={
            queue: HttpMockConfig(
                positive=HttpFixture(result={"items": [{"id": 101}]}),
                negative=HttpFixture(result={"items": []}),
            ),
        },
        http_unmatched="raise",
    )

build_mock_config должна быть объявлена выше класса. Передайте декоратору саму функцию, без скобок: config_factory=build_mock_config. Библиотека вызовет её сама при первом вызове метода объекта с is_mock=True.

При is_mock=False эта функция не вызывается: словари ответов и фикстуры не создаются, файлы моков не читаются. Импорты и код самой функции, как любой Python-код, остаются в памяти.

3. Повесьте два декоратора

Продолжение того же start.py:

# Шаг 3. Один декоратор охватывает обычные методы всего класса.
@mockable_class(config_factory=build_mock_config)
class MedicineApi:
    def __init__(self, *, config=None):
        self.cfg = config or MedicineConfig()

    # Здесь отдельный @mockable не нужен.
    def get_patient(self, *, session):
        return {"name": "Пациент из обычного кода"}

    # Шаг 4. Второй декоратор ставится только над общей отправкой запросов.
    @http_mockable
    def _http_request(self, *, session, method, url, name):
        response = requests.request(method=method, url=url, timeout=5)
        response.raise_for_status()
        return response.json()

    # Этот метод продолжит работать: мок подменяет только HTTP-ответ.
    def get_queue(self, *, session):
        data = self._http_request(
            session=session,
            method="GET",
            url=self.cfg.base_url + "/queues/get/info",
            name="queue_info",
        )
        return len(data["items"])

В вашем существующем _http_request сохраните тело метода и добавьте только декоратор. Если он использует apiRequest, а та вызывает requests, библиотека перехватит запрос ниже неё. Менять app.tools или добавлять туда импорт mockable не нужно. Метод должен принимать name; данные session или sessiondata также будут доступны HTTP-фикстуре.

В бизнес-методах исходного start.py есть ранние возвраты вида if self.cfg.is_mock: return .... При переносе соответствующих ответов в конфиг уберите эти возвраты. Иначе выполнение закончится до HTTP-запроса.

4. Вызывайте методы из любого файла

from start import MedicineApi, MedicineConfig

api = MedicineApi(config=MedicineConfig(is_mock=True))

def main():
    patient = api.get_patient(session={})
    places = api.get_queue(session={})
    print(patient)  # {'name': 'Тестовый пациент'}
    print(places)   # 1

if __name__ == "__main__":
    main()

В get_patient библиотека сразу вернёт значение из methods, не выполняя тело метода. В get_queue выполнится весь код: requests получит заданный JSON, а len(data["items"]) вычислит число мест. Повторные обращения получают тот же ответ выбранного кейса.

Для обычного запуска передайте MedicineConfig(is_mock=False). Если нужно сохранить именно api = MedicineApi(), выбирайте is_mock в настройках, которые использует конструктор по умолчанию.

Что и где менять

Задача Место изменения
Включить или выключить моки MedicineConfig.is_mock
Заменить результат метода methods["имя_метода"]
Задать ответ сервера http_requests[HttpKey(url, name)]
Выбрать кейс метода active_cases["имя_метода"]
Выбрать кейс запроса http_active_cases[HttpKey(url, name)]

Если метод присутствует в methods, он заменяется целиком. Его внутренние запросы не выполняются. Для проверки его бизнес-логики уберите запись метода из methods и настройте нужные HTTP-ответы в http_requests.

URL, имя и несколько запросов

HttpKey(url="https://example.test/info", name="queue_info") описывает пару: полный адрес и имя запроса. Пишите точно тот name, который передаётся в _http_request. Имена чувствительны к регистру.

Для нескольких запросов добавьте по записи на каждую пару URL и имени. Запросы выбирают свои ответы независимо; порядок записей в словаре не важен. Один URL с разными именами может иметь разные моки.

Путь /info отличается от /info/. Параметры после ? и fragment после # не входят в ключ. Например, ?id=1 и ?id=2 при одинаковом имени используют одно правило; если ответ зависит от параметра, используйте side_effect.

Можно ограничить метод: HttpMockConfig(method="GET", positive=...). Чтобы отдельно настроить GET и POST одного адреса, задайте им разные имена. Пустой name="" соответствует запросам без имени, а не любому имени.

Поддерживаются стандартные requests.request, requests.get/post/..., requests.Session().request и отправка подготовленного запроса через Session.send. Для Session внутри _http_request действует тот же декоратор.

Кейсы

Кейс Что проверять
TestCaseType.POSITIVE Штатное успешное выполнение
TestCaseType.NEGATIVE Ожидаемый отрицательный результат, например нет мест
TestCaseType.SPECIAL Особый или граничный случай
TestCaseType.ERROR Ошибка сервера или соединения

В конфиге из примера поменяйте http_active_cases={queue: TestCaseType.NEGATIVE}, чтобы get_queue вернул 0. Для другого ответа метода поменяйте active_cases={"get_patient": TestCaseType.NEGATIVE}.

positive обязателен; остальные поля negative, special, error необязательны. Без явного выбора используется POSITIVE. Если выбранный кейс не описан, используется positive, как в прежнем API. Название ERROR само по себе не вызывает исключение: результат определяется его фикстурой.

Конфиг создаётся один раз на объект и хранится, пока жив объект. После изменения фабрики или настроек кейсов создайте новый объект для новой проверки. Если переключить существующий объект на is_mock=False, следующий вызов метода освободит его сохранённый конфиг. При последующем включении он создастся заново. Не изменяйте настройки объекта посреди выполняющегося метода.

Фикстуры: данные, файлы и ошибки

MockFixture для функций и HttpFixture для HTTP поддерживают:

Поле Назначение
result Готовое значение
file_path Путь к JSON-файлу, читаемому при обращении
side_effect Функция, вычисляющая ответ
exception Исключение, которое нужно выбросить
description Пояснение для человека

Обычно задавайте один источник. Если источников несколько, приоритет: exception, side_effect, file_path, result.

Большой ответ можно хранить в файле:

from mockable import HttpFixture

answer = HttpFixture(file_path="mocks/queue.json")

Файл должен содержать JSON, например {"items": []}. Относительный путь отсчитывается от папки запуска программы. Абсолютный путь не зависит от неё.

У HTTP-фикстуры есть status_code (по умолчанию 200) и headers:

from requests.exceptions import Timeout
from mockable import HttpFixture

server_error = HttpFixture(status_code=503, result={"message": "Unavailable"})
connection_error = HttpFixture(exception=Timeout("Сервер не ответил"))
answer = HttpFixture(result={"items": []}, headers={"X-Test": "yes"})

HTTP-статус 503 сам по себе не выбрасывает исключение; его может выбросить response.raise_for_status(). Исключение Timeout из фикстуры выбрасывается при запросе. Ваша apiRequest может перехватить его своим except и вернуть (500, exception) — дальше работает обычная логика приложения.

Словари, списки, числа, bool и None сериализуются в JSON. Строки кодируются в UTF-8, bytes передаются как есть. None означает JSON null, b"" — пустое тело. Content-Type для JSON добавляется, если его не задали вручную.

Ответ зависит от параметров

В HttpFixture.side_effect передаётся один объект с данными запроса:

from mockable import HttpFixture

def build_reply(request):
    return {"ids": request.query.get("id", []), "items": []}

answer = HttpFixture(side_effect=build_reply)

Доступны method, url, name, headers, body, query, sessiondata. query хранит списки значений URL-параметров. body — подготовленное requests тело: байты, строка или None. Функция возвращает тело ответа; статус и заголовки берутся из HttpFixture.

В обычной MockFixture.side_effect передаются аргументы заменяемой функции, включая self для метода. Этот старый контракт сохранён.

Если мок не сработал

Проверьте self.cfg.is_mock, оба декоратора и config_factory. Проверьте, что фабрика возвращает MockConfig, а URL и имя совпадают с реальным запросом. Убедитесь, что бизнес-метод не завершился раньше из-за собственного if self.cfg.is_mock: return ... и не заменён целиком через methods.

По умолчанию http_unmatched="raise": незаданный HTTP-запрос вызывает MockRequestNotMatched без отправки в сеть. Для намеренно смешанного режима можно выбрать "real", разрешив незаданным запросам настоящий сервер.

Если приложение проглотило ошибку ненайденного мока, библиотека сообщит о ней при завершении внешнего вызова метода. Исключение приложения, уже выходящее из метода, не заменяется другим исключением.

Совместимость и границы

Существующие @mockable, @mockable_class без параметров и set_mock_config сохраняют старое поведение. Для отдельной функции:

from mockable import mockable, set_mock_config, MockConfig, MethodMockConfig, MockFixture

@mockable
def greeting():
    return "Обычный ответ"

set_mock_config(MockConfig(methods={
    "greeting": MethodMockConfig(positive=MockFixture(result="Тестовый ответ")),
}))
try:
    print(greeting())
finally:
    set_mock_config(None)

Автоматический класс с config_factory использует конфиг своего экземпляра, а не глобальный set_mock_config. Вложенные вызовы того же объекта используют его активную конфигурацию; разные объекты изолированы. Обычные унаследованные методы тоже покрываются. Конструктор, специальные методы и property автоматически не подменяются. Объект должен позволять хранение служебного атрибута конфигурации.

Для staticmethod/classmethod нет экземпляра с cfg: прямой вызов этих методов не включает автоматические моки. Если они вызываются внутри обычного метода объекта, они используют уже активный конфиг. Старый глобальный режим для них сохранён.

Перехват рассчитан на синхронный requests. Другие HTTP-клиенты, собственные переопределения Session.send и прямые вызовы адаптеров не покрываются. Моки не воспроизводят автоматически цепочки редиректов. Запросы логгера через requests также подпадают под активные правила.

В рабочем потоке вызывайте обычный декорированный метод объекта: он сам активирует конфиг. Прямой requests в новом потоке не наследует контекст вызывающего потока. Фабрика выполняется один раз; её результат и side_effect не должны содержать небезопасное совместно изменяемое состояние при параллельных вызовах.

Для отдельных низкоуровневых тестов оставлены http_mocks(config) и http_request_name(name), но при интеграции класса они не нужны. Предыдущее написание mockable_http работает как синоним http_mockable.

Примеры и тесты из исходников

Первый уровень — короткие примеры:

Второй уровень — один файл по образцу start.py. В нём при выключенных моках есть учебная заглушка вместо настоящего сервера. При переносе используйте свою реализацию _http_request.

После установки пакета из корня проекта:

python -m examples.simple.function_result
python -m examples.simple.http_response
python -m examples.start_like
python -m unittest discover -s tests

Можно запускать без установки локального пакета, используя подготовленное окружение:

$env:PYTHONPATH = "$PWD/src"
.\.venv\Scripts\python -m examples.start_like
.\.venv\Scripts\python -m unittest discover -s tests

Metadata

Release files for mockable 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mockable 0.1.0
File Size Uploaded Provenance
mockable-0.1.0.tar.gz 27.0 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for mockable 0.1.0
File Interpreter ABI Platform
mockable-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 44.0 kB

Release files / mockable-0.1.0.tar.gz

Download URL mockable-0.1.0.tar.gz
Size 27.0 kB
Tags Source
SHA-256 checksum
How to use checksums
4b782f743b95383e50dedcae84bc6055761ef6986be2868bb967ecf1f1224641
BLAKE2b-256 checksum
How to use checksums
95683c8219f806058df9a139c85c0d94ce818abaf579f8ac50dd43c9e5197a8b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.11

Release files / mockable-0.1.0-py3-none-any.whl

Download URL mockable-0.1.0-py3-none-any.whl
Size 16.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9a6f858befb194a97f8274b7e3ec140f6338925e51028f33aedfb60574f36be8
BLAKE2b-256 checksum
How to use checksums
1761d244ba7cae7fe09592b19f06980b9515bc567f3a9d82764c3b303c02e31d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.11

Security for mockable 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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