Тестируем классы
Тестируем классы: состояние, методы и инварианты объекта.
Инвариант — условие, которое всегда должно выполняться для корректного объекта (например, баланс счёта не может стать отрицательным).
Чем тест класса отличается от теста функции
У класса есть состояние, и методы его меняют. Поэтому тестируют не одно «вход → выход», а сценарии: создали объект → вызвали методы → проверили, что состояние стало правильным. Удобно создавать свежий объект в setUp, чтобы каждый тест стартовал с чистого листа.
Пример: банковский счёт
Класс с пополнением, снятием и запретом уйти в минус. Тесты проверяют и нормальное поведение, и инвариант.
import unittest
class Account:
def __init__(self, balance=0):
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("сумма должна быть положительной")
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("недостаточно средств")
self.balance -= amount
class TestAccount(unittest.TestCase):
def setUp(self):
self.acc = Account(balance=100) # свежий счёт на каждый тест
def test_initial_balance(self):
self.assertEqual(self.acc.balance, 100)
def test_deposit_increases_balance(self):
self.acc.deposit(50)
self.assertEqual(self.acc.balance, 150)
def test_withdraw_decreases_balance(self):
self.acc.withdraw(40)
self.assertEqual(self.acc.balance, 60)
def test_cannot_overdraw(self):
# Инвариант: нельзя снять больше, чем есть
with self.assertRaises(ValueError):
self.acc.withdraw(1000)
# И баланс при этом не изменился
self.assertEqual(self.acc.balance, 100)
unittest.main(argv=[''], exit=False, verbosity=2)
Вывод:
test_cannot_overdraw (__main__.TestAccount.test_cannot_overdraw) ... ok test_deposit_increases_balance (__main__.TestAccount.test_deposit_increases_balance) ... ok test_initial_balance (__main__.TestAccount.test_initial_balance) ... ok test_withdraw_decreases_balance (__main__.TestAccount.test_withdraw_decreases_balance) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.001s OK
Тест test_cannot_overdraw делает две проверки одного поведения: что вылетает исключение и что состояние не испортилось. Это законно — обе проверки про один сценарий «попытка перерасхода».
Тестируйте поведение, а не реализацию
Проверяйте, что делает класс через публичный интерфейс, а не как он устроен внутри. Если тест лезет в приватные детали (self.acc._internal_list[3]), он сломается при любом рефакторинге, даже если поведение не изменилось. Хороший тест переживает переписывание внутренностей.
Проверка цепочки действий
Тест может моделировать небольшой сценарий из нескольких шагов — так проверяют, что состояние корректно накапливается:
import unittest
class Stack:
def __init__(self):
self._items = []
def push(self, x):
self._items.append(x)
def pop(self):
return self._items.pop()
def size(self):
return len(self._items)
class TestStack(unittest.TestCase):
def test_push_pop_sequence(self):
s = Stack()
s.push(1)
s.push(2)
s.push(3)
self.assertEqual(s.size(), 3)
self.assertEqual(s.pop(), 3) # LIFO: последний вошёл — первый вышел
self.assertEqual(s.pop(), 2)
self.assertEqual(s.size(), 1)
unittest.main(argv=[''], exit=False, verbosity=2)
Вывод:
test_push_pop_sequence (__main__.TestStack.test_push_pop_sequence) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
Здесь мы проверяем через публичные push/pop/size, не заглядывая в _items. Внутреннее хранилище можно заменить — тест останется валидным.
С чего начинать тестировать класс
Удобный порядок: сначала проверьте начальное состояние только что созданного объекта (что после __init__ поля имеют ожидаемые значения), затем каждый публичный метод по отдельности на «счастливом пути», и только потом — нарушения инвариантов и ошибки. Так вы движетесь от простого к сложному и быстро получаете базовое покрытие. Если у класса много методов, не пытайтесь покрыть всё одним гигантским тестом — дробите по методам и сценариям.
Объекты в роли зависимостей
Часто класс принимает другой объект в конструкторе (репозиторий, сервис, логгер). Тестировать такой класс с настоящими зависимостями тяжело и медленно. Решение — подставить вместо них тест-дублёр (мок), о котором подробно пойдёт речь в разделе про unittest.mock. Пока запомните принцип: класс легче тестировать, когда его зависимости приходят снаружи (через конструктор или аргументы), а не создаются жёстко внутри. Это называется внедрением зависимостей и делает код тестируемым.
Итог
- У классов есть состояние — тестируем сценарии «создать → вызвать методы → проверить».
- Свежий объект в
setUpобеспечивает изоляцию. - Проверяйте инварианты (что нельзя нарушить) и через исключения тоже.
- Тестируйте поведение через публичный интерфейс, а не приватные детали.