Написание тестов на Python с помощью pytest: от простого к сложному

Раздел: Разработка на Python -> Тестирование

Основы написания тестов на Python с pytest

Как создать простой тест и запустить его?

Для начала требуется установить библиотеку pytest:

pip install pytest

A b test python (a/b тестирование в python)

Затем создается файл с тестами, имя которого начинается на test_ или заканчивается на _test.py. Внутри пишутся функции, начинающиеся на test_.

# test_example.py
def test_addition():
    result = 2 + 2
    assert result == 4

тесты алгоритмы и программирование python (тестирование алгоритмов и программ на python)

Запуск тестов выполняется командой:

pytest

Python code tests (тестирование кода в python)

Основной элемент - assert. Если условие ложно, тест считается проваленным. Это заменяет self.assertEqual и другие методы из unittest. Для проверки исключений используется pytest.raises.

import pytest

def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0

Py test python (написание тестов на python (pytest))

Типичная ошибка: название файла или функции не соответствует шаблону. Решение - проверить шаблон или запустить pytest с явным указанием файла: pytest test_something.py.

Ошибка импорта возникает, если не установлены зависимости или не настроен путь. Рекомендуется запускать тесты из корня проекта.

Как организовать тесты с использованием параметризации?

Параметризация позволяет запускать один тест с разными наборами данных с помощью декоратора @pytest.mark.parametrize:

import pytest

@pytest.mark.parametrize('a, b, expected', [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
])
def test_sum(a, b, expected):
    assert a + b == expected

Test data python (создание тестовых данных в python)

Цель: избежать дублирования кода и улучшить покрытие.

Как переиспользовать ресурсы с помощью фикстур?

Фикстуры - функции, подготавливающие данные перед тестом. Создаются с декоратором @pytest.fixture:

import pytest

@pytest.fixture
def db_connection():
    connection = connect_to_database()
    yield connection
    connection.close()

def test_query(db_connection):
    result = db_connection.fetch('SELECT 1')
    assert result == 1

У каждой фикстуры есть область видимости (scope): function, class, module, session. Для долгих ресурсов стоит указывать scope='session'.

Проблема: конфликт имен фикстур. Если две фикстуры в разных файлах называются одинаково, pytest может применить не ту. Решение - помечать фикстуры модулем или использовать уникальные имена.

Фикстуры с yield выполняют код после завершения теста. Важно не забыть закрыть ресурс в блоке after yield.

Как использовать маркеры для группировки тестов?

Маркеры позволяют метить тесты для выборочного запуска. Например, @pytest.mark.slow для медленных тестов:

import pytest

@pytest.mark.slow
def test_large_computation():
    ...

Запуск только медленных тестов: pytest -m slow. Маркеры регистрируют в pytest.ini для подавления предупреждений.

Как тестировать исключения и ошибки?

Используется контекст pytest.raises с возможностью проверки сообщения:

import pytest

def test_type_error():
    with pytest.raises(TypeError, match='unsupported operand type'):
        '2' + 2

Как организовать тестирование веб-запросов с моками?

Библиотека pytest-mock предоставляет фикстуру mocker для замены внешних вызовов:

import pytest

def test_request(mocker):
    mocker.patch('requests.get', return_value=Mock(status_code=200))
    response = my_app.fetch_data()
    assert response.status_code == 200

Ошибка: попытка замокировать асинхронную функцию без pytest-asyncio. Для асинхронных тестов следует добавлять маркер @pytest.mark.asyncio и устанавливать библиотеку pytest-asyncio.

Цели использования перечисленных подходов:

  • Параметризация сокращает повторяющийся код и расширяет набор тестовых данных.
  • Фикстуры изолируют ресурсы и упрощают подготовку/чистку данных.
  • Маркеры помогают разделить тесты на быстрые/медленные, интеграционные/юнит.
  • Моки заменяют реальные зависимости, делая тесты быстрыми и независимыми.

Расширенные примеры тестов на pytest

Рассмотрим более сложные сценарии с полным кодом и результатами.

Пример 1: Тестирование асинхронного кода

Сначала требуется установить pytest-asyncio:

Пример
pip install pytest-asyncio
Пример
# test_async.py
import asyncio
import pytest

async def fetch_data():
    await asyncio.sleep(0.1)
    return 42

@pytest.mark.asyncio
async def test_async_fetch():
    result = await fetch_data()
    assert result == 42

Запуск:

Пример
pytest test_async.py -v
test_async.py::test_async_fetch PASSED

Проблема: если забыть маркер @pytest.mark.asyncio, тест не выполнится или вызовет ошибку. Решение - всегда указывать маркер или настроить автоматическую регистрацию в pyproject.toml.

Пример 2: Параметризация с несколькими аргументами и фикстурами

Пример
# test_param.py
import pytest

@pytest.fixture
def base_value():
    return 10

@pytest.mark.parametrize('multiplier, expected', [
    (1, 10),
    (2, 20),
    (3, 30),
])
def test_multiplication(base_value, multiplier, expected):
    assert base_value * multiplier == expected

Результат:

test_param.py::test_multiplication[1-10] PASSED
test_param.py::test_multiplication[2-20] PASSED
test_param.py::test_multiplication[3-30] PASSED

Пример 3: Тестирование базы данных с фикстурой scope='session'

Пример
# test_db.py
import pytest
import sqlite3

@pytest.fixture(scope='session')
def db_connection():
    conn = sqlite3.connect(':memory:')
    conn.execute('CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)')
    conn.commit()
    yield conn
    conn.close()

def test_insert(db_connection):
    db_connection.execute('INSERT INTO test (id, value) VALUES (1, "hello")')
    db_connection.commit()
    cursor = db_connection.execute('SELECT value FROM test WHERE id=?')
    assert cursor.fetchone()[0] == 'hello'

def test_rollback(db_connection):
    # Каждый тест использует ту же сессионную БД, поэтому данные из предыдущего теста видны
    cursor = db_connection.execute('SELECT COUNT(*) FROM test')
    count = cursor.fetchone()[0]
    assert count == 1  # из предыдущего теста

Результат:

test_db.py::test_insert PASSED
test_db.py::test_rollback PASSED

Проблема: использование сессионной фикстуры может привести к влиянию тестов друг на друга. Для изоляции стоит использовать scope='function' или делать откат изменений.

Пример 4: Тестирование вызова внешнего API с mock

Пример
# test_api.py
import pytest
from unittest.mock import Mock
import requests

def fetch_user(user_id):
    response = requests.get(f'https://api.example.com/users/{user_id}')
    return response.json()

def test_fetch_user(mocker):
    mock_response = Mock()
    mock_response.json.return_value = {'id': 1, 'name': 'Alice'}
    mocker.patch('requests.get', return_value=mock_response)
    result = fetch_user(1)
    assert result == {'id': 1, 'name': 'Alice'}
    requests.get.assert_called_once_with('https://api.example.com/users/1')

Результат:

test_api.py::test_fetch_user PASSED

Пример 5: Использование плагина pytest-cov для покрытия

Установка:

Пример
pip install pytest-cov

Запуск с отчетом:

Пример
pytest --cov=my_module --cov-report=term-missing test/
Name           Stmts   Miss  Cover   Missing
-------------------------------------------
my_module.py      10      2    80%   3, 7
-------------------------------------------

Пример 6: Параллельный запуск с pytest-xdist

Установка:

Пример
pip install pytest-xdist

Запуск на нескольких ядрах:

Пример
pytest -n 4

Результат аналогичен, но тесты выполняются параллельно.

Проблема: параллельные тесты не должны зависеть друг от друга (общие файлы, БД). Решение - использовать изолированные ресурсы или фикстуры с xdist_group.

Написание тестов на Python (pytest) - comments

En
Py test python (python)