Metadata-Version: 2.4
Name: mockable
Version: 0.0.2
Summary: mockable for ses
Author-email: Arimaqqe <beebo120103@gmail.com>
License: MIT
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# mockable

Библиотека для управления моками через конфигурацию без изменения бизнес-логики приложения.

## Содержание

* [Установка и настройка](#установка-и-настройка)

  * [Установка](#установка)
  * [Пример использования](#пример-использования)
* [Поддерживаемые сценарии](#поддерживаемые-сценарии)
* [Как пользоваться](#как-пользоваться)

  * [`MockConfig`](#mockconfig)
  * [`MethodMockConfig`](#methodmockconfig)
  * [`MockFixture`](#mockfixture)

    * [Рекомендации по `MockFixture`](#рекомендации-по-mockfixture)

---

## Установка и настройка

### Установка

```shell
pip install mockable
```

### Пример использования

```py
from dataclasses import dataclass
from typing import Any, Dict

# 🔴 0. pip install mockable
# 🔴 1. Импортируем
from mockable import (
    MethodMockConfig,
    MockConfig,
    MockFixture,
    TestCaseType,
    mockable,
    mockable_class,
    set_mock_config,
)

# 🔴 2. Создаем переменную конфига
start_config = None


# 🔴 3. Пишем функцию инициализации конфига
# Это нужно, чтобы не инициализировать конфиг
#   при продовых запусках и не засорять память
def init_custom_config():
    global start_config
    # Конфиг моков
    start_config = MockConfig(
        # Какие кейсы включить, при тестировании
        # ⚠️ Если в здесь будет неправильно прописано имя метода, то мок не сработает
        active_cases={
            "api_method_a": TestCaseType.NEGATIVE,
            "api_method_b": TestCaseType.NEGATIVE,
            "outside_method_a": TestCaseType.POSITIVE,
        },
        # Описание кейсов
        # Структура ->
        #   <название метода>: MethodMockConfig(
        #       <характер случая>: MockFixture(
        #           result|side_effect|exception|file_path,
        #           description (опционально)
        #       )
        #   )
        methods={
            "api_method_a": MethodMockConfig(
                positive=MockFixture(
                    result="MOCK POSITIVE api_method_a",
                    description="описание позитивного кейса (штатная ситуация)",
                ),
                negative=MockFixture(
                    result="MOCK NEGATIVE api_method_a",
                    description="описание негативного кейса (например не хватило слотов)",
                ),
                special=MockFixture(
                    result="MOCK SPECIAL api_method_a",
                    description="особый случай (всего один слот, особенный врач)",
                ),
                error=MockFixture(exception=ValueError("User not found")),
            ),
            "api_method_b": MethodMockConfig(
                positive=MockFixture(
                    file_path="api_method_b_positive.json",
                    description="Путь к .json файлу с моком",
                ),
                negative=MockFixture(result="MOCK NEGATIVE api_method_b"),
            ),
            # ⚠️ Пример работы с функциональными моками
            "outside_method_a": MethodMockConfig(
                positive=MockFixture(
                    side_effect=lambda a: f"MOCK POSITIVE OUTSIDE A {a}",
                    description="Особая функциональность в моках",
                ),
                special=MockFixture(
                    side_effect=lambda a: f"MOCK SPECIAL OUTSIDE A {a}",
                ),
            ),
        },
    )

    # 🔴 3.1 Устанавливаем конфиг
    set_mock_config(start_config)


@dataclass
class Config:
    is_mock: bool = True


@mockable
def outside_method_a(a) -> str:
    return f"REAL {a}"


# 🔴 4. Добавляем @mockable_class
@mockable_class
class MedicineAPI:
    def __init__(self):
        self.cfg = Config()

        # 🔴 5. Пишем условие для is_mock
        if self.cfg.is_mock:
            init_custom_config()

    def api_method_a(self) -> str:
        # C включенным моком код ниже не запустится
        return "REAL"

    def api_method_b(self) -> Dict[str, Any]:
        # C включенным моком код ниже не запустится
        return {"result": "REAL"}


def main():
    api = MedicineAPI()
    print("Результат api_method_a ->", repr(api.api_method_a()))
    print("Результат api_method_b ->", repr(api.api_method_b()))
    print("Результат outside_method_a ->", repr(outside_method_a("some data")))


main()
```

---

## Поддерживаемые сценарии

| Сценарий   | Назначение                           |
| ---------- | ------------------------------------ |
| `POSITIVE` | Штатное успешное выполнение          |
| `NEGATIVE` | Ожидаемый негативный бизнес-сценарий |
| `SPECIAL`  | Особый или граничный случай          |
| `ERROR`    | Генерация исключения                 |

---

## Как пользоваться

### `MockConfig`

Основной объект конфигурации библиотеки.

Содержит список активных сценариев и описание доступных моков.

#### Поля

| Поле           | Тип                           | Описание                             |
| -------------- | ----------------------------- | ------------------------------------ |
| `active_cases` | `dict[str, TestCaseType]`     | Активный сценарий для каждого метода |
| `methods`      | `dict[str, MethodMockConfig]` | Описание доступных моков             |

#### Пример

```py
start_config = MockConfig(
    active_cases={
        "method_a": TestCaseType.POSITIVE,
        "method_b": TestCaseType.NEGATIVE,
        "method_c": TestCaseType.SPECIAL,
        "method_d": TestCaseType.ERROR,
    },
    methods={
        "method_a": MethodMockConfig(...),
        "method_b": MethodMockConfig(...),
        "method_c": MethodMockConfig(...),
        "method_d": MethodMockConfig(...),
    }
)
```

> ⚠️ Имя метода в `active_cases` должно точно совпадать с именем метода в `methods`, иначе мок не будет применён.

---

### `MethodMockConfig`

Описывает набор сценариев для одного метода.

Поле `positive` является обязательным. Остальные сценарии опциональны.

#### Поля

| Поле       | Обязательное | Описание                    |
| ---------- | ------------ | --------------------------- |
| `positive` | Да           | Штатное выполнение          |
| `negative` | Нет          | Негативный сценарий         |
| `special`  | Нет          | Особый или граничный случай |
| `error`    | Нет          | Исключение                  |

#### Пример

```py
MethodMockConfig(
    positive=MockFixture(...), # Обязательное!
    negative=MockFixture(...), # Опционально
    special=MockFixture(...),  # Опционально
    error=MockFixture(...),    # Опционально
)
```

---

### `MockFixture`

Описывает конкретный мок.

Для результата использовать следующие параметры:

| Поле          | Описание                                   | Приоритет |
| ------------- | ------------------------------------------ | --------- |
| `result`      | Возвращает заранее подготовленное значение |         4 |
| `file_path`   | Загружает результат из JSON-файла          |         3 |
| `side_effect` | Выполняет пользовательскую функцию         |         2 |
| `exception`   | Вызывает исключение                        |         1 |

Дополнительно можно указать:

| Поле          | Описание                     |
| ------------- | ---------------------------- |
| `description` | Описание назначения сценария |

#### Пример

```py
MethodMockConfig(
    positive=MockFixture(
        result={"status": "success"},
        description="Штатное выполнение",
    ),
    negative=MockFixture(
        result={"status": "error"},
        description="Недостаточно данных",
    ),
    special=MockFixture(
        result={"status": "special"},
        description="Особый сценарий",
    ),
    error=MockFixture(
        exception=TimeoutError("Service unavailable"),
        description="Сервис недоступен",
    ),
)
```

#### Пример использования `side_effect`

Через `lambda`:

```py
MockFixture(
    side_effect=lambda value: f"Result = {value}"
)
```

Через отдельную функцию:

```py
def mock_user_response(user_id: int) -> dict:
    return {
        "user_id": user_id,
        "name": "Mock User",
        "status": "active",
    }


MockFixture(
    side_effect=mock_user_response
)
```


#### Пример использования `file_path`

```py
MockFixture(
    file_path="mocks/method_a/positive.json"
)
```

---

#### Рекомендации по `MockFixture`

1. Заполняйте поле `description`.
2. Для сценария `ERROR` используйте `exception`.
3. Крупные ответы рекомендуется хранить в JSON-файлах.
4. Использовать абсолютные пути к JSON-файлам.
5. Использовать единый стиль именования файлов.

Примеры:

```text
/opt/mocks/method_a_positive.json
/opt/mocks/method_a_negative.json
```

или

```text
/opt/mocks/method_a/positive.json
/opt/mocks/method_a/negative.json
```
