JavaRush /Курси /Модуль 3: Django /Mock-тестування зовнішніх API

Mock-тестування зовнішніх API

Модуль 3: Django
Рівень 22 , Лекція 7
Відкрита

Іноді ми розробляємо додаток, який взаємодіє із зовнішніми API (наприклад, API для платіжних систем, соціальних мереж або курсів валют). Писати тести для таких взаємодій стає трохи складно, тому що:

  • Зовнішній сервіс може бути недоступний, коли ви тестуєте.
  • Результати зовнішнього API можуть бути непередбачуваними (наприклад, залежать від часу доби, даних користувача або конверсії валют).
  • Часті виклики зовнішнього API можуть бути дорогими або уповільнювати тестування.

Mock-тестування вирішує цю проблему. Воно дозволяє "підмінити" зовнішній API на фейкові дані під час виконання тестів. Ми можемо емулювати (симулювати) поведінку зовнішнього сервісу, повертаючи передбачувані відповіді.

Трохи термінології

Mock (читається як "мок") — це об'єкт, який підміняє інший об'єкт. Він "прикидається" реальним об'єктом або функцією та імітує їхню поведінку. У Python для Mock-тестування використовується бібліотека unittest.mock, яка є частиною стандартної бібліотеки.

Основні принципи Mock-тестування

Mock-тестування базується на таких концепціях:

  • Підміна об'єктів. Під час тестування замість реального об'єкта (наприклад, функції або класу, який робить HTTP-запити) використовується Mock-об'єкт. Цей Mock-об'єкт повертає передбачувані відповіді.

  • Ізоляція. Функція, що тестується, ізолюється від будь-яких взаємодій із зовнішнім світом. Усі дії, які потребують позапрограмної взаємодії, замінюються Mock-об'єктами.

  • Перевірка викликів. За допомогою Mock-об'єктів можна перевіряти, які методи були викликані, з якими аргументами та в якому порядку.

Використання unittest.mock

Встановлення бібліотеки

Спойлер — нічого встановлювати не потрібно! Бібліотека unittest.mock вже входить до стандартного комплекту Python. Можете використовувати її на здоров'я.

Основні інструменти unittest.mock Ось короткий список того, що нам знадобиться для тестування:

Інструмент Опис
mock.Mock Створює об'єкт-заглушку для заміни реального об'єкта
mock.patch Контекстний менеджер або декоратор для заміни об'єктів
mock.return_value Визначає значення, яке Mock-об'єкт буде повертати, якщо його викликати
mock.side_effect Дозволяє Mock-об'єкту виконувати певні дії при виклику (наприклад, викидати виключення)
mock.call_count Показує кількість викликів Mock-об'єкта
mock.assert_called_with() Перевіряє, що Mock-об'єкт був викликаний із заданими аргументами

Приклад: тестування функції, яка взаємодіє із зовнішнім API

Припустимо, ми пишемо додаток, який отримує поточний курс валют. Ось наша функція:

import requests

def get_exchange_rate(base_currency, target_currency):
    url = f"https://api.example.com/rates?base={base_currency}⌖={target_currency}"
    response = requests.get(url)
    if response.status_code == 200:
        return response.json().get("rate")
    else:
        raise Exception("Помилка під час отримання курсу валют")

Без Mock-тестування кожного разу при запуску тесту ми будемо відправляти реальний запит до API. Це дорого, нестабільно і уповільнює тестування. Тому ми використовуємо Mock.

Тестування з використанням mock.patch

Спочатку підміняємо метод requests.get, щоб він повертав заздалегідь задані дані:

from unittest.mock import patch
import pytest

@patch("requests.get")
def test_get_exchange_rate(mock_get):
    # Налаштовуємо mock-об'єкт
    mock_response = mock_get.return_value  # Це імітує повернений об'єкт response
    mock_response.status_code = 200
    mock_response.json.return_value = {"rate": 75.0}

    # Викликаємо функцію
    rate = get_exchange_rate("USD", "RUB")

    # Проводимо перевірки
    assert rate == 75.0
    mock_get.assert_called_once_with("https://api.example.com/rates?base=USD⌖=RUB")

Що тут відбувається:

  1. Ми використовуємо @patch для підміни виклику requests.get всередині нашої функції.
  2. mock_get.return_value дозволяє нам кастомізувати поведінку поверненого об'єкта.
  3. Ми перевіряємо, що наша функція коректно викликала requests.get із правильним URL.
  4. Також ми перевіряємо, що результат роботи функції відповідає очікуванням.

Обробка виключень

Ми можемо протестувати сценарій, коли API повертає помилковий HTTP-статус:

@patch("requests.get")
def test_get_exchange_rate_error(mock_get):
    # Налаштовуємо mock-об'єкт
    mock_response = mock_get.return_value
    mock_response.status_code = 500

    # Викликаємо функцію і очікуємо виключення
    with pytest.raises(Exception, match="Помилка під час отримання курсу валют"):
        get_exchange_rate("USD", "RUB")

Приклад: Підміна цілого класу

Припустимо, у нас є клас CurrencyConverter, який звертається до API і перетворює суму:

class CurrencyConverter:
    def __init__(self, base_currency):
        self.base_currency = base_currency

    def convert(self, target_currency, amount):
        rate = get_exchange_rate(self.base_currency, target_currency)
        return rate * amount

Щоб протестувати цей клас ізольовано, ми можемо підмінити функцію get_exchange_rate:

@patch("__main__.get_exchange_rate")
def test_currency_converter(mock_get_exchange_rate):
    # Налаштовуємо mock-об'єкт
    mock_get_exchange_rate.return_value = 75.0

    # Створюємо об'єкт і проводимо тест
    converter = CurrencyConverter("USD")
    result = converter.convert("RUB", 100)

    # Проводимо перевірки
    assert result == 7500.0
    mock_get_exchange_rate.assert_called_once_with("USD", "RUB")

Використання mock.side_effect

Іноді замість повернення фіксованого значення потрібно зімітувати різні сценарії. Це можна зробити за допомогою side_effect. Наприклад:

@patch("requests.get")
def test_get_exchange_rate_with_side_effect(mock_get):
    # Налаштовуємо side_effect для різних викликів
    def mock_json():
        return {"rate": 75.0}

    mock_response = mock_get.return_value
    mock_response.status_code = 200
    mock_response.json.side_effect = mock_json

    # Тестуємо
    rate = get_exchange_rate("USD", "RUB")

    # Перевіряємо
    assert rate == 75.0

Або у випадку викидання виключення:

@patch("requests.get")
def test_get_exchange_rate_with_exception(mock_get):
    mock_get.side_effect = Exception("API unavailable")

    with pytest.raises(Exception, match="API unavailable"):
        get_exchange_rate("USD", "RUB")

Практичне завдання

Тепер твоя черга. Напиши тест із використанням Mock-тестування для функції, яка відправляє дані про замовлення на сервер доставки. Приблизний функціонал:

def send_order_to_shipping(order_id, address):
    url = f"https://api.example.com/shipping"
    data = {"order_id": order_id, "address": address}
    response = requests.post(url, json=data)
    if response.status_code == 201:
        return response.json().get("tracking_id")
    else:
        raise Exception("Помилка API доставки")

Спробуй замінити requests.post, переконайся, що дані відправляються коректно, і перевір обробку помилок.

Корисні посилання

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ