← все темы
Тестирование в Python
Вопросов с разбором: 24. В боте эти же карточки приходят по расписанию повторений.
1 Блок 1 из 5: Основы — Какие бывают виды тестов и что такое пирамида тестирования? 🔥
- Юнит-тесты — проверяют одну единицу (функцию, класс) изолированно. Быстрые (миллисекунды), их много.
- Интеграционные — проверяют связку компонентов: код плюс реальная БД, репозиторий плюс ORM, сервис плюс брокер.
- E2E (сквозные) — весь сценарий через внешний интерфейс: HTTP API или браузер. Медленные и хрупкие, их мало.
- Ещё бывают: контрактные (совместимость API между сервисами), нагрузочные, smoke (система вообще поднялась).
Пирамида тестирования: внизу много быстрых юнит-тестов, в середине меньше интеграционных, наверху немного E2E.
Почему так: чем выше уровень, тем тест медленнее, дороже в поддержке и тем хуже указывает на место ошибки. Но юнит-тесты не ловят ошибки на стыках, поэтому нужны все уровни.
Антипаттерн «рожок мороженого»: в основном ручные и E2E-тесты, юнит-тестов почти нет.
- Интеграционные — проверяют связку компонентов: код плюс реальная БД, репозиторий плюс ORM, сервис плюс брокер.
- E2E (сквозные) — весь сценарий через внешний интерфейс: HTTP API или браузер. Медленные и хрупкие, их мало.
- Ещё бывают: контрактные (совместимость API между сервисами), нагрузочные, smoke (система вообще поднялась).
Пирамида тестирования: внизу много быстрых юнит-тестов, в середине меньше интеграционных, наверху немного E2E.
Почему так: чем выше уровень, тем тест медленнее, дороже в поддержке и тем хуже указывает на место ошибки. Но юнит-тесты не ловят ошибки на стыках, поэтому нужны все уровни.
Антипаттерн «рожок мороженого»: в основном ручные и E2E-тесты, юнит-тестов почти нет.
2 Блок 1 из 5: Основы — Чем pytest отличается от unittest? 🔥
unittest — стандартная библиотека, стиль xUnit из Java:
pytest — сторонняя библиотека, фактический стандарт:
Преимущества pytest:
- обычный
- фикстуры с внедрением зависимостей и scope вместо
-
- запускает и тесты на unittest — можно мигрировать постепенно.
class TestPrice(unittest.TestCase):
def setUp(self):
self.cart = Cart()
def test_empty_cart_total_is_zero(self):
self.assertEqual(self.cart.total(), 0)
pytest — сторонняя библиотека, фактический стандарт:
@pytest.fixture
def cart():
return Cart()
def test_empty_cart_total_is_zero(cart):
assert cart.total() == 0
Преимущества pytest:
- обычный
assert с подробным разбором при падении (assert rewriting);- фикстуры с внедрением зависимостей и scope вместо
setUp/tearDown;-
parametrize, маркеры, богатая экосистема плагинов (pytest-django, pytest-asyncio, pytest-cov, pytest-xdist);- запускает и тесты на unittest — можно мигрировать постепенно.
3 Блок 1 из 5: Основы — Каким должен быть хороший юнит-тест?
Структура AAA (Arrange — Act — Assert, или Given — When — Then):
Свойства хорошего теста (FIRST):
- Fast — быстрый, его запускают постоянно;
- Independent — не зависит от других тестов и порядка запуска;
- Repeatable — одинаковый результат везде: без реальной сети, текущего времени, случайности;
- Self-validating — сам говорит «прошёл/упал», без ручной проверки;
- Timely — пишется вместе с кодом.
Ещё:
- название описывает поведение:
- одна причина упасть — проверяется одно поведение;
- проверяет поведение, а не реализацию: рефакторинг без изменения поведения не должен ломать тесты.
def test_cancelled_order_cannot_be_paid():
order = make_order(status='cancelled') # Arrange
with pytest.raises(OrderCancelled): # Assert
order.pay(amount=500) # Act
Свойства хорошего теста (FIRST):
- Fast — быстрый, его запускают постоянно;
- Independent — не зависит от других тестов и порядка запуска;
- Repeatable — одинаковый результат везде: без реальной сети, текущего времени, случайности;
- Self-validating — сам говорит «прошёл/упал», без ручной проверки;
- Timely — пишется вместе с кодом.
Ещё:
- название описывает поведение:
test_cancelled_order_cannot_be_paid, а не test_pay_2;- одна причина упасть — проверяется одно поведение;
- проверяет поведение, а не реализацию: рефакторинг без изменения поведения не должен ломать тесты.
4 Блок 2 из 5: pytest — Что такое фикстуры в pytest и как они работают? 🔥
Фикстура — функция, которая готовит данные или окружение для теста. Тест получает её, указав имя в аргументах: pytest сам найдёт и вызовет фикстуру (внедрение зависимостей).
Teardown через yield — код после
Где хранить: общие фикстуры кладут в
@pytest.fixture
def user():
return User(name='Аня', email='anya@example.com')
@pytest.fixture
def order(user): # фикстура может зависеть от другой
return Order(customer=user)
def test_order_belongs_to_user(order, user):
assert order.customer == user
Teardown через yield — код после
yield выполнится после теста, даже если тест упал:@pytest.fixture
def db_connection():
conn = connect()
yield conn
conn.close()
Где хранить: общие фикстуры кладут в
conftest.py — pytest находит их автоматически для всех тестов в этой папке и вложенных, импортировать не нужно.
5 Блок 2 из 5: pytest — Что такое scope фикстуры и autouse?
Scope — как часто создаётся фикстура:
-
-
-
-
-
Правило: широкий scope — для дорогих и неизменяемых ресурсов (подключение, запущенный контейнер). Изменяемые данные — только
autouse=True — фикстура применяется ко всем тестам в области видимости без явного запроса. Подходит для глобальной подготовки: заморозить время, отключить сеть, очистить кэш.
-
function (по умолчанию) — для каждого теста заново;-
class — один раз на класс;-
module — один раз на файл;-
package — на пакет;-
session — один раз на весь запуск.@pytest.fixture(scope='session')
def engine():
engine = create_engine(TEST_DB_URL)
yield engine
engine.dispose()
Правило: широкий scope — для дорогих и неизменяемых ресурсов (подключение, запущенный контейнер). Изменяемые данные — только
function, иначе тесты начнут влиять друг на друга. Фикстура не может зависеть от фикстуры с более узким scope.autouse=True — фикстура применяется ко всем тестам в области видимости без явного запроса. Подходит для глобальной подготовки: заморозить время, отключить сеть, очистить кэш.
6 Блок 2 из 5: pytest — Как работает parametrize? 🔥
@pytest.mark.parametrize запускает один тест с разными наборами данных — каждый набор становится отдельным тестом с отдельным отчётом.@pytest.mark.parametrize('email, is_valid', [
('user@example.com', True),
('user@', False),
('', False),
('a@b.c', True),
])
def test_email_validation(email, is_valid):
assert validate_email(email) is is_validПолезное:
-
ids=[...] или pytest.param(..., id='empty') — читаемые имена кейсов в отчёте;-
pytest.param(..., marks=pytest.mark.xfail) — пометить отдельный кейс;- несколько декораторов
parametrize дают декартово произведение;- параметризовать можно и фикстуры:
@pytest.fixture(params=['sqlite', 'postgres']).Зачем: граничные случаи покрываются таблицей, а не копированием тестов.
7 Блок 2 из 5: pytest — Как проверить исключения, предупреждения и вывод в pytest?
def test_negative_amount_raises():
with pytest.raises(ValueError, match='отрицательн'):
Money(-1)
def test_error_details():
with pytest.raises(OrderNotFound) as exc_info:
service.get_order(999)
assert exc_info.value.order_id == 999
def test_deprecated_call_warns():
with pytest.warns(DeprecationWarning):
old_api()
def test_prints_greeting(capsys):
greet('Аня')
assert capsys.readouterr().out == 'Привет, Аня\n'
def test_logs_warning(caplog):
process(bad_item)
assert 'пропущен' in caplog.text
Важно:
match — регулярное выражение по тексту исключения. Внутри with pytest.raises держат только строку, которая должна упасть, иначе тест пройдёт из-за исключения в другом месте.
8 Блок 2 из 5: pytest — Какие встроенные фикстуры pytest полезны на практике?
-
-
-
-
-
tmp_path — временная папка (pathlib.Path), уникальная для теста. Для работы с файлами.-
monkeypatch — временно подменить атрибут, переменную окружения, элемент словаря; всё вернётся после теста.-
capsys / capfd — перехват stdout и stderr.-
caplog — перехват логов.-
request — информация о текущем тесте, параметры параметризованной фикстуры.def test_reads_config_from_env(monkeypatch, tmp_path):
config_file = tmp_path / 'app.toml'
config_file.write_text('debug = true')
monkeypatch.setenv('APP_CONFIG', str(config_file))
monkeypatch.setattr('app.settings.DEFAULT_TIMEOUT', 1)
assert load_config().debug is True
9 Блок 2 из 5: pytest — Что такое маркеры skip, xfail и собственные маркеры?
@pytest.mark.skip(reason='ждём фикс в библиотеке')
def test_a(): ...
@pytest.mark.skipif(sys.platform == 'win32', reason='только Linux')
def test_b(): ...
@pytest.mark.xfail(reason='известный баг #123', strict=True)
def test_c(): ...
@pytest.mark.slow
def test_full_import(): ...
- skip / skipif — не запускать (нет зависимости, не та платформа).
- xfail — ожидаемо падает. С
strict=True неожиданный успех тоже ошибка — значит, баг починили и маркер пора снять.- Свои маркеры (
slow, integration) регистрируют в pytest.ini/pyproject.toml и выбирают при запуске: pytest -m "not slow".Полезные флаги запуска:
-k "order and not cancel" — по имени, -x — остановиться на первом падении, --lf — только упавшие в прошлый раз, -n auto — параллельно (pytest-xdist).
10 Блок 3 из 5: Моки и подмены — Что такое test doubles: dummy, stub, spy, mock, fake? 🔥
Test double — объект, который подменяет реальную зависимость в тесте. Виды (по Мезаросу и Фаулеру):
- Dummy — заглушка, которую передают, но не используют (для заполнения аргументов).
- Stub — возвращает заранее заданные ответы: «курс валют всегда 90».
- Spy — stub, который ещё и запоминает, как его вызывали.
- Mock — объект с ожиданиями: тест проверяет, что его вызвали нужным образом.
- Fake — рабочая упрощённая реализация: репозиторий на словаре, SQLite вместо Postgres, in-memory брокер.
Два стиля проверки:
- проверка состояния — после действия смотрим результат (часто с fake);
- проверка поведения — проверяем, какие вызовы были сделаны (mock).
Практика: fake часто лучше моков — тесты меньше привязаны к деталям реализации.
- Dummy — заглушка, которую передают, но не используют (для заполнения аргументов).
- Stub — возвращает заранее заданные ответы: «курс валют всегда 90».
- Spy — stub, который ещё и запоминает, как его вызывали.
- Mock — объект с ожиданиями: тест проверяет, что его вызвали нужным образом.
- Fake — рабочая упрощённая реализация: репозиторий на словаре, SQLite вместо Postgres, in-memory брокер.
Два стиля проверки:
- проверка состояния — после действия смотрим результат (часто с fake);
- проверка поведения — проверяем, какие вызовы были сделаны (mock).
Практика: fake часто лучше моков — тесты меньше привязаны к деталям реализации.
unittest.mock.Mock в Python может играть роль stub, spy и mock одновременно.
11 Блок 3 из 5: Моки и подмены — Как работают Mock и MagicMock из unittest.mock?
Mock — объект, у которого любой атрибут и вызов возвращает новый Mock и запоминается.
-
-
-
MagicMock — то же, но с настроенными магическими методами (
from unittest.mock import Mock
gateway = Mock()
gateway.charge.return_value = {'status': 'ok'} # stub
gateway.refund.side_effect = TimeoutError # бросить исключение
gateway.get_rate.side_effect = [90, 91, 92] # ответы по очереди
service = PaymentService(gateway)
service.pay(order)
gateway.charge.assert_called_once_with(amount=500, currency='RUB') # spy/mock
assert gateway.charge.call_count == 1
-
return_value — что вернуть;-
side_effect — исключение, функция или итерируемое для последовательных ответов;-
assert_called_once_with, assert_not_called, call_args_list — проверки вызовов.MagicMock — то же, но с настроенными магическими методами (
__len__, __iter__, __enter__). AsyncMock — для корутин: вызов возвращает awaitable.
12 Блок 3 из 5: Моки и подмены — Где применять patch: в каком модуле подменять объект? 🔥
Главное правило: патчить там, где объект ищут (используют), а не там, где он определён.
Почему:
Если в коде
Формы: декоратор, контекстный менеджер
# app/notifications.py
from app.email import send_email
def notify(user):
send_email(user.email, 'Привет')
# неправильно: notifications уже держит свою ссылку на send_email
@patch('app.email.send_email')
# правильно: подменяем имя в модуле, который его использует
@patch('app.notifications.send_email')
def test_notify_sends_email(send_email_mock):
notify(user)
send_email_mock.assert_called_once()
Почему:
from x import y создаёт в модуле собственное имя y. patch заменяет атрибут в указанном модуле, а ссылка в другом модуле остаётся старой.Если в коде
import app.email и вызов app.email.send_email(...) — тогда патчат app.email.send_email.Формы: декоратор, контекстный менеджер
with patch(...), patch.object(SomeClass, 'method'), фикстура mocker из pytest-mock.
13 Блок 3 из 5: Моки и подмены — Что такое autospec и зачем он нужен?
Обычный
autospec строит мок по реальному объекту: доступны только существующие атрибуты, вызовы проверяются по сигнатуре. Похожая защита —
Рекомендация: мокать с
Mock принимает любые вызовы. Если метод переименовали или поменяли сигнатуру, тест с моком продолжит проходить, а код в проде упадёт.gateway = Mock()
gateway.chrage(100) # опечатка — Mock молча согласится
gateway = create_autospec(PaymentGateway, instance=True)
gateway.chrage(100) # AttributeError
gateway.charge() # TypeError: не хватает аргумента amount
@patch('app.service.PaymentGateway', autospec=True)
def test_pay(gateway_cls): ...
autospec строит мок по реальному объекту: доступны только существующие атрибуты, вызовы проверяются по сигнатуре. Похожая защита —
Mock(spec=PaymentGateway), но она проверяет только имена атрибутов, без сигнатур.Рекомендация: мокать с
autospec=True по умолчанию.
14 Блок 3 из 5: Моки и подмены — Когда моки вредят и что мокать, а что нет? 🔥
Мокать стоит:
- внешние системы за границей приложения: сторонние API, платёжки, почта, SMS;
- недетерминированное: время, случайность, генерацию UUID;
- медленное или опасное: отправка писем, списание денег.
Не стоит мокать:
- свой собственный код внутри тестируемого модуля — тест начинает проверять реализацию, а не поведение;
- то, чем не владеешь, напрямую (requests, boto3, ORM): лучше обернуть в свой адаптер и мокать его или подставить fake;
- value-объекты и простые структуры данных;
- БД — запросы на моках ничего не проверяют; лучше реальная тестовая БД.
Признаки злоупотребления моками:
- в тесте больше настройки моков, чем проверок;
- рефакторинг без изменения поведения ломает тесты;
- тесты зелёные, а в проде падает на стыке.
Правило: мок должен соответствовать реальному контракту (autospec, контрактные тесты), иначе тест проверяет фантазию.
- внешние системы за границей приложения: сторонние API, платёжки, почта, SMS;
- недетерминированное: время, случайность, генерацию UUID;
- медленное или опасное: отправка писем, списание денег.
Не стоит мокать:
- свой собственный код внутри тестируемого модуля — тест начинает проверять реализацию, а не поведение;
- то, чем не владеешь, напрямую (requests, boto3, ORM): лучше обернуть в свой адаптер и мокать его или подставить fake;
- value-объекты и простые структуры данных;
- БД — запросы на моках ничего не проверяют; лучше реальная тестовая БД.
Признаки злоупотребления моками:
- в тесте больше настройки моков, чем проверок;
- рефакторинг без изменения поведения ломает тесты;
- тесты зелёные, а в проде падает на стыке.
Правило: мок должен соответствовать реальному контракту (autospec, контрактные тесты), иначе тест проверяет фантазию.
15 Блок 4 из 5: Тесты с внешним миром — Как тестировать код, работающий с базой данных? 🔥
Подходы:
- Реальная БД того же типа, что в проде (Postgres в Docker или Testcontainers). Ловит ошибки SQL, ограничений и миграций. Предпочтительно.
- SQLite вместо Postgres — быстро, но другое поведение: типы, JSON, блокировки,
- Fake-репозиторий в памяти — для юнит-тестов бизнес-логики, а не для запросов.
Изоляция тестов:
- каждый тест в транзакции с откатом — быстро. Так делают Django
- очистка таблиц (TRUNCATE) — когда нужно проверить реальный коммит:
- схему создают один раз на сессию миграциями — заодно проверяются сами миграции.
- Реальная БД того же типа, что в проде (Postgres в Docker или Testcontainers). Ловит ошибки SQL, ограничений и миграций. Предпочтительно.
- SQLite вместо Postgres — быстро, но другое поведение: типы, JSON, блокировки,
ON CONFLICT. Даёт ложную уверенность.- Fake-репозиторий в памяти — для юнит-тестов бизнес-логики, а не для запросов.
Изоляция тестов:
- каждый тест в транзакции с откатом — быстро. Так делают Django
TestCase и фикстура db из pytest-django;- очистка таблиц (TRUNCATE) — когда нужно проверить реальный коммит:
on_commit, несколько соединений. В Django это TransactionTestCase;- схему создают один раз на сессию миграциями — заодно проверяются сами миграции.
@pytest.mark.django_db
def test_paid_orders_are_listed(client, order_factory):
order_factory(status='paid')
order_factory(status='new')
response = client.get('/api/orders?status=paid')
assert len(response.json()['results']) == 1
16 Блок 4 из 5: Тесты с внешним миром — Что лучше для тестовых данных: фикстуры-файлы или фабрики (factory_boy)?
Статичные фикстуры (JSON или YAML,
- ❌ тест зависит от данных, которых не видно в самом тесте;
- ❌ при изменении схемы файлы приходится править вручную;
- ❌ один файл обслуживает много тестов, и правка для одного ломает другие.
Фабрики (
Вывод: фабрики делают тест самодостаточным и устойчивым к изменениям схемы. Статичные данные оставляют для справочников.
loaddata в Django) — общий набор данных для всех тестов.- ❌ тест зависит от данных, которых не видно в самом тесте;
- ❌ при изменении схемы файлы приходится править вручную;
- ❌ один файл обслуживает много тестов, и правка для одного ломает другие.
Фабрики (
factory_boy, model_bakery) — создают объекты с разумными значениями по умолчанию; в тесте указывают только важные поля.class OrderFactory(factory.django.DjangoModelFactory):
class Meta:
model = Order
customer = factory.SubFactory(UserFactory)
status = 'new'
total = factory.Faker('pydecimal', left_digits=4, right_digits=2, positive=True)
def test_cancel_paid_order_creates_refund():
order = OrderFactory(status='paid') # видно, что важно для теста
...
Вывод: фабрики делают тест самодостаточным и устойчивым к изменениям схемы. Статичные данные оставляют для справочников.
17 Блок 4 из 5: Тесты с внешним миром — Как тестировать код, который ходит во внешние HTTP API?
Варианты по уровню:
1. Свой адаптер + fake: бизнес-логика зависит от
2. Перехват HTTP — проверить сам адаптер: запросы, заголовки, обработку ошибок.
3. Запись и воспроизведение (
4. Контрактные тесты (Pact) — проверить, что мок совпадает с реальным API провайдера.
Обязательно проверить: таймауты, 5xx, 429, некорректный JSON — то, что редко случается при ручной проверке.
1. Свой адаптер + fake: бизнес-логика зависит от
PaymentGateway, в тестах подставляется FakePaymentGateway. Самые устойчивые тесты.2. Перехват HTTP — проверить сам адаптер: запросы, заголовки, обработку ошибок.
# httpx + respx
@respx.mock
def test_get_rate():
respx.get('https://api.rates.io/usd').respond(json={'rate': 90})
assert RatesClient().usd() == 90
# requests + responses
@responses.activate
def test_get_rate_timeout():
responses.get('https://api.rates.io/usd', body=requests.Timeout())
with pytest.raises(RatesUnavailable):
RatesClient().usd()
3. Запись и воспроизведение (
vcrpy, pytest-recording) — один раз записать реальные ответы в кассету.4. Контрактные тесты (Pact) — проверить, что мок совпадает с реальным API провайдера.
Обязательно проверить: таймауты, 5xx, 429, некорректный JSON — то, что редко случается при ручной проверке.
18 Блок 4 из 5: Тесты с внешним миром — Как тестировать код, зависящий от времени?
Проблема:
Способ 1 — заморозить время:
Способ 2 — передавать время или часы явно (лучше для дизайна):
Граничные случаи, которые стоит проверить: конец месяца, 29 февраля, переход через полночь, часовые пояса (UTC против локального времени).
datetime.now() внутри кода делает тест недетерминированным: «подписка истекает через 30 дней» проходит или падает в зависимости от дня запуска.Способ 1 — заморозить время:
from freezegun import freeze_time # или time-machine — быстрее
@freeze_time('2026-01-31 23:59:00')
def test_subscription_expires_end_of_month():
sub = Subscription.start(days=1)
assert sub.expires_at == datetime(2026, 2, 1, 23, 59)
Способ 2 — передавать время или часы явно (лучше для дизайна):
def is_expired(subscription, now):
return subscription.expires_at <= now
class Service:
def __init__(self, clock=datetime.now):
self._clock = clock
Граничные случаи, которые стоит проверить: конец месяца, 29 февраля, переход через полночь, часовые пояса (UTC против локального времени).
19 Блок 4 из 5: Тесты с внешним миром — Как тестировать асинхронный код?
Обычный pytest не умеет запускать корутины. Нужен плагин: pytest-asyncio или anyio.
- В
- Асинхронные зависимости мокают через
- FastAPI-приложение тестируют через
- Проверять стоит и таймауты, и отмену (
import pytest
@pytest.mark.asyncio
async def test_fetch_user():
client = UserClient(base_url='https://api.test')
user = await client.get(42)
assert user.id == 42
@pytest.fixture
async def redis():
conn = await create_redis()
yield conn
await conn.aclose()
- В
pyproject.toml можно включить asyncio_mode = "auto", тогда маркер не нужен.- Асинхронные зависимости мокают через
AsyncMock.- FastAPI-приложение тестируют через
httpx.AsyncClient с ASGITransport или синхронным TestClient.- Проверять стоит и таймауты, и отмену (
asyncio.CancelledError), а не только успешный путь.
20 Блок 5 из 5: Качество тестов — Что такое покрытие кода и стоит ли стремиться к 100%? 🔥
Покрытие (coverage) — доля кода, выполненного во время тестов. Инструмент —
- Line coverage — какие строки выполнились.
- Branch coverage — прошли ли обе ветки каждого
100% — не цель:
- покрытие показывает, что код выполнился, а не что результат проверили. Тест без
- последние проценты дорогие и часто приводят к тестам ради цифры;
- зато низкое покрытие точно указывает на непроверенные места.
Разумный подход: высокое покрытие бизнес-логики, порог в CI, чтобы покрытие не падало (например, 80%), и проверка, что новый код покрыт. Качество тестов можно оценить мутационным тестированием (
coverage.py или pytest --cov.- Line coverage — какие строки выполнились.
- Branch coverage — прошли ли обе ветки каждого
if. Честнее: if x: y = 1 даёт 100% строк, даже если ветку «else» ни разу не проверили.100% — не цель:
- покрытие показывает, что код выполнился, а не что результат проверили. Тест без
assert тоже даёт покрытие;- последние проценты дорогие и часто приводят к тестам ради цифры;
- зато низкое покрытие точно указывает на непроверенные места.
Разумный подход: высокое покрытие бизнес-логики, порог в CI, чтобы покрытие не падало (например, 80%), и проверка, что новый код покрыт. Качество тестов можно оценить мутационным тестированием (
mutmut): если внесённую в код ошибку тесты не замечают — они слабые.
21 Блок 5 из 5: Качество тестов — Что такое flaky-тесты и как с ними бороться?
Flaky-тест — иногда проходит, иногда падает без изменения кода. Опасен тем, что команда перестаёт доверять красному CI.
Частые причины:
- зависимость от порядка — тест полагается на данные или состояние, оставленные другим тестом;
- время —
- асинхронность и гонки —
- случайность без фиксированного seed;
- внешние сервисы и сеть;
- порядок без ORDER BY — БД не гарантирует порядок строк;
- общие ресурсы при параллельном запуске: порты, файлы, одна БД.
Что делать:
- воспроизвести:
- изолировать состояние, заморозить время, ждать условие с таймаутом вместо
- не лечить ретраями — это маскирует проблему; flaky-тест чинят или временно выключают с задачей на починку.
Частые причины:
- зависимость от порядка — тест полагается на данные или состояние, оставленные другим тестом;
- время —
now(), полночь, часовые пояса;- асинхронность и гонки —
sleep(1) вместо ожидания события;- случайность без фиксированного seed;
- внешние сервисы и сеть;
- порядок без ORDER BY — БД не гарантирует порядок строк;
- общие ресурсы при параллельном запуске: порты, файлы, одна БД.
Что делать:
- воспроизвести:
pytest -p randomly (случайный порядок), pytest-repeat, --count=100;- изолировать состояние, заморозить время, ждать условие с таймаутом вместо
sleep;- не лечить ретраями — это маскирует проблему; flaky-тест чинят или временно выключают с задачей на починку.
22 Блок 5 из 5: Качество тестов — Что такое TDD?
TDD (Test-Driven Development) — разработка через тестирование. Цикл Red — Green — Refactor:
1. Red — написать маленький тест на ещё не существующее поведение и убедиться, что он падает.
2. Green — написать минимальный код, чтобы тест прошёл.
3. Refactor — улучшить код и тесты, не ломая их.
Плюсы:
- код изначально тестируемый: маленькие функции, явные зависимости;
- тест сначала падает — значит, он действительно что-то проверяет;
- быстрая обратная связь и безопасный рефакторинг;
- тесты служат спецификацией.
Минусы и ограничения:
- медленнее на старте, требует навыка;
- неудобен, когда задача исследовательская и интерфейс ещё неизвестен;
- не гарантирует хорошую архитектуру сам по себе.
На практике TDD особенно хорош для бизнес-логики с чёткими правилами и для исправления багов: сначала тест, воспроизводящий баг, потом исправление.
1. Red — написать маленький тест на ещё не существующее поведение и убедиться, что он падает.
2. Green — написать минимальный код, чтобы тест прошёл.
3. Refactor — улучшить код и тесты, не ломая их.
Плюсы:
- код изначально тестируемый: маленькие функции, явные зависимости;
- тест сначала падает — значит, он действительно что-то проверяет;
- быстрая обратная связь и безопасный рефакторинг;
- тесты служат спецификацией.
Минусы и ограничения:
- медленнее на старте, требует навыка;
- неудобен, когда задача исследовательская и интерфейс ещё неизвестен;
- не гарантирует хорошую архитектуру сам по себе.
На практике TDD особенно хорош для бизнес-логики с чёткими правилами и для исправления багов: сначала тест, воспроизводящий баг, потом исправление.
23 Блок 5 из 5: Качество тестов — Что такое property-based тестирование (Hypothesis)?
Обычный тест проверяет конкретные примеры. Property-based тест описывает свойство, которое верно для любых входных данных, а библиотека сама генерирует сотни случаев, включая граничные.
Типичные свойства:
- обратимость:
- идемпотентность:
- инварианты: сумма не меняется, длина сохраняется;
- сравнение с простой эталонной реализацией.
Сильная сторона: находит случаи, о которых не подумали: пустые строки, юникод, огромные числа,
from hypothesis import given, strategies as st
@given(st.lists(st.integers()))
def test_sort_is_idempotent(xs):
assert sorted(sorted(xs)) == sorted(xs)
@given(st.text())
def test_encode_decode_roundtrip(s):
assert decode(encode(s)) == s
Типичные свойства:
- обратимость:
decode(encode(x)) == x;- идемпотентность:
f(f(x)) == f(x);- инварианты: сумма не меняется, длина сохраняется;
- сравнение с простой эталонной реализацией.
Сильная сторона: находит случаи, о которых не подумали: пустые строки, юникод, огромные числа,
NaN. При падении Hypothesis уменьшает пример до минимального, на котором тест падает.
24 Блок 5 из 5: Качество тестов — Как тестировать API на Django и FastAPI? 🔥
Django / DRF:
FastAPI:
Что проверять: статус-коды, формат ответа и ошибок, права доступа (чужой ресурс → 403/404), валидацию, побочные эффекты в БД. Бизнес-логику при этом лучше покрывать юнит-тестами ниже по пирамиде.
@pytest.mark.django_db
def test_create_order(api_client, user):
api_client.force_authenticate(user)
response = api_client.post('/api/orders/', {'product_id': 1, 'qty': 2}, format='json')
assert response.status_code == 201
assert Order.objects.filter(customer=user).count() == 1
django.test.Client или APIClient из DRF прогоняют запрос через middleware, URL, view и сериализатор без реального сервера.FastAPI:
def test_get_order(client):
app.dependency_overrides[get_db] = lambda: fake_db
app.dependency_overrides[get_current_user] = lambda: User(id=1)
response = client.get('/orders/42') # client = TestClient(app)
assert response.status_code == 200
app.dependency_overrides.clear()
dependency_overrides подменяет зависимости (БД, текущего пользователя, внешние клиенты) без patch.Что проверять: статус-коды, формат ответа и ошибок, права доступа (чужой ресурс → 403/404), валидацию, побочные эффекты в БД. Бизнес-логику при этом лучше покрывать юнит-тестами ниже по пирамиде.
Ничего не нашлось — попробуйте другой запрос.
Хотите не просто прочитать, а запомнить?
Бот вернёт вам эти вопросы ровно тогда, когда вы начнёте их забывать.