Когда программист пишет код без тестов, где-то грустит один DevOps-инженер. А если серьёзно — юнит-тесты — это как подушка безопасности: ты надеешься, что они не понадобятся, но они могут спасти проект от полного фиаско.
Сегодня разберёмся:
- зачем вообще нужны юнит-тесты;
- как пользоваться встроенным модулем unittest;
- на что тесты не способны (да, у них тоже есть ограничения);
- типичные ошибки начинающих (и не только) разработчиков;
- и, конечно, 5 реальных примеров от простого к профессиональному уровню.
🧠 Зачем вообще нужны юнит-тесты?
Представьте, что вы пекарь. Вы испекли пирог (код) и хотите быть уверены, что он не отравит гостей (пользователей). Юнит-тест — это проба кусочка перед подачей. Он не гарантирует, что всё остальное идеальное, но поможет вовремя заметить, если вы случайно добавили соль вместо сахара.
Преимущества юнит-тестов:
- быстро проверяют, что код работает корректно;
- позволяют безопасно рефакторить;
- служат документацией (особенно если код не комментирован, а вы торопились);
- помогают отловить баги ещё до выката на прод.
🔧 Немного теории: что такое unittest
В Python встроен модуль unittest. Это старый добрый друг, проверенный временем. Написан по образу и подобию JUnit из мира Java.
Вот базовая структура теста на unittest:
import unittest
# Тестируемая функция
def add(a, b):
return a + b
# Класс с тестами
class TestMathOperations(unittest.TestCase):
def test_add(self):
# Проверяем, что 2 + 3 == 5
self.assertEqual(add(2, 3), 5)
# Запуск тестов
if __name__ == '__main__':
unittest.main()
📌 Комментарии по строчкам:
- import unittest — подключаем библиотеку тестов.
- def add(a, b) — функция, которую мы тестируем.
- class TestMathOperations(...) — все тесты пишем внутри наследника unittest.TestCase.
- def test_add(...) — методы начинаются с test_, иначе unittest их просто проигнорирует.
- self.assertEqual(...) — проверка: равен ли результат ожидаемому.
- unittest.main() — запускает все тесты в файле.
✅ Пример 1. Простейшая проверка функции
def is_even(n):
return n % 2 == 0
import unittest
class TestIsEven(unittest.TestCase):
def test_even(self):
self.assertTrue(is_even(2)) # 2 чётное
def test_odd(self):
self.assertFalse(is_even(3)) # 3 нечётное
if __name__ == '__main__':
unittest.main()
Что проверяем:
- возвращает ли функция True для чётных чисел;
- False — для нечётных.
👶 Уровень: новичок
📚 Полезно: учит базовому синтаксису unittest.
✅ Пример 2. Проверка исключений
def divide(a, b):
return a / b
import unittest
class TestDivide(unittest.TestCase):
def test_divide_zero(self):
# Проверяем, что при делении на 0 выбрасывается исключение
with self.assertRaises(ZeroDivisionError):
divide(10, 0)
if __name__ == '__main__':
unittest.main()
💥 Здесь мы проверяем, что функция корректно бросает исключение. Без теста кто-то мог бы попытаться делить на ноль в проде. Не смешно, особенно если это банковская система 😅
✅ Пример 3. Работа со списками и проверка порядка
def sort_names(names):
return sorted(names)
import unittest
class TestSortNames(unittest.TestCase):
def test_sorting(self):
unsorted = ['Tom', 'Anna', 'John']
sorted_expected = ['Anna', 'John', 'Tom']
self.assertEqual(sort_names(unsorted), sorted_expected)
if __name__ == '__main__':
unittest.main()
🧩 Здесь важно не только содержание, но и порядок элементов. assertEqual — сравнит списки поэлементно.
✅ Пример 4. Мокаем внешние зависимости
Теперь уровень повыше — симулируем поведение внешней функции (например, API-запроса), не вызывая её на самом деле.
import requests
def get_status_code(url):
response = requests.get(url)
return response.status_code
Теперь тест:
import unittest
from unittest.mock import patch
class TestStatusCode(unittest.TestCase):
@patch('requests.get')
def test_status_code(self, mock_get):
# Подменяем результат вызова requests.get
mock_get.return_value.status_code = 200
self.assertEqual(get_status_code('http://example.com'), 200)
if __name__ == '__main__':
unittest.main()
🕵️ Здесь мы используем patch — это как надеть маску на requests.get и сказать: «Сегодня ты возвращаешь 200, не дергайся!».
✅ Пример 5. Проверка бизнес-логики
Сложный пример: расчёт зарплаты с налогами.
def calculate_salary(gross_salary, tax_percent):
if gross_salary < 0:
raise ValueError("Salary cannot be negative")
tax = gross_salary * (tax_percent / 100)
return gross_salary - tax
Тесты:
class TestSalaryCalculation(unittest.TestCase):
def test_normal_case(self):
self.assertAlmostEqual(calculate_salary(1000, 20), 800)
def test_zero_tax(self):
self.assertEqual(calculate_salary(1000, 0), 1000)
def test_negative_salary(self):
with self.assertRaises(ValueError):
calculate_salary(-500, 20)
if __name__ == '__main__':
unittest.main()
📌 Используем assertAlmostEqual — когда работаете с float-значениями, это безопаснее, чем assertEqual.
❌ Что юнит-тесты НЕ покрывают
- Они не проверяют интеграцию между компонентами.
- Не заменяют ручное и UI-тестирование.
- Не спасут, если требования меняются каждую пятницу вечером.
🧨 Типичные ошибки
- Не запускать тесты. Вы удивитесь, как часто это случается.
- Тестировать слишком много. Не надо в одном тесте проверять 10 сценариев — лучше разнести.
- Отсутствие изоляции. Если тест зависит от другого теста — это путь в ад.
- Не использовать моки. Внешние сервисы — ненадёжные союзники.
💡 Когда писать юнит-тесты?
- Перед тем как выкатить новый функционал.
- Когда фиксите баг — сначала пишем тест, который воспроизводит баг.
- При рефакторинге — убедиться, что ничего не сломалось.
TDD (Test Driven Development) — сначала тест, потом код. Не всем подходит, но попробовать стоит.
🔚 Вывод
Писать юнит-тесты — это не скучная обязанность, а возможность спать спокойно ночью и не бояться deploy’а. Даже простые тесты уже повышают устойчивость вашего проекта.
И помните: лучше сто плохо написанных тестов, чем ни одного. Хотя нет, лучше десять хороших 😉