Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Поддержать проект
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Тесты, которые ловят реальные баги
tests_that_catch_real_bugs

Тесты, которые ловят реальные баги

Учимся думать как тестировщик: пишем тесты, которые находят реальные баги до продакшена через AAA, граничные случаи и понятные assert

⚠️ Урок устарел. Свежая версия в курсе pytest-basics: AAA-паттерн: тесты которые ловят баги.

#Научитесь находить баги ДО пользователей

❌ Сейчас: «Мой код работал, а в production сломался!»
✅ После урока: «Я знаю, какие тесты писать, чтобы баги не доходили до пользователей»

Увидите разницу между «тест проходит» и «тест полезен»
Научитесь думать как «ломатель»: граничные случаи и неверные данные
Поймёте AAA как инструмент мышления, а не просто форматирование

#Демонстрация: тест, который поймал бы production-баг

# ❌ Баг: BankAccount позволяет уйти в минус class BankAccount: def __init__(self, balance=0): self.balance = balance def withdraw(self, amount): self.balance -= amount # нет проверки на лимит # ✅ Исправленный код class BankAccountFixed: def __init__(self, balance=0, max_balance=10_000): self.balance = balance self.max_balance = max_balance def withdraw(self, amount): if amount > self.balance: raise ValueError("Not enough money") self.balance -= amount def deposit(self, amount): if self.balance + amount > self.max_balance: raise ValueError(f"Balance cannot exceed {self.max_balance}") self.balance += amount
import pytest def test_cannot_withdraw_to_negative_balance(): account = BankAccount() account.balance = 100 with pytest.raises(ValueError, match="Not enough money"): account.withdraw(200) # 100 - 200 = -100 🚨 assert account.balance == 100 # баланс не изменился def test_deposit_respects_max_balance(): account = BankAccountFixed(balance=9_500, max_balance=10_000) with pytest.raises(ValueError, match="cannot exceed"): account.deposit(600) # 9_500 + 600 = 10_100 > 10_000

#AAA: не про выравнивание, а про мышление

  • Arrange — подготовить всё: данные, окружение, моки. После этого шага тестируемая функция готова к вызову.
  • Act — одно действие, один вызов. Если действий два, это уже два теста.
  • Assert — проверка результата: возврат, изменённое состояние или побочный эффект.
def test_withdraw_reduces_balance(): # ARRANGE — готовим данные account = BankAccountFixed(balance=200) amount = 50 # ACT — одно действие account.withdraw(amount) # ASSERT — проверяем результат assert account.balance == 150

#Один тест — одно действие

# ❌ Два действия — непонятно, что сломалось def test_add_and_complete_task(): manager = TaskManager() manager.add("task 1") manager.complete(0) assert manager.all() == [] # ✅ Разносим def test_add_task(): manager = TaskManager() manager.add("task 1") assert manager.all() == ["task 1"] def test_complete_task_by_index(): manager = TaskManager() manager.add("task 1") manager.complete(0) assert manager.all() == []

#Думайте как ломатель: 5 типов граничных случаев

  1. Пустые значения: None, "", []
  2. Крайние числа: 0, отрицательные, «слишком большие»
  3. Неправильные типы: строка вместо числа
  4. Повторения: два одинаковых вызова подряд
  5. Состояние системы: пустой баланс, заполненный список

Примеры для BankAccount:

def test_deposit_negative_amount(): account = BankAccountFixed() with pytest.raises(ValueError): account.deposit(-100) def test_deposit_zero_amount(): account = BankAccountFixed() with pytest.raises(ValueError): account.deposit(0) def test_withdraw_from_empty_account(): account = BankAccountFixed() with pytest.raises(ValueError, match="Not enough money"): account.withdraw(100)

#Минимальные assert-паттерны (ничего лишнего)

import pytest from src.task_manager import TaskManager def test_assert_patterns(): manager = TaskManager() manager.add("task 1") # ✅ Проверка наличия/отсутствия в коллекции assert "task 1" in manager.all() assert "task 2" not in manager.all() # ✅ Исключение с текстом with pytest.raises(ValueError, match="empty"): manager.add("") # ✅ Тип — когда критично для логики assert isinstance(manager.all(), list)

#Анти‑паттерн: «мега-тест»

# ❌ Слишком много ответственности def test_task_manager_does_everything(): manager = TaskManager() manager.add("task 1") manager.add("task 2") manager.complete(0) manager.complete(1) with pytest.raises(ValueError): manager.add("") # валидация + завершение + дубли — всё вместе # ✅ Разбиваем def test_task_manager_adds_task(): manager = TaskManager() manager.add("task 1") assert manager.all() == ["task 1"] def test_task_manager_validates_empty_text(): manager = TaskManager() with pytest.raises(ValueError): manager.add("") def test_task_manager_completes_by_index(): manager = TaskManager() manager.add("task 1") manager.complete(0) assert manager.all() == []

#Уровни сложности

🎯 Базовый: один тест — одно действие, AAA, базовые assert'ы.
🚀 Продвинутый: 5 типов граничных случаев, шпаргалка вопросов, анти-паттерны и их разбиение.

#Практика

Найдите баги в TaskManager:

class TaskManager: def __init__(self): self.tasks = [] def add_task(self, text): self.tasks.append(text) # 🐛 валидация отсутствует def complete_task(self, index): del self.tasks[index] # 🐛 нет проверки границ

Напишите 3 теста, которые словят баги:

def test_add_task_empty_text_raises_error(): """Пустая задача должна вызывать ValueError""" manager = TaskManager() with pytest.raises(ValueError): manager.add_task("") def test_complete_task_invalid_index_raises_error(): """Несуществующий индекс должен вызывать IndexError""" manager = TaskManager() manager.add_task("task 1") with pytest.raises(IndexError): manager.complete_task(999) def test_add_task_long_text_works(): """Длинный текст (1000+ символов) сохраняется полностью""" manager = TaskManager() long_text = "x" * 1500 manager.add_task(long_text) assert manager.tasks[0] == long_text

#Шпаргалка: 5 вопросов для каждого теста

Держите под рукой, когда пишете тесты.

#📝 Про входные данные

  • Что если передать None?
  • Пустая строка/список?
  • Очень длинные данные?
  • Спецсимволы?
  • Неправильный тип?

#🔢 Про числа

  • Ноль?
  • Отрицательные?
  • Очень большие?
  • Дробные?

#🕒 Про состояние

  • Функция вызывается дважды?
  • Данные уже существуют?
  • «Пограничное» состояние системы?

#Чеклист: проверьте тест перед коммитом

  • Одно поведение — тест проверяет конкретную функциональность
  • Понятное имя — test_что_проверяем_при_каких_условиях
  • Полный Arrange — все данные подготовлены до вызова функции
  • Чистый Act — одна строка вызова тестируемой функции
  • Точный Assert — проверяет результат, а не процесс
  • Граничные случаи — есть проверки краёв и ошибок
  • Ясность при падении — по сообщению понятно, что сломалось

Бонус: покажите тест коллеге — если понял за 10 секунд, тест хороший.

Теперь вы не просто запускаете тесты — вы проверяете качество кода до того, как баги увидят пользователей. 🎯

Далее: Parametrize: один тест — десятки сценариев