Іноді ми розробляємо додаток, який взаємодіє із зовнішніми 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")
Що тут відбувається:
- Ми використовуємо
@patchдля підміни викликуrequests.getвсередині нашої функції. mock_get.return_valueдозволяє нам кастомізувати поведінку поверненого об'єкта.- Ми перевіряємо, що наша функція коректно викликала
requests.getіз правильним URL. - Також ми перевіряємо, що результат роботи функції відповідає очікуванням.
Обробка виключень
Ми можемо протестувати сценарій, коли 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, переконайся, що дані відправляються коректно, і перевір обробку помилок.
Корисні посилання
- Документація по
unittest.mock - pytest-mock — зручне розширення для Mock-тестування з використанням pytest.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ