Тестирование Python-приложений

Тестирование Python-приложений в масштабе

🔥 Важное для QA-специалистов! 🔥
В QaRocks ты найдешь туториалы, задачи и полезные книги, которых нет в открытом доступе. Уже более 16.000 подписчиков – будь среди нас! Заходи к нам в телеграм канал QaRocks

Перевод статьи «Testing Python Applications at Scale».

Тестирование Python-приложений в масштабе отличается от тестирования небольших проектов. И если набор тестов выполняется за 15 мин, есть коммуницирующие между собой микросервисы, либо вас каждый день огорчают нестабильные тесты, то вам стоит пересмотреть свой подход к тестированию.

Уже более 10 лет я создаю Python-системы для компаний, которые обрабатывают по миллионам транзакций. И за это время я понял, что хорошее тестирование не ограничивается только процентом покрытия. Важно укреплять доверие к тестам, не замедляя всего остального.

Самые болезненные проблемы

Медленные наборы тестов убивают культуру. Как только на CI уходит более 10-15 минут, разработчики перестают локально запускать тесты. Их объединяют в желтые сборки. Разработчики не пишут тесты, из-за слишком длинного цикла обратной связи. Я встречал команды, в которых тестовые наборы выполнялись за 30 минут, а результатам выполнения никто не верил.

А что хуже всего? Вовсе не количество тестов, а дорогостоящая настройка и очистка. Запуск реальных баз данных для каждого теста. Выполнение настоящих HTTP-вызовов к внешним сервисам. Многократное чтение больших файлов с диска.

Нестабильные тесты разрушают доверие. Интеграционные тесты, которые хаотично сбоят из-за состояния гонки или таймаутов. Сквозные тесты, падающие из-за проблем с сетью. В итоге вы заново прогоняете CI-пайплайны лишь для того, чтобы увидеть зеленую галочку. Но со временем таким результатам перестают доверять.

Я работал в командах, где стандартным ответом на упавшие тесты была фраза: «просто перезапусти». Это говорит о том, что стратегия тестирования не работает.

Неустойчивые тесты тормозят прогресс. Если тест зависит от приватных методов, или все внутренние функции заменены на моки, то он будет ломаться при каждом рефакторинге. Эти тесты не отлавливают ошибки и тормозят работу. И большую часть времени вы исправляете именно тесты, а не код.

Трата времени на расхождения в среде. Тесты успешно выполняются на компьютере, но падают в CI. Или же выполняются в CI, но сбоят в промежуточной среде. И вы часами ищете расхождения: разные версии Python, конфликты зависимостей или отсутствующие переменные.

Что действительно работает

Хорошенько изучите pytest. Этот фреймворк заслуживает внимания, хотя бы потому, что в нем есть система фикстур. Вам не нужно сражаться с методами setUp и tearDown – просто определите фикстуры для обработки сложных состояний (подключение к базе данных, аутентификация или тестовые данные). Фикстуры можно повторно использовать, комбинировать, да и очищаются они автоматически.

@pytest.fixture
def authenticated_client(db_session):
    user = User(email="test@example.com")
    db_session.add(user)
    db_session.commit()
    return APIClient(user=user)

Распараллельте все через pytest-xdis. Этот плагин однозначно необходим при масштабировании. Он запускает тесты параллельно и может вдвое сократить время CI. Просто добавьте -n auto и задействуйте все доступные ядра.

Загвоздка в том, что тесты должны быть хорошо изолированы. Если в тестах используются общие состояния, либо они зависят от порядка выполнения, то параллелизация быстро выявит такие проблемы. На самом деле, это хорошо, ведь подобные «нюансы» все равно аукнутся в будущем.

Следуйте пирамиде тестирования. Внизу пирамиды – много быстрых модульных тестов. Посередине – чуть меньше интеграционных тестов, а вверху – несколько сквозных тестов. С такой пирамидой вы получите хорошее покрытие и избавитесь от нестабильности при запуске через UI.

Здесь важнее не точное соотношение, а сам принцип: большая часть нашей уверенности должна исходить именно из быстрых тестов.

Добавляйте моки по границам, а не внутри. Пользуйтесь моками для внешних систем: API-клиенты, очереди сообщений или сторонние сервисы. Никаких моков для своих вспомогательных функций или внутренних модулей.

def test_payment_processing(mock_stripe_client):
    # Использует мок для внешнего API Stripe
    mock_stripe_client.charge.return_value = {"status": "success"}
    
    # Тестирует фактическую логику
    result = process_payment(amount=100, customer_id="123")
    assert result.success

Таким образом, тесты проверяют только логику и устойчивы к рефакторингу.

Используйте контрактные тесты для микросервисов. Полное сквозное тестирование – это медленно и нестабильно в том случае, если у вас есть несколько сервисов, которые общаются между собой. Контрактные тесты проверяют, что API каждого сервиса именно такое, какое ожидают увидеть остальные. Причем по факту эти тесты не запускают всю связку вместе.

Для этих целей мы использовали Pact. Он выявлял критические изменения на стадии разработки, а не в продакшене.

Дублируйте структуру кода в тестах. Если код приложения находится в src/app/feature, то положите тесты в tests/feature. Это же очевидно! Но я встречал тестовые директории, которые создавались по типам тестов, или все тесты вообще сгружали в общую папку. Если вы будете дублировать структуру кода, то сможете легче находить и обслуживать тесты.

Дорогостоящие ошибки

Детали реализации тестирования. Если тест ломается при переименовании приватного метода, значит, он тестирует не то, что нужно. Сделайте акцен на поведение, а не реализацию. Тесты должны отвечать на вопрос: «правильно ли это работает?», а не «вызовет ли это действие конкретно эти функции?».

Сломанная пирамида тестирования. Сюда относятся все модульные тесты, пропускающие ошибки интеграции, а также медленные и нестабильные сквозные тесты. Я видел обе крайности, и обходятся они весьма недешево.

В одном проекте у нас было 90%-ное покрытие модульными тестами, и все заменили на моки. Ничего не предвещало беды, пока не возникло состояние гонки. В результате оно обошлось нам в миллионы, поскольку транзакции считались дважды. Тесты были зелеными, но система не работала. В итоге мы отказались от 70% тестового набора и переписали его на интеграционные тесты с резидентными БД (in-memory базы данных) и реальными потоками сообщений. Эти тесты сразу же отловили ошибку.

Нет стратегии очистки. Тесты, оставляющие после себя грязные данные в БД, сообщения в очередях или файлы на дисках, испортят будущие прогоны. Между прочим, так и возникают загадочные сбои, которые появляются только при запуске тестов в определенном порядке.

Всегда выполняйте очистку. Для этого пользуйтесь фикстурами pytest с подходящим tearDown. Либо транзакциями БД, которые откатываются назад после каждого теста.

Замедление тестовых наборов. Производительность важна. Если на локальное выполнение тестов уходит слишком много времени, разработчики попросту не будут их запускать. Поищите тесты, которые выполняют «дорогостоящие» операции: файловый ввод-вывод, сетевые вызовы и т.д. Может, получится заменить их моками? Или удастся найти альтернативы из памяти?

Суровый урок

Если говорить о личном опыте, то мой самый ужасный сбой в тестировании произошел во время крупного рефакторинга. Все передавалось в CI. Везде были зеленые галочки. Развернули в прод, и сразу же отвалилось 20% трафика.

Оказалось, что нисходящий API изменил свой контракт, а у нас не было ни одного интеграционного теста, который бы покрывал эту ветку кода. Все наши тесты были моками или чисто модульными. А реальную интеграцию так никто и не проверил.

С тех пор я всегда добавляю несколько тестов, которые досконально проверят критические процессы с реальными зависимостями. Не все процессы, а те ветки, которые при поломке чреваты большими неприятностями.

Есть и другая история. Как-то раз тестирование на основе свойств (property-based testing) избавило нас от больших заморочек. При проверке одного свойства тест отловил ошибку параллелизма в потоке платежных операций. Причем эту ошибку мы никогда бы не обнаружили тестами на готовых примерах. Иногда очень важно выбрать правильный инструмент в правильное время.

Не забываем о практичности

При тестировании в масштабе важно найти баланс. Тесты должны быть:

  • Настолько быстрыми, чтобы люди могли их запускать
  • Настолько исчерпывающими, чтобы они отлавливали реальные баги
  • Настолько поддерживаемыми, чтобы они не ложились на всех тяжким бременем.

Не важно, какой у вас процент покрытия, если тесты проверяют не то что нужно. Небольшой набор хорошо написанных тестов всегда лучше гигантского набора нестабильных тестов.

Начните с пирамиды тестирования. Правильно пользуйтесь pytest. Заменяйте моками внешние системы. Следите за быстротой тестов. Подчищайте за собой. И вот вы уже знаете 90% того, что нужно.

Остальному научитесь на ошибках. Ведь не зря говорят: на ошибках – учатся, особенно на своих.

Об авторе: ведущий разработчик ПО с 14-летним опытом в создании масштабных распределенных систем. В настоящее время работает в Oracle Cloud Infrastructure. Ранее работал в Amazon, Salesforce, IBM и Tableau.

🔥 Какой была ваша первая зарплата в QA и как вы искали первую работу? 

Мега обсуждение в нашем телеграм-канале о поиске первой работы. Обмен опытом и мнения.

Читать в телеграм

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *