← Назад к вопросам

В чем разница между Pytest и Unit-тест?

1.0 Junior🔥 221 комментариев
#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Разница между 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 немного удобнее.

Сравнительная таблица

Особенностьunittestpytest
УстановкаВстроен (не требует pip)Требует pip install
СинтаксисVerbose, классовыйФункции, assert
FixturessetUp/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 в любом новом проекте.

Причины:

  1. Синтаксис проще и понятнее
  2. Сообщения об ошибках лучше (экономит время отладки)
  3. Fixtures мощнее и гибче
  4. Огромная экосистема плагинов
  5. Новые разработчики быстрее понимают код

Только если это старый проект с unittest или нет возможности устанавливать зависимости — остаюсь на unittest.

Заключение

pytest и unittest решают одну задачу по-разному:

  • unittest — стандартный, встроенный, классический подход
  • pytest — современный, гибкий, удобный

Для новых проектов я смело рекомендую pytest. Он сэкономит вам время и сделает код тестов понятнее.