Автотесты — инструмент команды, а не отдельный проект тестировщика
--tasker-version 1.1. Тестовый процесс в целом (план, отчёт, регресс вручную) разобран в разделе 11 курса ручного тестирования.Стратегия отвечает на три вопроса: что проверяем автотестами, на каком уровне пирамиды и в каком порядке это делаем. Её пишут на страницу-две и пересматривают раз в несколько выпусков.
| Порядок | Что автоматизировать | Почему первым |
|---|---|---|
| 1 | Smoke: вход, главная операция, главная страница | Минимум кода, а ответ «сборка вообще жива?» нужен на каждый коммит |
| 2 | Правила и расчёты — модульными тестами | Дёшево, быстро, много вариантов данных; дефекты расчётов — самые дорогие |
| 3 | API: операции, негативные случаи, права доступа | Стабильно, не зависит от вёрстки; права доступа — безопасность |
| 4 | Регресс по найденным дефектам: тест на каждый исправленный | Исправленный однажды дефект любит возвращаться |
| 5 | Главные сценарии в браузере — немного | Проверяют, что части собраны вместе; дороги в поддержке |
Что сознательно оставляют вручную: удобство, внешний вид, исследовательские проходы, редкие сценарии. Это тоже записывают в стратегию — иначе каждый новый человек в команде будет предлагать «автоматизировать всё».
Новая версия — повод прогнать весь набор. Пример — вторая версия shop.py. В её описании изменений одна строка: «Бесплатная доставка — от 2500 ₽ вместо 3000 ₽». На первой версии набор раздела 03 зелёный:
$ pytest -q
......... [100%]
9 passed in 0.13s
На второй:
$ pytest -q --tb=line
.F....F.. [100%]
=================================== FAILURES ===================================
E assert 0 == 300
+ where 0 = <function delivery at 0x7efd342f4860>(2999)
+ where <function delivery at 0x7efd342f4860> = shop.delivery
~/qa/tests/test_delivery.py:9: assert 0 == 300
E ValueError: could not convert string to float: '1 250.50'
~/qa/shop.py:30: ValueError: could not convert string to float: '1 250.50'
=========================== short test summary info ============================
FAILED tests/test_delivery.py::test_spb_below_3000 - assert 0 == 300
FAILED tests/test_parse.py::test_parse_with_space_and_comma - ValueError: cou...
2 failed, 7 passed in 0.12s
Два падения — и это два разных случая:
| Тест | Причина | Что делать |
|---|---|---|
test_spb_below_3000: доставка 0 вместо 300 при сумме 2999 | Требование изменилось: с 2500 ₽ доставка бесплатна. Тест устарел | Обновить тест по новому требованию: границы 2499 и 2500 |
test_parse_with_space_and_comma: строка «1 250,50» не разбирается | В описании изменений про суммы ничего нет. Сломали то, что работало, — регрессия | Баг-репорт; тест не трогать |
Правило разбора: каждое падение сверяют с описанием изменений и требованиями. Если изменение заявлено — обновляют тест (и это тоже проверка: тест должен упасть там, где ожидали). Если нет — это регрессия, даже если «новое поведение тоже логичное». Обновлять тест «чтобы позеленел», не выяснив причину, — худшее, что можно сделать с набором.
Критерии готовности (Definition of Done, DoD) — список, который должен выполниться, чтобы задачу считали сделанной. Пункты про тесты — в каждом DoD команды, которая всерьёз занимается качеством:
skip и xfail без баг-репортов нет;DoD делает автотесты частью работы, а не «потом, если останется время». Если тесты пишутся отдельно и позже, они всегда отстают от кода.
Тест — тоже код, и в нём бывают ошибки. Самая опасная — тест, который проходит всегда, даже на сломанной программе. Чек-лист ревью:
| Вопрос | Плохой признак |
|---|---|
| Тест вообще что-то проверяет? | Нет assert или expect; проверка assert r вместо кода и тела |
| Тест может упасть? | Ожидание вычислено тем же кодом, что проверяется; ожидание переписано из ответа программы |
| Ожидание обосновано? | Не ссылается ни на требование, ни на документацию |
| Тест независим? | Меняет общие данные без уборки; зависит от порядка и даты |
| Тест стабилен? | sleep, чтение текста без expect, CSS-цепочки |
| По имени понятно, что сломалось? | test_1, test_tasks, один тест на десять проверок |
| Пропуски объяснены? | skip и xfail без reason и номера баг-репорта |
Простой способ проверить, что тест может упасть: временно сломать проверяемое (поменять >= на >, ожидаемое значение) и убедиться, что тест покраснел. Так поступают со всеми тестами на дефекты: тест, который не падал на дефектной версии, ничего не доказывает.
| Метрика | Полезна для | Ловушка |
|---|---|---|
| Число автотестов | Видно, растёт ли набор | Сделать цель — получите сотни тестов без проверок |
| Покрытие кода | Найти непроверенный код | Выполненный код ≠ проверенный (раздел 10) |
| Доля зелёных прогонов | Здоровье конвейера | Достигается удалением и skip «неудобных» тестов |
| Время прогона | Удобство: длинный прогон перестают запускать | Ускорение ценой удаления браузерных проверок |
| Доля плавающих тестов | Доверие к набору | Скрывается автоматическим перезапуском |
| Дефекты, найденные пользователями | Итог работы всей команды по качеству | Растёт с числом пользователей — сравнивать долю, а не штуки |
Закон Гудхарта: метрика, ставшая целью, перестаёт быть хорошей метрикой. Метрики смотрят вместе и обсуждают причины, а не ставят план «покрытие 90% к концу квартала».
Функциональные автотесты отвечают «правильно ли работает». Нагрузочные — «как работает, когда пользователей много»: время ответа, сколько запросов в секунду выдерживает, когда начинаются ошибки.
| Вид | Вопрос |
|---|---|
| Нагрузочное | Укладывается ли время ответа в требования при ожидаемом числе пользователей? |
| Стрессовое | При какой нагрузке система ломается и как она восстанавливается? |
| На стабильность (длительное) | Не растёт ли память и время ответа за часы работы? |
Главные показатели: время ответа по процентилям (медиана и 95-й процентиль: «95% запросов быстрее N мс»), пропускная способность (запросов в секунду) и доля ошибок. Среднее время почти бесполезно: десять быстрых ответов и один в 10 секунд дают «нормальное» среднее.
Locust — инструмент нагрузочного тестирования на Python: поведение пользователя описывают классом, Locust запускает нужное число таких пользователей одновременно. Ставится отдельно: pip install locust==2.47.0.
"""Нагрузка на «Задачник»: каждый виртуальный пользователь входит и работает со списком задач."""
from locust import HttpUser, between, task
class TaskerUser(HttpUser):
wait_time = between(0.5, 1.5)
def on_start(self):
r = self.client.post("/api/login", json={"login": "ivan", "password": "Ivan2026pass"})
self.client.headers["Authorization"] = "Bearer " + r.json()["token"]
@task(5)
def list_tasks(self):
self.client.get("/api/tasks")
@task(1)
def stats(self):
self.client.get("/api/stats")
Запуск без веб-интерфейса: 20 пользователей, по 10 новых в секунду, 15 секунд, против «Задачника» на этом же компьютере:
$ locust -f locustfile.py --headless -u 20 -r 10 -t 15s --host http://127.0.0.1:8000 --only-summary 2>&1 | grep -v "^\[" | sed -n "/Type/,\$p" | head -7
Type Name # reqs # fails | Avg Min Max Med | req/s failures/s
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
POST /api/login 20 0(0.00%) | 23 11 42 22 | 1.37 0.00
GET /api/stats 43 0(0.00%) | 3 2 6 3 | 2.95 0.00
GET /api/tasks 250 0(0.00%) | 4 2 21 3 | 17.14 0.00
--------|----------------------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
Aggregated 313 0(0.00%) | 5 2 42 3 | 21.46 0.00
Столбцы: число запросов и ошибок, время ответа в миллисекундах (среднее, минимум, максимум, медиана), запросов в секунду. Без ключа --headless Locust открывает веб-интерфейс на http://localhost:8089 с графиками.