Тестирование и приёмка глазами разработчика
В предыдущей статье — https://courses.digitable.life/post/sdlc-and-career/04-development/ — мы дошли до момента, когда код написан, отревьюен и слит в основную ветку. Дальше в учебниках обычно идёт бодрая фраза «затем задача передаётся в тестирование». Это самая безответственная фраза во всём SDLC.
Потому что «передаётся в тестирование» звучит так, будто есть отдельные люди, чья работа — ловить ваши ошибки, и пока они этим заняты, вы можете заниматься следующей задачей. В реальности:
- В доброй половине команд отдельных тестировщиков нет вообще.
- Там, где они есть, они физически не могут проверить то, что вы не объяснили.
- Ошибка, которую нашёл тестировщик, всё равно возвращается к вам — но уже с потерянным контекстом, через день-два, когда вы в голове уже в другой задаче.
- А ошибка, которую не нашёл никто, приходит к вам ночью в виде звонка.
Эта статья — про качество с той стороны стола, где сидите вы. Не «как стать QA-инженером» (это отдельная профессия со своей глубиной), а: что разработчик обязан делать сам, где проходит граница ответственности, как выглядит приёмка и почему «у меня локально работает» — не аргумент.
Если вам нужна не рабочая практика, а теория тестирования как дисциплины — терминология, техники тест-дизайна, классы эквивалентности, — смотрите глоссарий ISTQB, он бесплатный и на русском тоже есть. Здесь будет про то, как это живёт в реальном процессе.
Зачем на самом деле нужны тесты
Начнём с честного признания, которому больше полувека. Эдсгер Дейкстра, «The Humble Programmer» (1972):
Тестирование программ может демонстрировать наличие ошибок, но никогда — их отсутствие.
Это не пессимизм, а определение границ инструмента. Тесты не доказывают, что программа корректна. Пространство входов практически любой реальной функции астрономически велико, вы проверяете конечное подмножество.
Тогда зачем? У тестов в продакшн-разработке три функции, и «поиск багов» — только третья по важности.
1. Тесты дают право менять код. Это главное. Система, которую вы не можете безопасно изменить, мертва — её можно только переписать. Майкл Физерс в «Working Effectively with Legacy Code» даёт провокационное определение: legacy-код — это код без тестов, независимо от того, написан он вчера или десять лет назад. Смысл в том, что без тестов любое изменение — азартная игра, и команда начинает бояться собственного репозитория. Рефакторинг без тестов — это не рефакторинг, это переписывание с надеждой.
2. Тесты — исполняемая спецификация. Хороший тест отвечает на вопрос «а что вообще должна делать эта функция в таком случае?» быстрее, чем чтение реализации и надёжнее, чем комментарий или Confluence-страница трёхлетней давности. Тест не может устареть незаметно: если поведение поменялось, а тест не обновили, CI покраснеет.
3. Тесты ловят ошибки. Да. Но в основном — регрессии, то есть повторное появление уже известных проблем. Новые, интересные, неочевидные ошибки чаще находят люди руками и прод.
Из этого следует практическое правило, которое стоит принять как рабочее:
Пишете тест — спрашивайте себя не «покрыл ли я эту строку», а «какое изменение в коде я хочу, чтобы этот тест поймал». Если ответа нет, тест бесполезен.
Кто отвечает за качество: три модели
Первое, что вас удивит при смене работы: устройство QA различается радикальнее, чем стек технологий.
Модель A: выделенный отдел тестирования
Классика крупного энтерпрайза, банков, телекома, госзаказа. Есть отдел QA со своим руководителем, своими метриками и своим бюджетом. Разработка передаёт релиз-кандидат «в тест», отдел прогоняет тест-план, заводит дефекты, ставит визу.
Плюсы честно: независимая проверка (человек, который не писал код, не находится в плену своих же предположений), формализованные тест-кейсы как актив компании, воспроизводимая приёмка, возможность отвечать перед регулятором «вот протокол испытаний».
Минусы честно: длинная петля обратной связи (дефект возвращается к разработчику через дни), размывание ответственности («я сдал, дальше не моё»), конфликт метрик — у QA KPI по найденным дефектам, у разработки по закрытым задачам, и они начинают спорить не о продукте, а о статусе тикета.
Модель B: QA внутри команды
Самое распространённое сегодня. Тестировщик сидит в той же кросс-функциональной команде, ходит на те же дейли, участвует в груминге, читает требования до того, как код написан. Разработчик пишет юнит- и интеграционные тесты, QA — сценарные и E2E, и вместе они договариваются, что где проверяется.
Это, как правило, лучший баланс. Главный выигрыш даже не в тестировании, а в том, что хороший QA задаёт вопросы на этапе требований: «а что если пользователь нажмёт кнопку дважды?», «а что с валютой, отличной от рубля?». Такие вопросы, заданные до разработки, экономят недели.
Модель C: разработчики отвечают за качество целиком
Типично для стартапов, продуктовых команд с сильной инженерной культурой, инфраструктурных команд. Выделенного QA нет, есть автотесты, стейджинг, feature flags, канареечные выкатки и мониторинг. Проверка гипотезы «работает ли» частично переносится в прод — но с сеткой безопасности.
Плюсы: короткая петля, никакого перекидывания ответственности, деплой десятки раз в день. Минусы: требует зрелой инфраструктуры и дисциплины. Без быстрого отката и вменяемого мониторинга это не «модель C», а «мы просто не тестируем». Разница огромная, а выглядит снаружи одинаково — на собеседовании обязательно уточняйте, есть ли у команды возможность откатиться за минуты, и как быстро они замечают деградацию.
Обратите внимание: ни одна из моделей не «правильнее». Отсутствие QA в команде, которая пилит биллинг для банка с обязательной сертификацией, — безответственность. Наличие отдела ручного регресса на две недели в стартапе, который ищет product-market fit, — способ умереть, не успев проверить гипотезу.
Уровни тестов: разбираемся в терминологии
Терминология в тестировании исторически размыта: «интеграционный тест» в двух компаниях может означать совершенно разное. Мартин Фаулер в «UnitTest» прямо признаёт, что единого определения юнит-теста нет, и предлагает делить по свойству изоляции: solitary (все соседи заменены на дублёры) и sociable (реальные соседи).
Гугл вообще отказался от смысловых названий в пользу test sizes — small/medium/large, где критерий чисто механический: что тесту разрешено трогать (поток, файловую систему, сеть, несколько машин). Это удивительно практичный подход: спор о названиях исчезает, остаётся спор о ресурсах и скорости.
Практическая шпаргалка, которая работает в большинстве команд:
| Уровень | Что внутри | Скорость | Что ловит | Что не ловит | Чем платите |
|---|---|---|---|---|---|
| Статический анализ | компилятор, типы, линтер, mypy/tsc, SAST |
миллисекунды | опечатки, None, неверные сигнатуры, часть уязвимостей |
любую логическую ошибку | ложные срабатывания, споры о правилах |
| Юнит | одна функция/класс, зависимости — дублёры | миллисекунды | ошибки в логике, граничные случаи, ветвления | всё, что про взаимодействие компонентов | хрупкость при рефакторинге, если мокать лишнее |
| Интеграционный | ваш код + реальная БД/очередь/файлы | секунды | SQL, миграции, сериализация, транзакции, конфиги | поведение чужих сервисов | нужно поднимать инфраструктуру |
| Контрактный | согласованность API между сервисами | секунды | «я поменял поле, а потребитель сломался» | бизнес-логику каждой стороны | требует договорённости обеих команд |
| Компонентный / API | сервис целиком через HTTP, соседи — заглушки | секунды-минуты | роутинг, авторизация, коды ответов, валидация | UI, интеграцию сквозь всю систему | нужен способ поднять сервис изолированно |
| E2E | вся система, браузер или реальные клиенты | минуты | «путь пользователя развалился» | причину поломки (диагностика ужасная) | флаки, время, поддержка |
| Ручной / исследовательский | человек | часы | то, о чём никто не подумал | воспроизводимость | не масштабируется, не повторяется в CI |
Важнейшая строка здесь — предпоследняя. У Гугла есть жёсткая статья на эту тему: «Just Say No to More End-to-End Tests». Основной аргумент: E2E-тест, который падает, сообщает вам «что-то где-то сломалось», и на локализацию уходит больше времени, чем на саму починку. Плюс он падает и когда всё в порядке (сеть моргнула).
Это не значит «E2E не нужны». Значит: их должно быть немного, и они должны покрывать критические денежные сценарии — регистрация, оплата, оформление заказа. Не «проверим все 40 полей формы через браузер».
Форма набора тестов: пирамида, трофей и рожок
Пирамида (Mike Cohn, популяризована Фаулером — TestPyramid, подробный разбор у Хама Фокке — The Practical Test Pyramid): много быстрых юнит-тестов, меньше интеграционных, совсем мало E2E. Логика — экономическая: чем ниже уровень, тем дешевле тест в написании, прогоне и диагностике.
Трофей (Kent C. Dodds, «Write tests. Not too many. Mostly integration.»): фундамент — статика (типы + линтер), а центр тяжести смещён к интеграционным тестам. Родился во фронтенде, где юнит-тест отдельного React-компонента с замоканными хуками проверяет в основном ваши моки, а не поведение. Есть и промежуточная модель — «соты» Spotify для микросервисов, где основная масса — интеграционные тесты сервиса.
Рожок мороженого — то, что получается само собой, если тесты писать «когда будет время»: гора ручного тестирования, под ней куча хрупких E2E, а юнит-тестов почти нет. Признаки, что вы в рожке: прогон CI идёт 40 минут, половина падений — «перезапусти, оно само пройдёт», релиз требует недели ручного регресса.
Практический вывод: форма — следствие архитектуры, а не идеология. У сервиса с богатой доменной логикой (расчёт тарифа, скоринг, начисление бонусов) естественно получается пирамида. У тонкого CRUD-сервиса, где логика — это в основном «сходить в базу и в соседний сервис», юнит-тесты почти бессмысленны, и правильная форма ближе к трофею.
Петли обратной связи: главная величина, которую вы оптимизируете
Смотрите на тестирование не как на набор ритуалов, а как на систему петель обратной связи разной длины. Ваша инженерная задача — ловить как можно больше проблем в левой части шкалы.
Отсюда вытекают вполне конкретные привычки, которые отличают мидла от джуна:
- Включённая проверка типов и линтер в IDE — не «чтобы код-ревью не ругался», а чтобы ошибка умерла за секунду.
- Прогон релевантного подмножества тестов локально перед пушем (
pytest tests/billing -x -q), а не «CI разберётся». - Тест, воспроизводящий баг, пишется до исправления. Иначе вы не знаете, что починили именно это.
- Если тест в CI идёт 20 минут — это не «данность», это дефект процесса, который стоит завести в бэклог.
Как выглядит хороший тест
Разница между тестом, который помогает, и тестом, который мешает, — не в инструменте. Она в четырёх свойствах: детерминизм, независимость, одна причина падать, читаемость.
Плохой тест:
def test_order():
# тест зависит от порядка выполнения, реального времени и внешней сети
svc = OrderService(db=get_global_db(), clock=datetime, http=requests)
svc.create(user_id=1, items=[{"sku": "A", "qty": 2}])
orders = svc.list_all()
assert len(orders) == 1
assert orders[0].created_at.date() == datetime.now().date()
Что здесь не так:
- Глобальная БД — тест сломается, если до него отработал другой тест, оставивший заказ (
len == 1перестанет выполняться). datetime.now()— тест упадёт при прогоне в 23:59:59.- Реальный
requests— тест зависит от чужого сервиса. - Три разных утверждения о трёх разных вещах: непонятно, что именно сломалось при падении.
- Название
test_orderне говорит ничего.
Хороший тест:
import pytest
from decimal import Decimal
def test_order_total_applies_percentage_discount(order_service, fixed_clock):
"""Скидка 10% применяется к сумме позиций до налога."""
# Arrange — явно задаём всё, что влияет на результат
order = order_service.create(
user_id=42,
items=[Item(sku="A", qty=2, price=Decimal("100.00"))],
discount_percent=10,
)
# Act
total = order_service.calculate_total(order.id)
# Assert — ровно одно утверждение о поведении
assert total == Decimal("180.00")
Разница: имя теста читается как предложение спецификации, входные данные видны прямо в тесте (не спрятаны в глобальной фикстуре), время зафиксировано, утверждение одно. Структура Arrange-Act-Assert (в BDD-варианте Given-When-Then) — не эстетика, а способ за три секунды понять упавший тест.
Отдельно про параметризацию — она резко повышает плотность полезного:
@pytest.mark.parametrize(
"amount,expected_fee",
[
(Decimal("0.00"), Decimal("0.00")), # граница снизу
(Decimal("0.01"), Decimal("0.01")), # минимальная сумма
(Decimal("999.99"), Decimal("10.00")), # до порога
(Decimal("1000.00"), Decimal("0.00")), # ровно на пороге: комиссия отменяется
(Decimal("1000.01"), Decimal("0.00")), # сразу после порога
],
)
def test_transfer_fee_at_boundaries(amount, expected_fee):
assert calculate_fee(amount) == expected_fee
Обратите внимание на выбор значений: это не случайные числа, а границы. Техника называется анализом граничных значений, и она даёт больше найденных ошибок на единицу усилий, чем любая другая. Подавляющее большинство реальных багов живёт на границах: ноль, пустой список, ровно на пороге, на единицу больше, максимальное значение типа.
Когда границы неочевидны или пространство входов большое, есть property-based тестирование: вы описываете свойство, а библиотека сама ищет контрпример. В Python это Hypothesis:
from hypothesis import given, strategies as st
@given(st.lists(st.integers()))
def test_sort_is_idempotent_and_preserves_multiset(xs):
once = my_sort(xs)
assert my_sort(once) == once # повторная сортировка ничего не меняет
assert sorted(once) == sorted(xs) # элементы не потерялись и не появились
Такой тест находит то, о чём вы не подумали: пустой список, дубликаты, отрицательные числа, -0.0. И при падении Hypothesis сам минимизирует контрпример до самого простого.
Дублёры: моки, стабы и почему их легко переборщить
Терминология Джерарда Месароша из xUnit Test Patterns, популяризованная Фаулером в «Mocks Aren’t Stubs» и «TestDouble»:
| Дублёр | Что делает | Когда нужен |
|---|---|---|
| Dummy | объект-затычка, только чтобы заполнить параметр | параметр не участвует в сценарии |
| Stub | возвращает заранее заданные ответы | нужно смоделировать ответ зависимости |
| Spy | стаб, который ещё и записывает вызовы | нужно проверить факт вызова постфактум |
| Mock | заранее заданные ожидания, падает при их нарушении | проверяем протокол взаимодействия |
| Fake | работающая упрощённая реализация (in-memory репозиторий) | нужна реальная семантика без реальной инфраструктуры |
Из этого списка самый недооценённый — fake. In-memory реализация репозитория даёт быстрые тесты с настоящей семантикой, без «а теперь замокаем семь вызовов ORM».
Главная ловушка: чрезмерное мокание превращает тест в зеркало реализации. Если тест выглядит так —
def test_create_order():
repo = Mock()
mailer = Mock()
svc = OrderService(repo, mailer)
svc.create(...)
repo.save.assert_called_once()
mailer.send.assert_called_once_with(template="order_created")
— то он проверяет не «заказ создался», а «внутри метода вызвались вот эти два метода в таком порядке». Переименуете шаблон письма при рефакторинге — тест упадёт, хотя поведение не изменилось. Такие тесты дают ложное ощущение защиты и активно мешают рефакторингу: их приходится переписывать при каждой правке.
Это старый спор двух школ: «лондонская» (mockist, изоляция каждой единицы) против «классической» (Detroit, реальные соседи, дублёры только для медленных/недетерминированных зависимостей). Рабочий компромисс, который применяют в большинстве команд:
Заменяйте дублёрами то, что медленно (сеть, внешние сервисы), недетерминированно (время, случайность, UUID) или имеет побочные эффекты вовне (отправка письма, списание денег). Всё остальное — реальное.
Для баз данных и очередей современный ответ — не моки, а Testcontainers: реальный PostgreSQL или Kafka в докере, поднимаемый на время тестов. Это медленнее моков, но проверяет то, что моки не проверяют никогда: ваш SQL, миграции, ограничения целостности, поведение транзакций.
import pytest
from testcontainers.postgres import PostgresContainer
@pytest.fixture(scope="session")
def pg_url():
# один контейнер на всю сессию: подъём стоит секунды, не делайте его на каждый тест
with PostgresContainer("postgres:16-alpine") as pg:
yield pg.get_connection_url()
Покрытие: полезная метрика, вредная цель
Покрытие кода (coverage) измеряет, какая доля строк/веток исполнилась во время прогона тестов. Полезно. Но обратите внимание на формулировку: исполнилась, а не «проверилась».
Вот тест со стопроцентным покрытием и нулевой ценностью:
def test_calculate_total():
calculate_total(order_id=1) # ни одного assert
Строки исполнились, покрытие 100%, а тест не упадёт, даже если функция начнёт возвращать случайное число. Это не выдуманный пример — так выглядят тесты в командах, где ввели KPI «покрытие не ниже 80%».
Мартин Фаулер в «TestCoverage» формулирует главное: покрытие полезно для поиска непокрытых мест, а не как цель. Низкое покрытие — надёжный сигнал проблемы. Высокое покрытие — не доказательство её отсутствия. Как только число становится KPI, оно перестаёт быть измерением (закон Гудхарта в чистом виде).
Более честная метрика — мутационное тестирование: инструмент вносит в ваш код мелкие искажения (> меняет на >=, + на -, return x на return None) и смотрит, поймают ли тесты. Не поймали — мутант «выжил», значит эта строка покрыта, но не проверена. Инструменты: PIT для JVM, Stryker для JS/C#/Scala, mutmut и cosmic-ray для Python.
# Python: покрытие с ветвлениями — минимум, который стоит включить
pytest --cov=src --cov-branch --cov-report=term-missing
# мутационное тестирование — медленно, поэтому натравливают на критический модуль
mutmut run --paths-to-mutate src/billing/
Мутационное тестирование дорого по времени, поэтому его редко гоняют на всём проекте. Разумная практика: применять к критичному ядру (расчёты, деньги, права доступа), один раз, чтобы честно понять качество своих тестов.
Флаки: самая недооценённая проблема
Flaky-тест — тест, который на неизменном коде то падает, то проходит. Это не мелкая неприятность, это разрушитель доверия ко всему набору тестов. Как только команда привыкает к «ну да, перезапусти», красный CI перестаёт что-либо значить, и настоящая поломка проезжает в прод под видом очередной флаки.
Масштаб проблемы: в Flaky Tests at Google сообщается, что около 84% переходов теста из зелёного в красное оказывались флаками, а порядка 1,5% всех тестов имеют признак нестабильности. Академический разбор причин — Luo et al., «An Empirical Analysis of Flaky Tests» (FSE 2014): лидируют асинхронное ожидание, зависимость от порядка тестов и конкурентность.
Типичные источники и что с ними делать:
| Причина | Как выглядит | Лечение |
|---|---|---|
| Реальное время | падает в полночь, в конце месяца, при переходе на летнее время | инжектируйте часы (clock как зависимость), фиксируйте время в тесте |
sleep() в ожидании |
«обычно 200 мс хватает», на загруженном раннере — нет | явное ожидание условия с таймаутом, а не фиксированная пауза |
| Зависимость от порядка | проходит поодиночке, падает в наборе | изолируйте состояние; проверьте прогоном в случайном порядке (pytest -p randomly) |
| Общее состояние | глобальные переменные, кэш, singleton, общая БД | откат транзакции после теста, свежая схема, отдельная база на воркер |
| Конкурентность | гонка, которая проявляется раз в 50 прогонов | не «добавить sleep», а разобраться в синхронизации |
| Сеть/внешние сервисы | «упал, потому что чужой стенд лежал» | дублёры или контрактные тесты вместо реального вызова |
| Неупорядоченные коллекции | сравнение списка, порядок которого не гарантирован | сравнивайте множества или сортируйте перед сравнением |
Рабочий процесс с флаками, который используют зрелые команды: тест, пойманный на нестабильности, немедленно карантинится (выносится из блокирующего прогона) и на него заводится тикет с владельцем и сроком. Не «игнорируем», не «пусть висит красным» — карантин плюс обязательство починить. Если тикет не чинится месяц, тест удаляется: нестабильный тест хуже отсутствующего, потому что он ещё и тратит время людей.
Отдельно: автоматический ретрай — это анестезия, а не лечение. Он допустим как временная мера с обязательным логированием факта ретрая и метрикой «сколько тестов прошли только со второй попытки». Если эта метрика не отслеживается, ретраи просто прячут гниль.
Где тесты живут в пайплайне
Полный набор тестов почти никогда не гоняют на каждый коммит: слишком долго. Тесты раскладывают по стадиям, балансируя скорость и уверенность.
затронутых модулей] C --> D[Push, открыт Pull Request] D --> E[CI на PR: быстрый набор
цель — до 10 минут] E --> E1[статический анализ + SAST] E --> E2[все юнит-тесты] E --> E3[интеграционные на Testcontainers] E --> E4[контрактные тесты] E1 & E2 & E3 & E4 --> F{Зелено и есть аппрув?} F -->|нет| A F -->|да| G[Merge в основную ветку] G --> H[CI на main: сборка артефакта
+ полный набор] H --> I[Деплой на тестовый стенд] I --> J[E2E критических сценариев] J --> K{Зелено?} K -->|нет| L[Откат или фикс вперёд
main должен быть релизопригоден] K -->|да| M[Кандидат на релиз] N[Ночью: нагрузочные,
долгие E2E, security-скан,
прогон в случайном порядке] -.-> M M --> O[Приёмка и релиз] style E fill:#4f9d69,fill-opacity:0.25 style H fill:#b08a3e,fill-opacity:0.25 style N fill:#4a7fb5,fill-opacity:0.25
Ключевые практические договорённости, которые стоит знать:
- Бюджет времени на PR-прогон. Общепринятый ориентир — 10 минут. Дальше человек уходит из контекста, начинает параллелить задачи и качество ревью падает. Если не влезаете — выносите медленное в ночной прогон.
- Основная ветка всегда релизопригодна. Красный main — это не «потом починим», это остановка работы всей команды. Во многих командах действует правило: тот, кто сломал main, чинит его немедленно и ничего другого не делает.
- Ночные прогоны — для того, что долго и не блокирует: нагрузочные тесты, полный E2E, сканирование зависимостей на уязвимости, прогон в случайном порядке для отлова зависимостей между тестами.
Как всё это устроено внутри CI-системы, детально разобрано в треке DevOps: https://courses.digitable.life/post/devops/01-ci-fundamentals/ и https://courses.digitable.life/post/devops/04-cd-and-release-strategies/.
Минимальный пример конфигурации, чтобы было понятно, как это выглядит в жизни:
# .github/workflows/pr.yml — быстрый набор на каждый Pull Request
name: pr-checks
on: pull_request
jobs:
fast:
runs-on: ubuntu-latest
timeout-minutes: 15 # защита от зависшего прогона
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12", cache: pip }
- run: pip install -r requirements-dev.txt
- run: ruff check . # линтер: секунды
- run: mypy src/ # типы: десятки секунд
- run: pytest tests/unit -q --cov=src --cov-branch
- run: pytest tests/integration -q # поднимает Testcontainers
# E2E здесь НЕТ намеренно: они уезжают в прогон на main и в ночной
Тестовые окружения и данные: где обычно больно
Про уровни тестов пишут все. Про то, что реально съедает время, — почти никто.
Окружения. Классический набор: локальное → dev → test/QA → staging (максимально похож на прод) → production. В энтерпрайзе стендов бывает больше, у них есть расписание и владельцы, и главная проблема звучит так: «стенд занят соседней командой», «на стенде развёрнута другая версия смежного сервиса», «на стенде вторую неделю лежит интеграция с внешней системой». Ожидание стенда — одна из самых частых причин простоя задачи, о которой не пишут в методичках.
Современный ответ на это — эфемерные окружения: на каждый PR поднимается изолированный экземпляр приложения, живущий до мержа. Дорого по инфраструктуре, но убирает целый класс конфликтов.
Данные. Три стратегии, у каждой цена:
- Синтетические данные, создаваемые тестом. Самый чистый вариант: тест сам создаёт всё, что ему нужно, и убирает за собой. Тест самодостаточен и читаем. Минус — не ловит проблемы, которые проявляются только на «грязных» реальных данных.
- Общий набор фикстур (seed). Быстро, но со временем превращается в неприкасаемый монолит: никто не знает, кто зависит от
user_id=1, и любое изменение ломает десятки тестов. - Копия продовых данных. Единственный способ поймать реальность — и одновременно юридическая мина. Персональные данные в тестовом контуре — это нарушение (в РФ — 152-ФЗ, в ЕС — GDPR), а тестовые контуры защищены заведомо хуже прода. Обязательна анонимизация/маскирование при выгрузке, и делается она автоматически, а не «Вася выгрузил дамп себе на ноут».
Правило, которое стоит запомнить сразу: никогда не тяните дамп прода на локальную машину. Это самый частый способ джуна случайно устроить компании утечку. Если нужны реальные данные — есть процедура и обезличенная выгрузка, спросите тимлида.
Проверка своей задачи перед передачей
Момент, который отличает разработчика, с которым приятно работать, от того, чьи задачи возвращаются по три раза. Прежде чем перевести задачу в «Готово к тестированию», пройдите по своему изменению руками — не по коду, а по продукту.
Разумный личный чек-лист:
- Прошёл основной сценарий из описания задачи от начала до конца в интерфейсе (не только через unit-тест).
- Прошёл один-два негативных сценария: пустой ввод, слишком длинная строка, отмена посередине, двойное нажатие кнопки.
- Проверил, что старое поведение не сломалось: то, что рядом на этом же экране/эндпоинте.
- Посмотрел, что попало в логи: нет ли туда утечки персональных данных, токенов, полного тела запроса.
- Проверил миграцию БД: применяется на копии реальных данных, есть план отката, не блокирует таблицу надолго.
- Убедился, что все критерии приёмки из тикета выполнены — буквально, по пунктам.
- Написал в комментарии к задаче, что и как проверять: где смотреть, какие данные подготовить, какие крайние случаи важны.
Последний пункт — самый недооценённый. Тестировщик не читает ваш код и не сидел на обсуждении в вашей голове. Комментарий вида «Изменил расчёт скидки. Смотреть в корзине, важен случай, когда позиций больше 10 и есть промокод — там раньше применялось дважды» экономит обеим сторонам полдня.
Ручное и исследовательское тестирование
Существует устойчивый миф, что ручное тестирование — устаревшая деятельность, которую вот-вот вытеснит автоматизация. Это недоразумение, происходящее из смешения двух разных вещей. Джеймс Бах и Майкл Болтон различают testing и checking:
- Checking — проверка известного утверждения. «Если сумма 1000, комиссия 0». Это можно и нужно автоматизировать: машина делает это быстрее, дешевле и не устаёт.
- Testing — исследование: попытка выяснить, что вообще происходит с продуктом, включая то, о чём никто не подумал. Это принципиально человеческая деятельность.
Автоматизация не находит нового. Автоматический тест проверяет ровно то ожидание, которое вы в него заложили. Никакой набор автотестов не скажет вам «этот флоу неудобен», «здесь двусмысленная формулировка ошибки», «а если открыть две вкладки?».
Исследовательское тестирование делается не хаотично: обычно берётся хартия (charter) — короткая формулировка «что исследуем и с какой целью», ставится таймбокс на 45–90 минут, ведутся заметки. Разработчику полезно уметь это самому — потратить час на «поиграть с собственной фичей злонамеренно» находит удивительно много.
Баг-репорт и жизненный цикл дефекта
Когда проблема найдена, она превращается в тикет. Хороший баг-репорт содержит:
- Шаги воспроизведения — пронумерованные, с конкретными данными, а не «оформить заказ».
- Ожидаемый результат и фактический результат — отдельно.
- Окружение: стенд, версия/коммит, браузер, роль пользователя.
- Доказательства: скриншот, видео, кусок лога,
trace_idзапроса. - Воспроизводимость: всегда или, скажем, 1 из 5 раз (это критично для диагностики).
«Не работает оплата» — это не баг-репорт, это сообщение о настроении. Требуйте нормальных репортов и сами пишите такие же, когда заводите баг на смежную команду.
Дальше у дефекта есть жизненный цикл. Названия статусов различаются, суть одинакова:
Три статуса здесь заслуживают особого внимания, потому что вокруг них происходит большая часть конфликтов.
Rejected / «не баг, а фича». Иногда это правда, иногда — попытка избежать работы. Здоровый способ разрешать спор — вернуться к критериям приёмки и требованиям (https://courses.digitable.life/post/sdlc-and-career/02-requirements/). Если поведение противоречит требованиям — это баг. Если требования молчат — это не баг, это пробел в требованиях, и решать должен владелец продукта, а не самый громкий участник спора. Плохой признак в команде: разработчик закрывает баги как Rejected в одиночку, без обсуждения.
Deferred. Осознанное «признаём, но не чиним сейчас» — нормальная взрослая практика. Опасность в том, что Deferred легко становится кладбищем: тикет уходит туда и никогда не возвращается. Хорошая гигиена — регулярный (раз в квартал) пересмотр отложенных дефектов; часть из них к этому моменту стоит просто закрыть честно, а не делать вид, что когда-нибудь займётесь.
Reopened. Это метрика качества вашей работы. Высокий процент переоткрытых багов означает: чините симптом, а не причину, и не проверяете исправление сами.
Severity и Priority: почему это разные вещи
Постоянный источник путаницы у новичков.
- Severity (серьёзность) — техническая характеристика: насколько сильно ломается система. Обычно оценивает тот, кто нашёл.
- Priority (приоритет) — бизнес-решение: насколько срочно чинить. Оценивает владелец продукта или тимлид.
Они не совпадают, и это нормально:
Классические примеры: падение сервиса при использовании функции, которой пользуются два человека в год, — высокая серьёзность, низкий приоритет. Опечатка в названии компании на главной странице — минимальная серьёзность, максимальный приоритет.
Приёмка: кто и на каком основании говорит «годится»
Тестирование отвечает на вопрос «работает ли так, как задумано». Приёмка отвечает на другой: «то ли задумали и берём ли мы это».
Здесь важно не путать два понятия, о которых шла речь в https://courses.digitable.life/post/sdlc-and-career/04-development/:
- Acceptance Criteria (критерии приёмки) — свои для каждой задачи. «Пользователь с истёкшей картой видит понятное сообщение и предложение обновить карту.» Их формулирует аналитик или владелец продукта, желательно до начала разработки.
- Definition of Done — общий для всех задач в команде. «Код отревьюен, тесты написаны и зелёные, документация обновлена, фича за флагом, метрики добавлены.» Это про инженерную зрелость, а не про конкретную функциональность.
Задача считается сделанной, только когда выполнены оба набора условий. Классическая ошибка джуна — «функционал работает, значит готово», при том что не написано ни одного теста и не обновлён README.
Как это выглядит в жизни:
Обратите внимание на последнюю стрелку. Настоящая приёмка происходит не когда владелец продукта сказал «ок», а когда пользователи начали пользоваться. Именно поэтому современные команды не разделяют жёстко «протестировали и забыли» — за релизом идут метрики и наблюдение, о чём подробно в https://courses.digitable.life/post/sdlc-and-career/06-release-and-operations/.
UAT: приёмка в энтерпрайзе и аутсорсе
В продуктовой компании приёмку делает владелец продукта, и это занимает полчаса. В аутсорсе и энтерпрайзе есть отдельная стадия — UAT (User Acceptance Testing), приёмочные испытания, которые проводит заказчик или его представители — реальные сотрудники бизнес-подразделения.
Что важно понимать про UAT:
- Это юридически значимая процедура. По результатам подписывается акт, от которого зависит оплата этапа. Из-за этого UAT — самая политически заряжённая точка проекта: заказчик может использовать «непринятие» как рычаг в переговорах о цене или объёме.
- Проверяют его не тестировщики, а предметные специалисты — бухгалтеры, операционисты, врачи. Они не знают, что «так задумано», и находят вещи, которые команда считала очевидными.
- Программа и методика испытаний (в госсекторе — по ГОСТ 34) описывается заранее, и там же фиксируется, что считается успешным прохождением. Если программу не согласовали заранее, приёмка превращается в бесконечный поток «а ещё хотелось бы».
- Для разработчика UAT означает: вопросы прилетают напрямую и срочно, и отвечать на них надо на языке предметной области, а не «там просто на бэкенде нужный флаг не проставился».
Подробнее про формальные процессы согласования — в https://courses.digitable.life/post/sdlc-and-career/10-enterprise/, а про типы проектов и специфику аутсорса — в https://courses.digitable.life/post/sdlc-and-career/09-project-types/.
Регресс и релизное тестирование
Регрессионное тестирование — проверка, что новое не сломало старое. Это то место, где две модели разработки расходятся сильнее всего.
Энтерпрайз-подход. Есть регресс-набор — сотни или тысячи тест-кейсов, часть автоматизирована, часть прогоняется руками. Перед релизом объявляется code freeze: в релизную ветку принимаются только исправления найденных дефектов. Регресс идёт от нескольких дней до нескольких недель.
Плюсы: высокая уверенность, формальный протокол, подходит для систем, где ошибка стоит очень дорого (расчёт зарплат, клиринг, медицинское оборудование). Минусы: релизы редкие и оттого крупные, а крупный релиз рискованнее серии мелких — при поломке непонятно, какое из 200 изменений виновато. Плюс к концу регресса часть проверенного уже устарела.
Продуктовый/стартап-подход. Регресс автоматизирован и вписан в пайплайн, а дополнительную уверенность дают не проверки перед релизом, а способ выкатки: feature flags (фича в коде, но выключена), канареечный релиз (на 1% трафика), автоматический откат по метрикам.
Плюсы: релиз десятки раз в день, маленькие изменения, мгновенный откат. Минусы: требует зрелой инфраструктуры и наблюдаемости; часть проблем находят пользователи (хоть и небольшая их доля).
Про механику — Feature Toggles и CanaryRelease у Фаулера, плюс исследовательская база DORA, которая последовательно показывает: команды с частыми маленькими релизами имеют и более высокую скорость, и более низкий процент отказов. Это контринтуитивно и потому важно: «чаще выкатывать» и «стабильнее» — не противоположности.
Обратите внимание на нижнюю строку: откат — это не катастрофа и не позор, а штатная операция. В командах, где откат считается ЧП, люди начинают скрывать проблемы, и это гораздо хуже.
Нефункциональное тестирование: что ещё бывает
Кроме «работает ли функция», есть характеристики, которые ломаются иначе и проверяются отдельно:
- Нагрузочное / производительность. Сколько запросов в секунду держим, что с задержкой на 99-м перцентиле, где сломается первым. Инструменты: k6, Gatling, JMeter, Locust. Ключевой момент: смотрите не на среднее, а на хвост распределения — среднее время ответа скрывает, что каждый сотый пользователь ждёт 8 секунд.
- Стресс и деградация. Что происходит, когда нагрузка выше проектной: система деградирует плавно или падает целиком, восстанавливается ли сама.
- Безопасность. SAST (анализ кода), DAST (атака на работающее приложение), проверка зависимостей на известные уязвимости, периодический пентест. Ориентир по типовым проблемам — OWASP Top 10. Подробнее — https://courses.digitable.life/post/devops/17-security-in-pipeline/.
- Доступность (accessibility). Работает ли с клавиатуры, читается ли скринридером, достаточен ли контраст. В части юрисдикций и для госзаказа — обязательное требование, а не пожелание.
- Совместимость. Браузеры, версии ОС, размеры экранов, старые версии мобильного приложения, которые пользователи не обновляют годами.
- Локализация. Длина строк в немецком, направление письма в арабском, форматы дат и десятичных разделителей, часовые пояса.
- Обновление и миграции. Отдельная категория, про которую забывают: приложение проверили с нуля, а сценарий «обновление со старой версии с существующими данными» — нет. Это регулярный источник аварий на релизе.
Как разработчику: вас не просят быть экспертом во всём этом. Вас просят помнить, что эти категории существуют, и задавать вопрос «а нагрузку эта штука выдержит?» до, а не после релиза.
Энтерпрайз против стартапа: сводка
| Аспект | Энтерпрайз / банк / госзаказ | Стартап / продуктовая команда |
|---|---|---|
| Кто тестирует | отдельный отдел QA, часто на аутсорсе | сама команда, иногда один QA на несколько команд |
| Формализация | тест-планы, тест-кейсы, протоколы испытаний | чек-листы, автотесты, заметки в тикете |
| Регресс | дни-недели, значительная доля вручную | минуты-часы, автоматизирован |
| Приёмка | UAT с актом, комиссии, ГОСТ 34 в госсекторе | демо владельцу продукта, A/B-тест на пользователях |
| Частота релизов | недели-кварталы, релизные окна | десятки раз в день |
| Ошибка в проде | инцидент с разбором, иногда регуляторные последствия | откат за минуты, постмортем без поиска виновных |
| Тестовые данные | обезличенные выгрузки, отдельные контуры, строгий доступ | генерация на лету, иногда прод-подобная песочница |
| Чему научитесь | дисциплине, работе со сложными интеграциями, ответственности за деньги | автоматизации, инфраструктуре, быстрым петлям, тестированию в проде |
| Чем рискуете | привыкнуть, что «качество — не моя зона» | привыкнуть, что «проверим в проде», и обжечься на первой серьёзной системе |
Ни то ни другое не «правильнее» — это разные ответы на разную цену ошибки. Полезно за карьеру попробовать оба: банковская дисциплина без стартапной скорости даёт медлительного инженера, стартапная скорость без дисциплины — инженера, которого нельзя пускать к деньгам.
Типичные ошибки, которые делают почти все
- Писать тесты после того, как код «готов», в последний час перед дедлайном. Такие тесты подгоняются под реализацию и не проверяют ничего, кроме того, что код делает то, что делает.
- Мокать всё подряд. Получаются тесты, которые проверяют моки и разваливаются при любом рефакторинге.
- Гнаться за процентом покрытия. Приводит к тестам без ассертов и к покрытию геттеров.
- Терпеть флаки. Разрушает доверие к CI быстрее, чем что-либо ещё.
- Один огромный E2E-тест на весь бизнес-процесс. Падает по любому поводу, не говорит, что сломалось, чинится час.
- Тесты, зависящие друг от друга по порядку. Работают, пока кто-то не запустит прогон параллельно.
- Не воспроизводить баг тестом перед исправлением. Половина «исправлений» после этого чинит не ту проблему.
- Передавать задачу в тест без описания, что проверять. Гарантированный возврат тикета и потерянный день.
- Спорить о статусе тикета вместо разговора о продукте. «Это не баг» без ссылки на требования — не аргумент.
- Считать, что после мержа задача закончилась. Она заканчивается, когда работает у пользователей и не сломала метрики.
- Тестировать только счастливый путь. Реальные пользователи вводят эмодзи в поле «имя», жмут «назад» посередине оплаты и открывают три вкладки.
- Тянуть прод-данные к себе. Юридический риск для компании и репутационный для вас.
Чего от вас ждут по грейдам
Это один из самых явных срезов, по которому определяют уровень инженера (подробнее — https://courses.digitable.life/post/sdlc-and-career/12-grades/):
- Junior. Пишет тесты на свой код, когда попросят или когда есть шаблон рядом. Умеет воспроизвести баг по внятному репорту. Не ломает то, что рядом. Проверяет свою задачу руками перед сдачей.
- Middle. Сам решает, какие тесты нужны и на каком уровне, не спрашивая. Пишет тест на баг до исправления. Замечает и чинит флаки. Не мокает лишнее. Аргументирует, почему на эту задачу хватит интеграционного теста, а на ту нужен E2E.
- Senior. Отвечает за стратегию тестирования в своей области: где проходит граница между уровнями, что уезжает в ночной прогон, какой бюджет времени у PR-прогона. Проектирует код так, чтобы его было легко тестировать (внедрение зависимостей, отделение чистой логики от ввода-вывода). Замечает системную проблему («мы третий раз ловим этот класс ошибок в проде — значит, у нас дыра на уровне контрактов»).
- Lead. Договаривается о качестве с бизнесом: сколько мы готовы платить за уверенность, какой уровень дефектов приемлем, что делаем при конфликте «срок против качества».
На собеседовании про тестирование спрашивают почти всегда, и лучшие ответы — не определения из глоссария, а истории: «у нас был флаки-тест, который падал раз в двадцать прогонов, оказалось — зависимость от часового пояса, вот как нашли». Про подготовку к таким разговорам — https://courses.digitable.life/post/sdlc-and-career/16-interview-candidate/.
Мини-итог
- Тесты нужны в первую очередь не для поиска ошибок, а чтобы код можно было безопасно менять.
- Модель организации QA сильно различается между компаниями; при выборе работы это стоит выяснять явно, как и стек.
- Уровни тестов различаются не названиями, а тем, что тесту разрешено трогать, — и отсюда его скоростью и качеством диагностики.
- Форма набора (пирамида, трофей) выводится из архитектуры, а не выбирается по моде. Рожок мороженого получается сам собой, если тесты откладывать.
- Оптимизируйте длину петли обратной связи: ловить ошибку в IDE за секунду в сотни раз дешевле, чем в проде через неделю.
- Покрытие — сигнал, а не цель. Настоящее качество тестов честнее показывает мутационное тестирование.
- Флаки — не мелочь, а разрушитель доверия ко всей системе проверок. Карантин плюс тикет с владельцем, а не «перезапусти».
- Баг-репорт без шагов воспроизведения — не баг-репорт. Severity и Priority — разные вещи и оцениваются разными людьми.
- Приёмка отвечает на вопрос «то ли мы сделали», а не «работает ли». UAT в энтерпрайзе — юридически значимая процедура, а не формальность.
- Задача заканчивается не мержем, а тем, что она работает у пользователей.
Что почитать дальше
- Мартин Фаулер — TestPyramid, Mocks Aren’t Stubs, TestCoverage, Feature Toggles
- Хам Фокке — The Practical Test Pyramid
- Google Testing Blog — Just Say No to More End-to-End Tests, Flaky Tests at Google, Test Sizes
- Джерард Месарош — xUnit Test Patterns (каталог паттернов и запахов тестов)
- Майкл Физерс — «Working Effectively with Legacy Code» (как вводить тесты туда, где их нет)
- Владимир Хориков — «Unit Testing Principles, Practices, and Patterns» (лучший современный разбор, что делает тест ценным)
- Джеймс Бах и Майкл Болтон — Testing and Checking Refined
- Testcontainers, Hypothesis, Pact — рабочие инструменты
- DORA — исследовательская база про связь частоты релизов и стабильности
- Глоссарий ISTQB — если нужна каноническая терминология
Что дальше
Код написан, проверен и принят. Теперь его надо доставить пользователям — и потом с ним жить: следить, дежурить, разбирать инциденты и чинить в три часа ночи. Об этом следующая статья: Релиз и эксплуатация: деплой, мониторинг, дежурства, инциденты.