Тесты редко пишут с удовольствием. Обычно их пишут на бегу, в конце спринта, когда менеджер уже спрашивает про релиз, — и именно поэтому они получаются хрупкими и бесполезными. LLM здесь работает не как «кнопка сделать тесты», а как напарник, который знает синтаксис pytest, помнит про pytest.mark.parametrize и не устаёт придумывать граничные случаи.
Ниже — 12 готовых промтов, которые можно копировать в ChatGPT, Claude или локальную модель. У каждого — задача, пример входных данных и пояснение, что именно вы получите на выходе. Проверяйте любой сгенерированный код: модель уверенно пишет тест, который зелёный, но проверяет не то, что вы думали.
Генерация и параметризация
1. Базовый тест по сигнатуре функции
Задача: быстро получить скелет теста для чистой функции без побочных эффектов.
Промт:
Ты — Python-разработчик, пишущий тесты на pytest. Вот функция:
<код>
{вставь функцию}
</код>
Напиши тесты на pytest, которые покрывают:
1) типичный happy path,
2) граничные значения аргументов (пустая строка, 0, отрицательные числа, None, если допустимо),
3) ожидаемые исключения с pytest.raises и проверкой текста ошибки.
Не используй unittest. Каждый тест — отдельная функция с говорящим именем.
В конце — краткий список случаев, которые остались непокрытыми, и почему.
Пример: функция parse_price(text: str) -> Decimal, принимающая строки вида "1 234,50 ₽". Модель выдаст тест на обычную строку, на строку без копеек, на неразрывный пробел и на ValueError для "abc".
Последний пункт промта — самый ценный: список непокрытых случаев часто выявляет дыры в самом ТЗ, а не в коде.
2. Параметризованные тесты вместо копипасты
Задача: превратить пять почти одинаковых тестов в один parametrize.
Промт:
Сожми эти тесты в один параметризованный тест через @pytest.mark.parametrize.
Требования:
- каждый набор данных — отдельный элемент списка кортежей;
- добавь параметр ids с человекочитаемыми именами;
- если ожидается исключение, передавай его класс в параметре и проверяй через pytest.raises;
- сохрани все исходные проверки, ничего не выкидывай.
<код тестов>
{вставь тесты}
</код>
Зачем: параметризация — главный инструмент против «тестов-близнецов». Плюс при падении pytest покажет test_parse[negative], а не безымянный test_parse_3, и вы сразу поймёте, какой кейс сломался.
Фикстуры и изоляция
3. Фикстуры с правильной областью жизни
Задача: вынести подготовку окружения в фикстуры, не путая scope.
Промт:
У меня есть тесты, каждый из которых сам создаёт временную БД и поднимает подключение.
Вынеси подготовку в фикстуры pytest.
Правила:
- подключение к БД — scope="session";
- чистая схема на каждый тест — scope="function" с очисткой в yield-блоке;
- фикстуры положи в conftest.py;
- покажи, как тест запрашивает фикстуру по имени.
Объясни для каждой фикстуры, почему выбран именно такой scope и что сломается при другом.
Тонкость: scope="session" у подключения экономит секунды на старте, но если тест мутирует состояние и не убирает за собой, следующий тест упадёт «на пустом месте». Модель по умолчанию любит ставить function — и это правильный дефолт.
yield и очистку после него. Без teardown-а тесты пройдут поодиночке и развалятся при запуске всей папки — классическая ловушка.
4. Фабрика тестовых данных
Задача: избавиться от простыней User(name="...", email="...", created_at=...) в каждом тесте.
Промт:
Сделай pytest-фикстуру-фабрику для модели User (dataclass с полями name, email, created_at, is_active).
Требования:
- фикстура возвращает функцию make_user(**overrides);
- все поля имеют разумные дефолты, email уникален (uuid);
- overrides перекрывают любые поля;
- покажи три примера использования.
Выигрыш: тест читается как «сделай активного юзера с конкретным email», а не как «собери объект из восьми полей». Когда в модель добавят поле, править придётся одну фабрику, а не сорок тестов.
Мокирование
5. Мок внешнего HTTP API
Задача: тестировать код, который ходит в платёжный шлюз, не делая реальных запросов.
Промт:
Функция get_rate() делает requests.get к внешнему API и парсит JSON.
Напиши тесты с моками через pytest + monkeypatch или unittest.mock.
Покрой:
1) успешный ответ 200 с валидным JSON;
2) 200, но в JSON нет нужного поля;
3) 404 и 500 — ожидаем наше кастомное исключение ApiError;
4) таймаут requests.Timeout;
5) проверку, что URL собран правильно (assert на аргумент вызова).
Покажи оба подхода: monkeypatch и MagicMock, объясни разницу.
Что важно: пункт 5 — assert на аргументы вызова — ловит баги, где код ходит не на тот endpoint. Без него мок «зелёный», а в проде 404.
6. Мок базы данных в сервисном слое
Задача: протестировать бизнес-логику без реальной БД.
Промт:
Сервис OrderService принимает репозиторий через конструктор (dependency injection).
Напиши тесты с фейковым репозиторием (не MagicMock, а класс FakeOrderRepo с dict внутри).
Покрой: создание заказа, отказ при недостатке товара, идемпотентность при повторном вызове с тем же idempotency_key.
Объясни, почему фейк лучше MagicMock именно здесь.
Ключевое различие: MagicMock подтвердит любой вызов и промолчит о неверной логике. Фейк с реальным словарём ведёт себя как настоящий репозиторий и падает там, где падал бы прод.
Property-based testing
7. Первый property-based тест на Hypothesis
Задача: найти входы, о которых вы не подумали.
Промт:
Напиши property-based тест на Hypothesis для функции encode/decode (base64-подобное кодирование).
Свойства, которые надо проверить:
1) decode(encode(x)) == x для любых bytes;
2) длина результата encode кратна 4;
3) encode не падает на пустой строке.
Используй @given и st.binary(). Покажи, как воспроизвести упавший пример через @example.
Почему это работает: Hypothesis не перебирает случаи из вашего списка, а генерирует их по стратегии. Он найдёт пустой ввод, строку из одних нулевых байтов и всё, что вы забыли. Минус — тест медленнее обычного и иногда падает на «странном» входе, который надо отдельно разобрать.
8. Сужение стратегий для домена
Задача: не генерировать бессмысленные данные вроде отрицательного возраста.
Промт:
У меня property-based тест для расчёта скидки: скидка(purchase_amount, user_age).
Ограничь стратегии Hypothesis: сумма от 0 до 1_000_000, возраст от 14 до 100.
Добавь инвариант: скидка никогда не больше 30% и не отрицательна.
Объясни, зачем фильтровать домен и что будет, если этого не делать (assume vs стратегии).
Неочевидный вывод: assume() отбрасывает неподходящие примеры уже после генерации, и при узком домене Hypothesis может сдаться с FailedHealthCheck. Правильнее сузить саму стратегию через st.integers(min_value=14, max_value=100).
Покрытие и баг-репорты
9. Разбор отчёта coverage
Задача: найти ветки, которые не выполняются ни одним тестом.
Промт:
Вот вывод pytest --cov=app --cov-report=term-missing:
{вставь таблицу}
Для файла app/pricing.py найди непокрытые строки.
Предложи минимальный набор тестов, который закроет именно эти ветки, не раздувая набор.
Для каждой непокрытой строки объясни, какое условие её не достигает.
Тонкость: term-missing показывает номера строк, но не говорит, какое условие их пропускает. Модель по коду восстановит логику — и часто укажет на ветку if discount and user.is_vip, для которой нет теста с VIP-пользователем.
10. Тест из баг-репорта
Задача: превратить «упало у клиента» в воспроизводящий тест до фикса.
Промт:
Баг-репорт: «При оформлении заказа с промокодом на 100% скидки итоговая сумма становится отрицательной».
Вот код расчёта: {вставь функцию}.
Напиши failing-тест на pytest, который воспроизводит баг.
Тест должен падать на текущем коде и проходить после исправления.
Покажи ожидаемое поведение и текущее.
Зачем: тест, написанный до фикса, — единственное доказательство, что баг реально исправлен, а не «вроде перестал воспроизводиться». Плюс он остаётся в репозитории как регрессия.
11. Проверка качества сгенерированных тестов
Задача: поймать тесты, которые зелёные, но бесполезные.
Промт:
Вот тест, сгенерированный ранее: {вставь тест}.
Оцени его критически:
- есть ли assert, который реально проверяет поведение, а не просто «не упало»;
- не тестирует ли он реализацию вместо контракта;
- не сломается ли он при безобидном рефакторинге;
- какие мутации кода он пропустит?
Дай вердикт: оставить, переписать, удалить.
Практика: «мутационное мышление» — самый быстрый способ отличить настоящий тест от декоративного. Если тест переживёт замену return x + 1 на return x + 2, он бесполезен.
12. Тест на асинхронный код
Задача: не забыть про pytest-asyncio и не получить «coroutine was never awaited».
Промт:
Напиши async-тесты на pytest для функции fetch_user(user_id), которая делает await к httpx-клиенту.
Используй pytest-asyncio, мок httpx через respx или AsyncMock.
Покрой: успех, 404, таймаут.
Добавь в пример конфигурацию asyncio_mode в pytest.ini.
Режим отказа: без asyncio_mode = auto в конфиге каждый async-тест нужно помечать @pytest.mark.asyncio, иначе pytest молча пропустит корутину. Это одна из самых частых причин «тестов нет, а покрытие есть».
Что в итоге
| Промт | Когда применять | Риск |
|---|---|---|
| Базовый тест по сигнатуре | новая чистая функция | забудет про исключения |
| Параметризация | много похожих тестов | потеряет часть кейсов |
| Фикстуры со scope | подготовка окружения | неверный teardown |
| Фабрика данных | модели с многими полями | дефолты не отражают домен |
| Мок HTTP | внешние API | не проверит URL |
| Фейк-репозиторий | сервисный слой | сложнее в поддержке |
| Hypothesis | инварианты и парсеры | медленные прогоны |
| Разбор coverage | закрытие веток | тесты ради цифры |
| Тест из бага | регрессии | фиксирует неверное поведение |
Главный trade-off всей затеи: LLM ускоряет написание тестов в разы, но не отвечает за их смысл. Быстрый черновик от модели плюс пять минут вашей проверки — это дешевле, чем час на рутину; сгенерированные тесты без ревью — дороже, чем их отсутствие, потому что создают иллюзию покрытия.
Начните с одного промта на функцию, которую давно хотели покрыть, и посмотрите, что вернёт модель. Если хотите смежный навык — разберите промты для код-ревью: тестирование и ревью отлично работают в паре. А для работы с данными пригодятся промты для PostgreSQL.