Как писать автотесты на python

Как писать автотесты на python

Автоматизация тестирования на Python – это не абстрактная практика, а инструмент, позволяющий обнаруживать ошибки до выхода кода в продакшен. Системы CI/CD, такие как GitHub Actions, GitLab CI или Jenkins, обрабатывают сотни тестов за секунды, и написание качественных автотестов становится критически важным навыком для разработчиков.

В Python для написания автотестов чаще всего используют unittest, pytest и режеnose. pytest выделяется лаконичностью, расширяемостью через плагины и удобной работой с фикстурами. Например, при тестировании API с использованием requests и pytest можно построить модульные и интеграционные тесты, которые охватывают все критические пути выполнения.

Ключевые рекомендации: не тестируйте реализацию, тестируйте поведение; используйте assert, а не условные конструкции; выносите повторяющийся код в фикстуры; пишите тесты до написания основной логики, если работаете по TDD. Код теста должен быть таким же читаемым, как и рабочий код – с понятными именами функций и проверок.

Сложные системы требуют тестирования на нескольких уровнях: от юнит-тестов до end-to-end. В Python можно легко интегрировать инструменты типа Selenium, Playwright или HTTPX для более комплексного покрытия. Автотест – не цель, а средство: плохой тест, не отражающий бизнес-логику, может нанести больше вреда, чем его отсутствие.

Настройка окружения для запуска автотестов на Python

Создайте отдельное виртуальное окружение для тестов с помощью venv или virtualenv. Это гарантирует изоляцию зависимостей: python -m venv venv, затем source venv/bin/activate (Linux/macOS) или venv\Scripts\activate (Windows).

Установите необходимые библиотеки. Базовый набор: pytest для запуска тестов, requests для работы с HTTP, pytest-cov для покрытия кода: pip install pytest requests pytest-cov.

Для управления зависимостями создайте файл requirements.txt и добавьте в него список библиотек. Используйте команду pip freeze > requirements.txt после установки нужных пакетов. Для установки зависимостей в будущем – pip install -r requirements.txt.

Настройте конфигурационный файл pytest.ini в корне проекта для централизованного управления параметрами запуска. Пример содержимого:
[pytest]
addopts = -v --cov=your_package --cov-report=term-missing
testpaths = tests

Создайте директорию tests/ и убедитесь, что каждый файл начинается с test_, а тестовые функции – с test_. Это обеспечит автоматическое обнаружение тестов.

Для упрощения запуска используйте make или tox. tox позволяет запускать тесты в разных версиях Python. Установите его: pip install tox и создайте файл tox.ini.

Если проект размещён в репозитории, добавьте venv/ и __pycache__/ в .gitignore. Это исключит временные и локальные файлы из контроля версий.

Выбор фреймворка для написания автотестов: pytest против unittest

unittest входит в стандартную библиотеку Python и реализует классический подход к написанию тестов в стиле JUnit. Каждый тест оформляется в виде метода внутри класса, имена методов должны начинаться с test_. Для проверки условий используются строго определённые методы, такие как assertEqual, assertTrue, assertRaises.

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

Для параметризации unittest требует сторонних решений или переопределения методов. В pytest используется декоратор @pytest.mark.parametrize, который позволяет запускать один тест с разными входными данными без дублирования кода.

Поддержка фикстур в pytest значительно шире: фикстуры можно использовать в виде аргументов тестовых функций, объединять в цепочки, задавать области действия (scope). unittest ограничен методами setUp и tearDown, привязанными к жизненному циклу класса.

Если требуется тонкий контроль над запуском тестов, фильтрация, кастомные хуки или расширения – pytest предлагает мощную систему плагинов и командную строку с множеством опций. unittest в этом плане значительно менее гибок и чаще требует дополнительного кода для реализации схожего функционала.

Рекомендация: для проектов с высокими требованиями к читаемости, масштабируемости и расширяемости тестов, оптимален pytest. unittest целесообразен в проектах с минимальными зависимостями, где важна совместимость только со стандартной библиотекой Python.

Создание первого автотеста с использованием pytest

Создание первого автотеста с использованием pytest

Установите pytest через pip:

pip install pytest

Создайте файл test_math.py в корне проекта. Все имена тестов и файлов должны начинаться с test_, чтобы pytest мог их обнаружить.

Пример функции для тестирования:

def add(a, b):
return a + b

Пример автотеста:

def test_add_positive_numbers():
assert add(2, 3) == 5
def test_add_zero():
assert add(0, 0) == 0
def test_add_negative_numbers():
assert add(-1, -4) == -5

Запустите тесты в терминале:

pytest

Рекомендации:

  • Каждый тест проверяет один конкретный сценарий.
  • Не добавляйте логики в тесты – только вызов функции и проверка результата.
  • Названия тестов должны описывать ожидаемое поведение.

Структура проекта:

.
├── test_math.py
└── ...

Для запуска одного теста укажите его имя:

pytest -k test_add_zero
pytest -v

Использование pytest на старте позволяет быстро выявлять ошибки в логике и обеспечивает воспроизводимость поведения функций.

Использование фикстур в автотестах для подготовки данных

Использование фикстур в автотестах для подготовки данных

Фикстуры в pytest позволяют централизованно создавать, конфигурировать и уничтожать объекты, необходимые для выполнения тестов. Это упрощает поддержку тестов и снижает дублирование кода. Определяются фикстуры с помощью декоратора @pytest.fixture.

Для подготовки данных удобно использовать фикстуры, которые возвращают заранее сконфигурированные структуры, модели или объекты. Например, при тестировании API можно создать фикстуру, которая возвращает словарь с валидными данными для POST-запроса:

import pytest
@pytest.fixture
def valid_user_payload():
return {
"username": "test_user",
"email": "user@example.com",
"password": "securePass123"
}

Эта фикстура может использоваться в тесте, который проверяет успешное создание пользователя:

def test_create_user(client, valid_user_payload):
response = client.post("/api/users", json=valid_user_payload)
assert response.status_code == 201
assert response.json()["username"] == "test_user"

Фикстуры могут быть параметризованы для генерации набора тестов с разными входными данными. Это делается с помощью @pytest.fixture(params=[...]):

@pytest.fixture(params=["user1@example.com", "user2@example.com"])
def email(request):
return request.param

Также фикстуры могут зависеть друг от друга. Это полезно, когда необходимо сначала создать объект в базе данных, а затем передать его в другой фикстуре:

@pytest.fixture
def user_model():
return User(username="tester", email="tester@example.com")
@pytest.fixture
def persisted_user(user_model, db_session):
db_session.add(user_model)
db_session.commit()
return user_model

Для управления временем жизни фикстур используется аргумент scope. Значения: function, class, module, session. Например, если подключение к базе данных нужно создать один раз для всех тестов:

@pytest.fixture(scope="session")
def db_connection():
conn = connect_to_db()
yield conn
conn.close()

Использование фикстур делает автотесты модульными, чистыми и независимыми. Это особенно критично при увеличении количества тестов и усложнении логики подготовки окружения.

Параметризация тестов для проверки разных входных данных

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

Пример теста, проверяющего функцию деления с различными входами:

import pytest
def divide(a, b):
return a / b
@pytest.mark.parametrize(
"a, b, expected",
[
(10, 2, 5),
(9, 3, 3),
(5, 2, 2.5),
(0, 1, 0)
]
)
def test_divide(a, b, expected):
assert divide(a, b) == expected
  • Каждая тройка (a, b, expected) – это отдельный запуск теста.
  • Поддерживаются любые типы данных: числа, строки, списки, словари.
  • Параметры можно комбинировать, чтобы протестировать все комбинации аргументов.

Для проверки ошибок используйте pytest.raises с параметризацией:

@pytest.mark.parametrize("a, b", [(1, 0), (5, 0)])
def test_divide_by_zero(a, b):
with pytest.raises(ZeroDivisionError):
divide(a, b)

При большом объёме данных удобно выносить параметры в отдельный файл или использовать генераторы:

test_data = [(i, i + 1, i / (i + 1)) for i in range(1, 100)]
@pytest.mark.parametrize("a, b, expected", test_data)
def test_generated_data(a, b, expected):
assert divide(a, b) == expected
  • Генерация параметров повышает масштабируемость тестов.
  • Ошибки легко локализуются благодаря отображению конкретных входных данных при падении теста.

Мокинг внешних зависимостей с помощью unittest.mock

Когда приложение зависит от внешних ресурсов, таких как базы данных, веб-сервисы или файлы, тестирование становится сложным и ресурсозатратным. В таких случаях использование моков позволяет изолировать тестируемый код от внешних зависимостей, ускоряя и упрощая процесс тестирования.

Модуль unittest.mock в Python предоставляет инструменты для создания мок-объектов и контроля их поведения, что делает тесты независимыми от внешних систем.

Основные возможности unittest.mock

Основные возможности undefinedunittest.mock</code>«></p>
<ul>
<li><code>Mock</code> – основной класс для создания мок-объектов, которые можно настроить для имитации поведения реальных объектов.</li>
<li><code>patch</code> – позволяет временно заменять объекты или функции в тестах.</li>
<li><code>MagicMock</code> – расширение класса <code>Mock</code>, добавляющее дополнительные магические методы (например, для поддержки операций с индексами или вызова методов).</li>
</ul>
<h3>Пример использования <code>Mock</code></h3>
<p>Предположим, у нас есть функция, которая делает запрос к внешнему API:</p>
<pre><code>import requests
def get_user_data(user_id):
response = requests.get(f

Для тестирования этой функции мы можем заменить реальный вызов requests.get на мок, который будет возвращать заранее подготовленные данные.

from unittest.mock import Mock, patch
import unittest
class TestGetUserData(unittest.TestCase):
@patch('requests.get')
def test_get_user_data(self, mock_get):
mock_response = Mock()
mock_response.json.return_value = {'id': 1, 'name': 'John Doe'}
mock_get.return_value = mock_response
result = get_user_data(1)
mock_get.assert_called_once_with('https://api.example.com/users/1')
self.assertEqual(result, {'id': 1, 'name': 'John Doe'})

В этом примере:

  • Мы используем patch для замены requests.get на мок.
  • Настроили мок-объект так, чтобы его метод json возвращал заранее заданные данные.
  • Проверили, что функция get_user_data вернула ожидаемый результат.

Использование patch с контекстным менеджером

Помимо декоратора patch, можно использовать его и как контекстный менеджер:

with patch('requests.get') as mock_get:
mock_response = Mock()
mock_response.json.return_value = {'id': 1, 'name': 'Jane Doe'}
mock_get.return_value = mock_response
result = get_user_data(2)
self.assertEqual(result, {'id': 1, 'name': 'Jane Doe'})

Настройка поведения моков

Настройка поведения моков

  • return_value – используется для задания возвращаемого значения функции или метода.
  • side_effect – позволяет задать функцию, которая будет вызвана вместо обычного значения. Это полезно, если нужно моделировать исключения или сложную логику.

Пример с использованием side_effect для имитации исключения:

def get_user_data_with_error(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
if response.status_code != 200:
raise ValueError('Failed to fetch user data')
return response.json()
class TestGetUserDataWithError(unittest.TestCase):
@patch('requests.get')
def test_get_user_data_with_error(self, mock_get):
mock_get.return_value.status_code = 500
mock_get.return_value.json.side_effect = ValueError('Failed to fetch user data')
with self.assertRaises(ValueError):
get_user_data_with_error(1)

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

Когда использовать мокинг

  • Мокинг полезен, когда внешние зависимости сложно тестировать или они замедляют выполнение тестов.
  • Не стоит злоупотреблять мокацией – если зависимости можно легко и эффективно интегрировать в тесты, то лучше использовать реальные данные.
  • Для тестов, которые зависят от времени или случайных значений, можно использовать моки для обеспечения повторяемости тестов.

Группировка и маркировка тестов для выборочного запуска

Группировка и маркировка тестов важны для оптимизации процесса тестирования. Это позволяет запускать только нужные тесты, что ускоряет цикл тестирования и упрощает диагностику проблем.

В Python для группировки тестов удобно использовать фреймворк pytest. Один из способов группировки – это использование меток, или так называемых тегов. Метки позволяют выделить тесты по различным признакам, таким как функциональные блоки, типы тестов (например, smoke, regression) или важность.

Для применения меток можно использовать декораторы. Пример создания метки для теста:

import pytest
@pytest.mark.smoke
def test_login():
assert login() == True

Теперь тест test_login помечен как smoke и его можно запускать выборочно, например:

pytest -m smoke

Это запустит все тесты, помеченные меткой smoke.

Для группировки тестов по функциональности можно использовать несколько меток одновременно. Например, для тестов, проверяющих авторизацию, можно применить такие метки:

import pytest
@pytest.mark.login
@pytest.mark.regression
def test_login():
assert login() == True

Такой подход позволяет запускать только те тесты, которые относятся к определённой функциональной области или стадии разработки:

pytest -m "login and regression"

Для более сложной фильтрации можно комбинировать логические операторы and, or, not, что даёт гибкость при выборе тестов для запуска.

Важный момент – правильное использование и документирование меток. Не следует злоупотреблять количеством меток, чтобы избежать путаницы и снизить эффективность группировки. Систематическое использование меток помогает поддерживать тесты в структурированном виде и облегчает их управление в больших проектах.

Кроме того, можно использовать маркировку для выборочного пропуска тестов в зависимости от условий окружения. Пример: тесты для базы данных могут быть помечены как db, и их можно исключать на системах, где база не настроена:

@pytest.mark.skipif(not db_ready, reason="База данных не настроена")
def test_database_connection():
assert connect_to_db() == True

Таким образом, группировка и маркировка тестов помогают гибко управлять процессом тестирования и позволяют запускать только релевантные тесты в нужный момент времени. Это особенно важно в крупных проектах с множеством тестов и разных типов окружений.

Формирование отчётов о результатах тестирования с использованием pytest-html

Для автоматизации тестирования с помощью Python и библиотеки pytest часто возникает необходимость в создании удобных и информативных отчётов о выполнении тестов. Один из популярных способов – использование плагина pytest-html, который позволяет генерировать HTML-отчёты, содержащие подробную информацию о прошедших и неудачных тестах.

Чтобы начать использовать pytest-html, нужно установить его с помощью команды:

pip install pytest-html

После установки, для генерации отчёта достаточно запустить тесты с ключом —html, указав путь, куда должен быть сохранён результат:

pytest --html=report.html

Это создаст файл report.html, который будет содержать всю информацию о выполнении тестов: общее количество, успешные и неудачные тесты, а также подробности по каждому из них.

Отчёт, созданный с помощью pytest-html, может включать различные полезные элементы, такие как:

  • Общий статус тестирования (прошли ли все тесты или есть ошибки)
  • Детали для каждого теста (время выполнения, сообщения об ошибках и трассировки)
  • Графики, иллюстрирующие статистику тестов

Для более гибкой настройки отчёта можно использовать дополнительные параметры плагина. Например, с помощью опции —self-contained-html можно сделать отчёт полностью самодостаточным, включив все стили и скрипты внутрь самого файла:

pytest --html=report.html --self-contained-html

Если необходимо добавить кастомные метки или информацию в отчёт, можно использовать хуки pytest. Например, для добавления пользовательских данных в отчёт используйте следующий код:


def pytest_html_report_title(report):
report.title = "Отчёт о тестировании"

Также можно добавить логотип или кастомное описание для отчёта. Для этого используется параметр pytest_html, который позволяет задавать дополнительные метаданные в отчёт, такие как версию приложения, среду выполнения тестов и другие данные:


def pytest_configure(config):
config._metadata['Тестируемая версия'] = '1.0.0'
config._metadata['Среда выполнения'] = 'QA'

Наконец, если вам нужно интегрировать отчёты с системой CI/CD, просто укажите путь к файлу отчёта в конфигурации вашего инструмента для автоматизации сборки, и результат будет доступен прямо после выполнения тестов.

Вопрос-ответ:

Ссылка на основную публикацию