← Назад к вопросам
Если unit-тест вызывает сторонний ресурс, какие действия предпринять
1.8 Middle🔥 191 комментариев
#Тестирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Если unit-тест вызывает сторонний ресурс — что делать
Это критическая ошибка в тестировании. Unit-тесты должны быть изолированы, быстры и детерминированы. Вызов сторонних ресурсов нарушает все эти принципы.
Почему это проблема
# ❌ ПЛОХО: unit-тест вызывает реальный API
import requests
def test_get_user_from_api():
response = requests.get('https://api.github.com/users/guido') # Реальный запрос!
assert response.status_code == 200
assert response.json()['login'] == 'guido'
# Проблемы:
# 1. Медленно (сетевая задержка 100-500ms вместо 1ms)
# 2. Нестабильно (API может быть down, интернет отключен)
# 3. Не изолировано (зависит от внешнего сервиса)
# 4. Дорого (API quotas, rate limits)
# 5. Трудно тестировать edge cases (ошибки, timeout, rate limits)
# 6. CI/CD может упасть из-за внешних факторов
Решение: Mocking и Stubbing
Основной принцип: Тестируй логику, не интеграцию. Заменяй (mock'ируй) внешние зависимости.
Вариант 1: unittest.mock (встроенный)
from unittest.mock import patch, Mock
import requests
# ✅ ХОРОШО: mock'ируем requests.get
@patch('requests.get')
def test_get_user_from_api(mock_get):
# Настраиваем mock
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {'login': 'guido'}
# Вызываем функцию
response = requests.get('https://api.github.com/users/guido')
# Проверяем результат
assert response.status_code == 200
assert response.json()['login'] == 'guido'
# Проверяем, что mock был вызван правильно
mock_get.assert_called_once_with('https://api.github.com/users/guido')
# Плюсы: встроено, нет зависимостей
# Минусы: verbose, сложно для сложных сценариев
Вариант 2: pytest-mock (для pytest)
import pytest
from unittest.mock import Mock
import requests
@pytest.fixture
def mock_github_api(mocker):
"""Fixture для мокирования GitHub API"""
mock = mocker.patch('requests.get')
mock.return_value.status_code = 200
mock.return_value.json.return_value = {'login': 'guido'}
return mock
def test_get_user(mock_github_api):
response = requests.get('https://api.github.com/users/guido')
assert response.json()['login'] == 'guido'
# Плюсы: чище, чем unittest.mock, pytest интеграция
Вариант 3: VCR.py (запись и воспроизведение)
Для сложных сценариев: запиши реальный запрос один раз, потом воспроизводи.
import pytest
import requests
from vcr import VCR
my_vcr = VCR(
cassette_library_dir='tests/fixtures/cassettes',
record_mode='once', # Запись один раз, потом из файла
)
@pytest.mark.vcr # Или @my_vcr.use_cassette('github_api.yaml')
def test_get_user_with_vcr():
response = requests.get('https://api.github.com/users/guido')
assert response.json()['login'] == 'guido'
assert response.status_code == 200
# Первый запуск: записывает в cassettes/fixtures/cassettes/
# Последующие: воспроизводит из файла (очень быстро)
# Преимущества:
# - Первый запуск реальный (не мокируешь неправильно)
# - Потом очень быстро
# - Невозможно случайно сломать тест неправильным mock'ом
Файл cassettes/github_api.yaml:
interactions:
- request:
body: null
headers: {}
method: GET
uri: https://api.github.com/users/guido
response:
body: {string: '{"login": "guido", ...}'}
headers:
content-type: [application/json]
status: {code: 200, message: OK}
Вариант 4: Dependency Injection (правильная архитектура)
Лучший подход: Разработай код так, чтобы легко подменять зависимости.
# ❌ ПЛОХО: жёсткая зависимость на requests
class UserService:
def get_user(self, username):
response = requests.get(f'https://api.github.com/users/{username}')
return response.json()
def test_user_service():
# Сложно mock'ировать
pass
# ✅ ХОРОШО: зависимость injected
class UserService:
def __init__(self, http_client=None):
self.http_client = http_client or requests
def get_user(self, username):
response = self.http_client.get(f'https://api.github.com/users/{username}')
return response.json()
def test_user_service():
mock_client = Mock()
mock_client.get.return_value.json.return_value = {'login': 'guido'}
service = UserService(http_client=mock_client)
user = service.get_user('guido')
assert user['login'] == 'guido'
mock_client.get.assert_called_once_with('https://api.github.com/users/guido')
Вариант 5: Fake implementation (для сложных зависимостей)
# Interface/Protocol
from abc import ABC, abstractmethod
class DatabaseClient(ABC):
@abstractmethod
def query(self, sql: str):
pass
# Реальная реализация
class PostgresClient(DatabaseClient):
def query(self, sql: str):
return psycopg2.connect(...).execute(sql)
# Fake для тестов (in-memory)
class FakeDatabaseClient(DatabaseClient):
def __init__(self):
self.data = {}
def query(self, sql: str):
# In-memory simulation
if 'SELECT * FROM users' in sql:
return [{'id': 1, 'name': 'Guido'}]
return []
# Тест
def test_with_fake_db():
db = FakeDatabaseClient()
service = UserService(db=db)
users = service.get_all_users()
assert len(users) == 1
assert users[0]['name'] == 'Guido'
# Плюсы: контролируемо, быстро, не требует mock'ирования
Вариант 6: Responses library (специально для HTTP)
import responses
import requests
@responses.activate # Перехватывает все HTTP запросы
def test_with_responses():
# Регистрируем mock'ированный ответ
responses.add(
responses.GET,
'https://api.github.com/users/guido',
json={'login': 'guido'},
status=200,
)
response = requests.get('https://api.github.com/users/guido')
assert response.json()['login'] == 'guido'
# Или с матчингом:
@responses.activate
def test_with_matcher():
def request_callback(request):
if 'guido' in request.url:
return (200, {}, json.dumps({'login': 'guido'}))
return (404, {}, '')
responses.add_callback(
responses.GET,
'https://api.github.com/users/',
callback=request_callback,
)
Практическое руководство
Шаг 1: Определи тип сторонней зависимости
- HTTP API → mock requests или responses или VCR
- База данных → fixture с test database или fake
- Файловая система → tempfile, fake filesystem
- Email → mock smtp
- 3rd party SDK → mock the SDK
Шаг 2: Выбери инструмент
Просто и быстро? → unittest.mock
Внешний HTTP? → responses или VCR
Сложная логика БД? → Fake implementation
Microservice? → Contract testing (Pact)
Шаг 3: Пиши тест
import pytest
from unittest.mock import Mock, patch
@patch('myapp.external_service.api_call')
def test_something(mock_api):
mock_api.return_value = {'success': True}
result = my_function()
assert result == expected
mock_api.assert_called_once()
Типичные ошибки
# ❌ Неправильный путь для patch
@patch('external_service.api_call') # НЕПРАВИЛЬНО
def test(mock):
pass
# ✅ Правильно: patch где используется
@patch('myapp.module.api_call') # Где api_call вызывается
def test(mock):
pass
# ❌ Mock слишком сложный
mock = Mock()
mock.response.data[0].user.profile.settings = {}
# ✅ Mock должен быть простым и понятным
mock = Mock(return_value={'user_id': 123})
Чеклист для unit-тестов
- ✅ Тест работает за < 1 мс (без network/IO)
- ✅ Тест повторяемый (один и тот же результат)
- ✅ Тест не зависит от интернета
- ✅ Тест не требует credentials/API ключей
- ✅ Тест не вызывает реальные сервисы
- ✅ Тест не пишет на диск (или использует tempfile)
- ✅ Тест не меняет системное состояние
Интеграционные тесты (это другое!)
# ИНТЕГРАЦИОННЫЕ тесты (не unit!) могут вызывать реальные сервисы
@pytest.mark.integration
def test_integration_with_real_api():
# Это OK для интеграционных тестов
response = requests.get('https://api.github.com/users/guido')
assert response.status_code == 200
# Но их:
# 1. Запускают отдельно (не в CI/CD каждый раз)
# 2. Помечают @pytest.mark.integration
# 3. Требуют активного интернета
# 4. Медлительнее unit-тестов
Вывод
Unit-тесты НЕ должны вызывать сторонние ресурсы. Если тест вызывает:
- HTTP API → Mock'ируй с unittest.mock, responses или VCR
- БД → Используй test database или fake
- Файлы → Используй tempfile или fake filesystem
- Сторонние SDK → Mock'ируй SDK
Это даёт:
- ⚡ Быстрые тесты (ms вместо ms)
- 🎯 Надёжные тесты (не зависят от внешних факторов)
- 🔄 Повторяемые тесты (одинаковый результат каждый раз)
- 🐛 Легко найти баги (изолированная логика)
Профессиональный код = изолированные unit-тесты + отдельные интеграционные тесты.