Антипаттерны: тесты на реализацию и флаки
Тесты, которые приносят больше вреда, чем пользы, и как их распознать.
Флаки-тест (flaky) — это тест, который на неизменном коде то проходит, то падает; тест на реализацию проверяет как устроен код, а не что он делает.
Антипаттерн 1: тесты на детали реализации
Такой тест привязан к внутреннему устройству: «функция использует именно цикл», «вызывает метод X дважды». Стоит безобидно переписать реализацию — тест краснеет, хотя поведение не изменилось. В итоге тесты мешают рефакторингу вместо того, чтобы его защищать.
Правило: проверяйте наблюдаемое поведение (вход → выход), а не внутренние шаги.
# Две реализации — поведение одинаковое
def sum_v1(nums):
total = 0
for n in nums:
total += n
return total
def sum_v2(nums):
return sum(nums) # рефакторинг: другой способ, тот же результат
# Хороший тест проверяет ПОВЕДЕНИЕ, поэтому проходит на обеих
for impl in (sum_v1, sum_v2):
assert impl([1, 2, 3]) == 6
assert impl([]) == 0
print("Тест на поведение переживает рефакторинг — обе реализации зелёные")
Вывод:
Тест на поведение переживает рефакторинг — обе реализации зелёные
Антипаттерн 2: флаки-тесты
Флаки-тест нестабилен: причина — зависимость от времени, случайности, порядка тестов, сети, общих ресурсов. Опасность не только в ложных падениях: команда привыкает к «красному» и начинает игнорировать все падения, включая настоящие баги. Один флаки-тест способен подорвать доверие ко всему набору.
| Причина флаки | Лечение |
| Зависимость от времени/даты | Передавать время снаружи, фиксировать |
| Случайность без seed | Задавать seed |
| Зависимость от порядка тестов | Изолировать состояние |
| Сеть/внешние сервисы | Использовать дублёры (stub/fake) |
| Гонки и таймеры (sleep) | Ждать условие, а не фиксированное время |
Демонстрация нестабильности и её устранения
import random
# ПЛОХО (концептуально): результат зависит от неуправляемой случайности
# -> иногда проходит, иногда нет. Не делаем так.
# ХОРОШО: фиксируем seed -> тест детерминирован
def pick(seed):
rnd = random.Random(seed)
return rnd.choice(["A", "B", "C"])
# Один и тот же seed всегда даёт один результат -> не флаки
assert pick(1) == pick(1)
assert pick(7) == pick(7)
print("Случайность зафиксирована seed — тест больше не флакает")
Вывод:
Случайность зафиксирована seed — тест больше не флакает
Итог
- Тесты на реализацию мешают рефакторингу — проверяйте поведение, а не устройство.
- Флаки-тесты подрывают доверие ко всему набору — их нужно чинить или удалять.
- Источники флаки — время, случайность, порядок, сеть; лечатся фиксацией и дублёрами.