Как делают софт и карьера Тестирование и приёмка глазами разработчика
0%

Тестирование и приёмка глазами разработчика

Тестирование и приёмка глазами разработчика

В предыдущей статье — 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», а разобраться в синхронизации
Сеть/внешние сервисы «упал, потому что чужой стенд лежал» дублёры или контрактные тесты вместо реального вызова
Неупорядоченные коллекции сравнение списка, порядок которого не гарантирован сравнивайте множества или сортируйте перед сравнением

Рабочий процесс с флаками, который используют зрелые команды: тест, пойманный на нестабильности, немедленно карантинится (выносится из блокирующего прогона) и на него заводится тикет с владельцем и сроком. Не «игнорируем», не «пусть висит красным» — карантин плюс обязательство починить. Если тикет не чинится месяц, тест удаляется: нестабильный тест хуже отсутствующего, потому что он ещё и тратит время людей.

Отдельно: автоматический ретрай — это анестезия, а не лечение. Он допустим как временная мера с обязательным логированием факта ретрая и метрикой «сколько тестов прошли только со второй попытки». Если эта метрика не отслеживается, ретраи просто прячут гниль.

Где тесты живут в пайплайне

Полный набор тестов почти никогда не гоняют на каждый коммит: слишком долго. Тесты раскладывают по стадиям, балансируя скорость и уверенность.

Ключевые практические договорённости, которые стоит знать:

  • Бюджет времени на 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 поднимается изолированный экземпляр приложения, живущий до мержа. Дорого по инфраструктуре, но убирает целый класс конфликтов.

Данные. Три стратегии, у каждой цена:

  1. Синтетические данные, создаваемые тестом. Самый чистый вариант: тест сам создаёт всё, что ему нужно, и убирает за собой. Тест самодостаточен и читаем. Минус — не ловит проблемы, которые проявляются только на «грязных» реальных данных.
  2. Общий набор фикстур (seed). Быстро, но со временем превращается в неприкасаемый монолит: никто не знает, кто зависит от user_id=1, и любое изменение ломает десятки тестов.
  3. Копия продовых данных. Единственный способ поймать реальность — и одновременно юридическая мина. Персональные данные в тестовом контуре — это нарушение (в РФ — 152-ФЗ, в ЕС — GDPR), а тестовые контуры защищены заведомо хуже прода. Обязательна анонимизация/маскирование при выгрузке, и делается она автоматически, а не «Вася выгрузил дамп себе на ноут».

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

Проверка своей задачи перед передачей

Момент, который отличает разработчика, с которым приятно работать, от того, чьи задачи возвращаются по три раза. Прежде чем перевести задачу в «Готово к тестированию», пройдите по своему изменению руками — не по коду, а по продукту.

Разумный личный чек-лист:

  • Прошёл основной сценарий из описания задачи от начала до конца в интерфейсе (не только через unit-тест).
  • Прошёл один-два негативных сценария: пустой ввод, слишком длинная строка, отмена посередине, двойное нажатие кнопки.
  • Проверил, что старое поведение не сломалось: то, что рядом на этом же экране/эндпоинте.
  • Посмотрел, что попало в логи: нет ли туда утечки персональных данных, токенов, полного тела запроса.
  • Проверил миграцию БД: применяется на копии реальных данных, есть план отката, не блокирует таблицу надолго.
  • Убедился, что все критерии приёмки из тикета выполнены — буквально, по пунктам.
  • Написал в комментарии к задаче, что и как проверять: где смотреть, какие данные подготовить, какие крайние случаи важны.

Последний пункт — самый недооценённый. Тестировщик не читает ваш код и не сидел на обсуждении в вашей голове. Комментарий вида «Изменил расчёт скидки. Смотреть в корзине, важен случай, когда позиций больше 10 и есть промокод — там раньше применялось дважды» экономит обеим сторонам полдня.

Ручное и исследовательское тестирование

Существует устойчивый миф, что ручное тестирование — устаревшая деятельность, которую вот-вот вытеснит автоматизация. Это недоразумение, происходящее из смешения двух разных вещей. Джеймс Бах и Майкл Болтон различают testing и checking:

  • Checking — проверка известного утверждения. «Если сумма 1000, комиссия 0». Это можно и нужно автоматизировать: машина делает это быстрее, дешевле и не устаёт.
  • Testing — исследование: попытка выяснить, что вообще происходит с продуктом, включая то, о чём никто не подумал. Это принципиально человеческая деятельность.

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

Исследовательское тестирование делается не хаотично: обычно берётся хартия (charter) — короткая формулировка «что исследуем и с какой целью», ставится таймбокс на 45–90 минут, ведутся заметки. Разработчику полезно уметь это самому — потратить час на «поиграть с собственной фичей злонамеренно» находит удивительно много.

Баг-репорт и жизненный цикл дефекта

Когда проблема найдена, она превращается в тикет. Хороший баг-репорт содержит:

  1. Шаги воспроизведения — пронумерованные, с конкретными данными, а не «оформить заказ».
  2. Ожидаемый результат и фактический результат — отдельно.
  3. Окружение: стенд, версия/коммит, браузер, роль пользователя.
  4. Доказательства: скриншот, видео, кусок лога, trace_id запроса.
  5. Воспроизводимость: всегда или, скажем, 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-тест на пользователях
Частота релизов недели-кварталы, релизные окна десятки раз в день
Ошибка в проде инцидент с разбором, иногда регуляторные последствия откат за минуты, постмортем без поиска виновных
Тестовые данные обезличенные выгрузки, отдельные контуры, строгий доступ генерация на лету, иногда прод-подобная песочница
Чему научитесь дисциплине, работе со сложными интеграциями, ответственности за деньги автоматизации, инфраструктуре, быстрым петлям, тестированию в проде
Чем рискуете привыкнуть, что «качество — не моя зона» привыкнуть, что «проверим в проде», и обжечься на первой серьёзной системе

Ни то ни другое не «правильнее» — это разные ответы на разную цену ошибки. Полезно за карьеру попробовать оба: банковская дисциплина без стартапной скорости даёт медлительного инженера, стартапная скорость без дисциплины — инженера, которого нельзя пускать к деньгам.

Типичные ошибки, которые делают почти все

  1. Писать тесты после того, как код «готов», в последний час перед дедлайном. Такие тесты подгоняются под реализацию и не проверяют ничего, кроме того, что код делает то, что делает.
  2. Мокать всё подряд. Получаются тесты, которые проверяют моки и разваливаются при любом рефакторинге.
  3. Гнаться за процентом покрытия. Приводит к тестам без ассертов и к покрытию геттеров.
  4. Терпеть флаки. Разрушает доверие к CI быстрее, чем что-либо ещё.
  5. Один огромный E2E-тест на весь бизнес-процесс. Падает по любому поводу, не говорит, что сломалось, чинится час.
  6. Тесты, зависящие друг от друга по порядку. Работают, пока кто-то не запустит прогон параллельно.
  7. Не воспроизводить баг тестом перед исправлением. Половина «исправлений» после этого чинит не ту проблему.
  8. Передавать задачу в тест без описания, что проверять. Гарантированный возврат тикета и потерянный день.
  9. Спорить о статусе тикета вместо разговора о продукте. «Это не баг» без ссылки на требования — не аргумент.
  10. Считать, что после мержа задача закончилась. Она заканчивается, когда работает у пользователей и не сломала метрики.
  11. Тестировать только счастливый путь. Реальные пользователи вводят эмодзи в поле «имя», жмут «назад» посередине оплаты и открывают три вкладки.
  12. Тянуть прод-данные к себе. Юридический риск для компании и репутационный для вас.

Чего от вас ждут по грейдам

Это один из самых явных срезов, по которому определяют уровень инженера (подробнее — 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 в энтерпрайзе — юридически значимая процедура, а не формальность.
  • Задача заканчивается не мержем, а тем, что она работает у пользователей.

Что почитать дальше

Что дальше

Код написан, проверен и принят. Теперь его надо доставить пользователям — и потом с ним жить: следить, дежурить, разбирать инциденты и чинить в три часа ночи. Об этом следующая статья: Релиз и эксплуатация: деплой, мониторинг, дежурства, инциденты.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов