Хорошие тесты: принципы FIRST

Признаки хороших тестов: быстрые, изолированные, детерминированные, понятные.

FIRST — набор принципов хорошего теста: Fast, Independent, Repeatable, Self-validating, Timely.

Почему важно качество тестов, а не только их наличие

Плохие тесты хуже, чем их отсутствие: они медленные, падают случайно, и им перестают доверять. Команда начинает игнорировать красный прогон — и весь смысл теряется. Хороший тест-набор подчиняется принципам FIRST.

F — Fast (быстрые)

Юнит-тесты должны выполняться за миллисекунды. Тогда их гоняют часто — после каждого изменения. Если набор идёт минуты, его запускают редко, и баги накапливаются. Главный враг скорости — реальные обращения к БД, сети, диску. Их убирают моками (раздел про дублёры).

I — Independent (изолированные)

Тесты не должны зависеть друг от друга или от порядка запуска. Помните: unittest идёт по алфавиту, а не по порядку в файле. Тест, который полагается на данные, оставленные другим тестом, — мина замедленного действия.

import unittest

class Counter:
    total = 0   # ОПАСНО: состояние класса общее для всех

class TestBad(unittest.TestCase):
    def setUp(self):
        # Правильно: сбрасываем общее состояние перед каждым тестом
        Counter.total = 0

    def test_one(self):
        Counter.total += 1
        self.assertEqual(Counter.total, 1)

    def test_two(self):
        Counter.total += 5
        self.assertEqual(Counter.total, 5)   # благодаря setUp — не 6

unittest.main(argv=[''], exit=False, verbosity=2)

Вывод:

test_one (__main__.TestBad.test_one) ... ok
test_two (__main__.TestBad.test_two) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Без сброса в setUp второй тест ждал бы 5, но получил бы 6 (1 от первого теста + 5). Изоляция через setUp спасает.

R — Repeatable (детерминированные)

Тест обязан давать один и тот же результат при каждом запуске. «Иногда зелёный, иногда красный» (flaky-тест) — худшее, что бывает: ему нельзя верить. Источники недетерминизма — время, случайность, порядок словарей, внешние сервисы. Их фиксируют моками.

import unittest
from unittest.mock import patch
import random

def pick_winner(players):
    return random.choice(players)

class TestWinner(unittest.TestCase):
    @patch("random.choice", return_value="Анна")
    def test_deterministic(self, mock_choice):
        # Зафиксировали случайность — результат предсказуем КАЖДЫЙ раз
        self.assertEqual(pick_winner(["Анна", "Борис"]), "Анна")

unittest.main(argv=[''], exit=False, verbosity=2)

Вывод:

test_deterministic (__main__.TestWinner.test_deterministic) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

S — Self-validating (самопроверяемые)

Тест сам решает, прошёл он или нет, — через assert'ы. Никакого «посмотрите глазами на вывод». Если для оценки результата нужен человек — это не автотест.

T — Timely (вовремя)

Тесты пишут вместе с кодом, а лучше — до него (об этом следующий урок про TDD). Тесты, отложенные «на потом», обычно не пишутся никогда.

Понятные имена и читаемость

Имя теста — документация. Когда в CI падает test_withdraw_more_than_balance_raises_error, причина ясна без чтения кода. А test_3 заставляет лезть внутрь. Хороший тест читается как утверждение о поведении.

Памятка FIRST

БукваПринцип
FFast — быстрые
IIndependent — независимые
RRepeatable — детерминированные
SSelf-validating — с автоматической проверкой
TTimely — написанные вовремя

Итог

  • Качество тестов важнее их количества: плохим тестам перестают верить.
  • FIRST: быстрые, независимые, детерминированные, самопроверяемые, своевременные.
  • Изоляцию дают setUp и сброс общего состояния; детерминизм — моки времени/случайности.
  • Понятное имя теста = документация и быстрая диагностика.
Проверьте себя
1. Что означают буквы FIRST?
AFirst-In-Real-Software-Test
BFast, Independent, Repeatable, Self-validating, Timely
CFive Important Rules of Software Testing
DFunctions In Reusable Smart Tests
2. Что такое flaky-тест?
AОчень быстрый тест
BТест, который иногда зелёный, иногда красный без изменений кода — ему нельзя верить
CТест без assert
DТест в pytest
3. Как обеспечить детерминизм теста, зависящего от random?
AЗапускать много раз
BЗамокать источник случайности через patch, задав фиксированный результат
CУдалить тест
DИгнорировать падения
Поддержать проект