Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Разница между Pytest и Unit-тест (unittest)
В Python есть два основных подхода к unit-тестированию: встроенный модуль unittest и более современный фреймворк pytest. Оба решают одну задачу, но очень по-разному.
Что такое Unit-тест (unittest)?
unittest — это встроенный модуль в Python, который предоставляет фреймворк для написания unit-тестов. Он стандартный, не требует установки, используется везде.
Что такое Pytest?
pytest — это независимый фреймворк для тестирования, более современный и гибкий. Требует установки (pip install pytest), но предоставляет намного более удобный синтаксис.
Основные различия
1. Синтаксис
Unit-тест (verbose, старомодный):
import unittest
class TestCalculator(unittest.TestCase):
def setUp(self):
self.calc = Calculator()
def test_add(self):
result = self.calc.add(2, 3)
self.assertEqual(result, 5)
def test_subtract(self):
result = self.calc.subtract(5, 3)
self.assertEqual(result, 2)
def tearDown(self):
# cleanup
pass
if __name__ == '__main__':
unittest.main()
Pytest (простой, лаконичный):
import pytest
@pytest.fixture
def calc():
return Calculator()
def test_add(calc):
result = calc.add(2, 3)
assert result == 5
def test_subtract(calc):
result = calc.subtract(5, 3)
assert result == 2
Pytest использует обычные функции и встроенную функцию assert, что намного проще.
2. Assertions
Unit-тест (много методов):
self.assertEqual(a, b)
self.assertNotEqual(a, b)
self.assertTrue(x)
self.assertFalse(x)
self.assertIsNone(x)
self.assertIn(a, b)
self.assertRaises(Exception, func)
self.assertGreater(a, b)
Pytest (одна функция):
assert a == b
assert a != b
assert x is True
assert x is False
assert x is None
assert a in b
with pytest.raises(Exception):
func()
assert a > b
Pytest дает более понятные сообщения об ошибках благодаря introspection.
3. Fixtures (подготовка тестовых данных)
Unit-тест (setUp/tearDown):
class TestDatabase(unittest.TestCase):
def setUp(self):
# Каждый тест начинается с нового состояния
self.db = Database(':memory:')
self.db.connect()
def tearDown(self):
# Очистка после каждого теста
self.db.close()
def test_insert(self):
self.db.insert('users', {'name': 'John'})
assert self.db.count('users') == 1
Pytest (fixtures):
@pytest.fixture
def db():
database = Database(':memory:')
database.connect()
yield database # Тест выполняется здесь
database.close() # Очистка
def test_insert(db):
db.insert('users', {'name': 'John'})
assert db.count('users') == 1
Fixtures более гибкие — можно переиспользовать, комбинировать, иметь разные scope'ы.
4. Параметризация (запуск теста с разными данными)
Unit-тест (нужна вспомогательная функция):
import unittest
from parameterized import parameterized
class TestCalculator(unittest.TestCase):
@parameterized.expand([
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
])
def test_add(self, a, b, expected):
assert self.calc.add(a, b) == expected
Нужна дополнительная библиотека parameterized.
Pytest (встроенная функция):
@pytest.mark.parametrize('a,b,expected', [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
])
def test_add(a, b, expected):
calc = Calculator()
assert calc.add(a, b) == expected
Встроенная поддержка параметризации.
5. Структура проекта
Unit-тест требует наследования:
class TestSomething(unittest.TestCase):
def test_...
Все тесты должны быть методами класса.
Pytest просто функции:
def test_something():
assert ...
Любые функции, начинающиеся с test_, автоматически запускаются.
6. Запуск тестов
Unit-тест:
# В конце файла нужен блок
if __name__ == '__main__':
unittest.main()
# Или через командную строку
python -m unittest test_module
python -m unittest test_module.TestClass.test_method
Pytest:
# Просто
pytest
# С фильтром
pytest test_calc.py::test_add
# С маркерами
pytest -m slow
# С покрытием
pytest --cov=myproject
Pytest автоматически открывает все функции test_*.
7. Сообщения об ошибках
Unit-тест:
AssertionError: 5 != 6
Pytest (намного подробнее):
> assert calc.add(2, 3) == 6
E assert 5 == 6
E where 5 = <Calculator object>.add(2, 3)
Pytest показывает весь стек вызовов и значения переменных.
8. Мокирование
Unit-тест:
from unittest.mock import Mock, patch
mock_db = Mock()
mock_db.save.return_value = True
with patch('mymodule.Database', mock_db):
service = UserService()
service.create_user({'name': 'John'})
Pytest (с pytest-mock):
def test_create_user(mocker):
mock_db = mocker.Mock()
mock_db.save.return_value = True
mocker.patch('mymodule.Database', mock_db)
service = UserService()
service.create_user({'name': 'John'})
Синтаксис похож, но pytest-mock немного удобнее.
Сравнительная таблица
| Особенность | unittest | pytest |
|---|---|---|
| Установка | Встроен (не требует pip) | Требует pip install |
| Синтаксис | Verbose, классовый | Функции, assert |
| Fixtures | setUp/tearDown | @pytest.fixture |
| Параметризация | Требует parameterized | Встроенная |
| Сообщения об ошибках | Базовые | Очень подробные |
| Learning curve | Средний | Низкий |
| Производство | Везде | Популярен в новых проектах |
| Экосистема плагинов | Базовая | Огромная |
Когда использовать что?
Используй unittest если:
- Старый проект, везде unittest
- Не хочешь установку зависимостей
- Нужна максимальная совместимость
- Привыкнут к классовому стилю
Используй pytest если:
- Новый проект
- Нужна простота и быстрота
- Требуется большое количество параметризаций
- Важны подробные сообщения об ошибках
- Нужны плагины (pytest-asyncio, pytest-cov и т.д.)
Практический пример: Реальный проект
# conftest.py — общие fixtures
import pytest
from sqlalchemy import create_engine
from app.models import Base
@pytest.fixture
def db():
engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(engine)
yield engine
Base.metadata.drop_all(engine)
@pytest.fixture
def app(db):
app = create_app(database=db)
return app
# test_users.py
import pytest
@pytest.mark.asyncio
async def test_create_user(app):
response = await app.post('/api/users', json={'name': 'John'})
assert response.status_code == 201
assert response.json()['name'] == 'John'
@pytest.mark.parametrize('invalid_data', [
{'name': ''}, # Empty name
{'name': None}, # Null name
{}, # Missing name
])
def test_create_user_invalid(app, invalid_data):
response = app.post('/api/users', json=invalid_data)
assert response.status_code == 400
@pytest.mark.slow
def test_bulk_operations(app, db):
# Slow test marked with @slow
# Можно запустить отдельно: pytest -m slow
pass
Мой совет
За 10+ лет я работал с обоими. Сейчас я выбираю pytest в любом новом проекте.
Причины:
- Синтаксис проще и понятнее
- Сообщения об ошибках лучше (экономит время отладки)
- Fixtures мощнее и гибче
- Огромная экосистема плагинов
- Новые разработчики быстрее понимают код
Только если это старый проект с unittest или нет возможности устанавливать зависимости — остаюсь на unittest.
Заключение
pytest и unittest решают одну задачу по-разному:
- unittest — стандартный, встроенный, классический подход
- pytest — современный, гибкий, удобный
Для новых проектов я смело рекомендую pytest. Он сэкономит вам время и сделает код тестов понятнее.