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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| mockable-0.1.0.tar.gz | 27.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.