Основы Pytest

Содержание:

  1. Первый тест с pytest
  2. Тестовый класс
  3. Fixtures (фикстуры)
  4. Шпаргалка по запуску Pytest

Первый тест с pytest

Note

Pytest — фреймворк для автоматизированного тестирования на Python (обзорно мы знакомились с ним в 01.02 - Знакомство с AQA). Начнем мы с создания простого теста, построим представление, а после уже будем углубляться в различные особенности этого фреймворка.

Создание тестового файла

Tip

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

  • test_*.py (например, test_my_functions.py)
  • _test.py (например, my_module_test.py)

Создайте файл, test_addition.py. В этом файле мы будем писать наши тесты.


Написание простой тестовой функции

Note

Тестовые функции в pytest должны начинаться с префикса test_. Это позволяет pytest автоматически находить и запускать их. Давайте напишем простую функцию, которая проверяет сложение двух чисел:

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

Tip

Разберем этот код:

  • def test_addition(): — определение тестовой функции. Имя функции начинается с test_, что является обязательным для pytest.
  • assert 2 + 2 == 4 — это утверждение (assertion). Утверждения используются для проверки ожидаемых результатов. В данном случае мы утверждаем, что сумма 2 и 2 равна 4. Если это условие истинно, тест считается пройденным. Если условие ложно, тест считается проваленным.

Tip

Если вы все сделали правильно, то у вас должен был появиться запуск тестовой функции! как на скриншоте ниже (для pycharm)

🧪Практика:

Example

  1. Сделать файл, тестовую функциюю.
  2. Запустить тестовую функцию
  3. Изменить результат выражения assert, чтобы возникла ошибка. Ознакомиться с исключением

Вспоминаем про использование операторов assert

Note

Оператор assert — ключевой инструмент в тестировании. Он принимает логическое выражение и проверяет его истинность. Если выражение истинно, выполнение теста продолжается. Если выражение ложно, тест завершается с ошибкой AssertionError.

Вот несколько примеров использования assert:

  • assert a == b: Проверяет, что a равно b
  • assert a != b: Проверяет, что a не равно b
  • assert a > b, assert a < b, assert a >= b, assert a <= b: Проверяют отношения “больше”, “меньше”, “больше или равно”, “меньше или равно”
  • assert x in y: Проверяет, что x содержится в y (например, x является элементом списка или подстрокой в строке)
  • assert x not in y: Проверяет, что x не содержится в y

Note

Можно добавлять информативное сообщение к assert, которое будет выводиться в случае провала теста:

def test_string_contains():
    text = "hello world"
    substring = "world"
    assert substring in text, f"Подстрока '{substring}' не найдена в строке '{text}'"

Запуск тестов из командной строки

Note

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

  • Скриншот на всякий случай

pytest

Note

Pytest автоматически обнаружит все файлы, начинающиеся с test_ или заканчивающиеся на _test.py, и запустит все функции, начинающиеся с test_.

Можно указать конкретный файл или даже конкретную функцию для запуска:

  • pytest test_addition.py: Запустить тесты только из файла test_addition.py.
  • pytest test_addition.py::test_addition: Запустить только функцию test_addition из файла test_addition.py

Note

Про правила запуска мы еще успеем отдельно поговорить, на текущий момент этого будет достаточно для практики


Разбор вывода pytest

После запуска тестов pytest выводит результаты в терминале. Рассмотрим основные элементы вывода:

  • . (точка): Тест пройден успешно.
  • F (Failing): Тест провален.
  • E (Error): Во время выполнения теста произошла ошибка (например, исключение).
  • s (skipped): Тест был пропущен (например, с помощью маркировки @pytest.mark.skip)
  • x (xfailed): Тест ожидаемо провален (с помощью маркировки @pytest.mark.xfail).

Note

В случае проваленных тестов (F или E) pytest выводит подробную информацию об ошибке, включая место, где произошло исключение, и сообщение об ошибке (если оно было добавлено к assert)

  • Пример полного цикла:

    Создаем файл test_my_functions.py с тестами:

    # test_my_functions.py
    def add(a, b):
        return a + b
     
    def test_add_positive_numbers():
        assert add(2, 3) == 5
     
    def test_add_negative_numbers():
        assert add(-1, -4) == -5
     
    def test_add_positive_and_negative():
        assert add(5, -2) == 3
     
    def test_add_string():
        assert add("test", "string") == "test string" #тест который должен упасть
        

    Запускаем тесты из командной строки:

    pytest

    Вывод будет примерно таким (в зависимости от вашей версии pytest):

    (.venv) PS C:\Users\Николай\PycharmProjects\lessons> pytest
    ========================================================== test session starts ==========================================================
    platform win32 -- Python 3.10.4, pytest-8.3.4, pluggy-1.5.0
    rootdir: C:\Users\Николай\PycharmProjects\lessons
    collected 5 items                                                                                                                        
     
    test_addition.py .                                                                                                                 [ 20%]
    test_my_functions.py ...F                                                                                                          [100%]
     
    =============================================================== FAILURES ================================================================ 
    ____________________________________________________________ test_add_string ____________________________________________________________ 
     
        def test_add_string():
    >       assert add("test", "string") == "test string" #тест который должен упасть
    E       AssertionError: assert 'teststring' == 'test string'
    E
    E         - test string
    E         ?     -
    E         + teststring
     
    test_my_functions.py:19: AssertionError
    ======================================================== short test summary info ======================================================== 
    FAILED test_my_functions.py::test_add_string - AssertionError: assert 'teststring' == 'test string'
    ====================================================== 1 failed, 4 passed in 0.07s ====================================================== 

Tip

Этот вывод показывает, что три теста прошли успешно (.), а один тест (test_add_string) провалился (F), и предоставляет подробную информацию о причине провала

🧪Практика:

Example

  1. Повторить то же самое написав Pytest в терминал
  2. Запустить сделать так, чтобы все тесты прошли - посмотреть на вывод
  3. Изменить 2 тестовые функции, чтобы они упали. Запустить тесты и ознакомиться как выглядит падение нескольких тестов

Тестовый класс

Note

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

Создание тестового класса

Tip

Тестовый класс создается как обычный класс Python, но с одним важным условием: его имя должно начинаться с префикса Test. Например, TestMyClass, TestUserRegistration. Методы внутри тестового класса, которые являются тестами, должны начинаться с префикса test_

Возьмем наш тестовый модуль, test_my_functions.py и сделаем там тестовый класс:

# test_my_functions.py
def add(a, b):
    return a + b
 
class TestMyFunctions:
 
    def test_add_positive_numbers(self):
        assert add(2, 3) == 5
    
    def test_add_negative_numbers(self):
        assert add(-1, -4) == -5
 
    def test_add_positive_and_negative(self):
        assert add(5, -2) == 3
 
    def test_add_string(self):
        assert add("test", "string") == "test string" #тест который должен упасть

Tip

  • Обратите внимание, что методы класса принимают аргумент self. Это стандартное соглашение для методов экземпляра класса в Python.
  • Имя класса начинается с Test
  • Как устроены классы и self, мы разбирали в 03.04 - ООП Классы
  • Слева напротив class TestMyFunctions: появилась возможность запуска тестов этого класса

🧪Практика:

Example

  1. Преобразовать ваш предыдущий тест, использовав в нем тестовый класс
  2. Попробовать запустить тестовый класс напрямую
  3. Запустить тесты используя терминал и команду pytest . Проанализировать вывод
  4. Запустить тесты класса напрямую через терминал. Пример: pytest test_my_functions.py::TestMyFunctions

Fixtures (фикстуры):

Note

Фикстуры — это функции, которые предоставляют данные или ресурсы для тестов. Они выполняют настройку (setup) перед выполнением теста и, опционально, очистку (teardown) после его завершения

Создаем фикстуру

Для создания фикстуры используется декоратор @pytest.fixture.

Пример:

import pytest
 
@pytest.fixture
def input_data():
    return [1, 2, 3, 4, 5]
 
def test_sum(input_data): #используем фикстуру как аргумент функции
    assert sum(input_data) == 15
 
def test_len(input_data): #используем фикстуру как аргумент функции
    assert len(input_data) == 5

Tip

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

  • Обратите внимание, что мы импортировали pytest: import pytest
  • @pytest.fixture: Декоратор, который преобразует функцию input_data в фикстуру
  • def input_data():: Функция, которая возвращает список чисел. Этот список будет доступен в тестах, использующих фикстуру
  • Фикстуры передаются в тестовую функцию в виде аргументов (в эти вот скобочки)
  • def test_sum(input_data): и def test_len(input_data):: Тестовые функции, которые принимают фикстуру input_data в качестве аргумента. Pytest автоматически передаст результат выполнения фикстуры в эти функции

Область видимости фикстур (Scope)

Фикстуры могут иметь разную область видимости (scope), которая определяет, как часто фикстура будет создаваться и уничтожаться. Доступные области видимости:

  • function (по умолчанию): Фикстура создается для каждого тестового метода.
  • class: Фикстура создается один раз для всех тестовых методов в классе.
  • module: Фикстура создается один раз для всех тестов в модуле (файле).
  • session: Фикстура создается один раз для всей тестовой сессии (то есть прогон тестов начался - фикстура для всех)

Note

Область видимости задается с помощью параметра scope декоратора @pytest.fixture

Например:

@pytest.fixture(scope="function")
@pytest.fixture(scope="class")
@pytest.fixture(scope="module")
@pytest.fixture(scope="session")

Принцип работы фикстур (внутри)

Note

Фикстуры в pytest работают по принципу отложенного выполнения и зависимости. Это означает, что фикстура не выполняется, пока она явно не запрошена тестом.

Note

  1. Запрос фикстуры: Тест запрашивает фикстуру, указывая её имя в качестве аргумента функции теста
  2. Поиск и выполнение: Pytest ищет фикстуру с указанным именем. Если фикстура не найдена в текущем файле, pytest ищет её в файле conftest.py (который может находиться в корне проекта или в любой из его поддиректорий). После нахождения фикстура выполняется
  3. Кэширование: Результат выполнения фикстуры кэшируется. Это значит, что если несколько тестов запрашивают одну и ту же фикстуру с одинаковой областью видимости, фикстура будет выполнена только один раз, а затем её кэшированный результат будет передан во все запрашивающие тесты.
  4. Жизненный цикл (создание и уничтожение):
    • Фикстура создается (выполняется код функции фикстуры) непосредственно перед первым тестом, который её запрашивает, в пределах её области видимости.
    • Фикстура “уничтожается” (выполняется код после yield, если он есть) после последнего теста, который её использовал, в пределах её области видимости. Если yield отсутствует, фикстура считается “уничтоженной” сразу после возврата значения

Tip

Здесь вы увидели 2 незнакомых слова: это файл conftest.py и yield

conftest.py - это будет отдельный файл, который будет хранить фикстуры, его разберем дальше yield - тоже будем дальше разбирать этот оператор, пока не обращаем на него внимания


Conftest

Tip

conftest.py — это специальный файл в pytest, который служит для хранения фикстур и плагинов, доступных для нескольких тестовых файлов. Pytest автоматически обнаруживает этот файл и использует определенные в нем фикстуры без необходимости их импортировать в тестовые файлы

Размещение:

Note

Файл conftest.py может быть размещен в любой директории вашего проекта. Фикстуры, определенные в conftest.py, будут доступны для всех тестов в этой директории и во всех ее поддиректориях.

Пример:

Предположим, у нас есть следующая структура проекта:

my_project/ ├── tests/ │ ├── unit/ │ │ ├── test_math.py │ │ └── test_strings.py │ └── integration/ │ └── test_database.py └── conftest.py

Note

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

  • Пример с разными уровнями conftest.py:

Tip

Если у вас есть несколько уровней вложенности директорий с тестами, вы можете использовать несколько файлов conftest.py. Фикстуры будут доступны в пределах директории, где находится conftest.py, и во всех ее поддиректориях.

> `my_project/
├── tests/
│   ├── unit/
│   │   ├── test_math.py
│   │   └── conftest.py # Фикстуры для unit тестов
│   └── integration/
│       └── test_database.py
└── conftest.py # Фикстуры для всех тестов`
> 
> 
> В этом случае фикстуры из корневого `conftest.py` будут доступны для всех тестов, а фикстуры из `tests/unit/conftest.py` будут доступны только для тестов в директории `unit` и ее поддиректориях.
> 

Note

Важные моменты про conftest.py:

  • Фикстуры, определенные в conftest.py, не нужно импортировать в тестовые файлы.
  • conftest.py автоматически обнаруживается pytest.
  • Можно использовать несколько файлов conftest.py для организации фикстур по разным уровням вложенности

🧪Практика:

Example

Взять пример из урока:

import pytest
 
@pytest.fixture
def input_data():
    return [1, 2, 3, 4, 5]
 
def test_sum(input_data): #используем фикстуру как аргумент функции
    assert sum(input_data) == 15
 
def test_len(input_data): #используем фикстуру как аргумент функции
    assert len(input_data) == 5
  1. Задача - сделать в корне проекта файл conftest.py
  2. Перенести фикстуру в только что созданный файл
@pytest.fixture
def input_data():
  1. Запустить тест. Все должно сработать

FAQ если не работает:

  1. Если вы положили все правильно, но пишет что фикстура не найдена, то лезем в конфигурацию запуска правой кнопкой мыши на запуск → Modify Run Configuration

    • скрин

  2. смотрим на его working directory. Здесь должна быть корневая директория, чтобы был виден conftest из корня

    • скрин

  3. Если это не так, то меняем на корневую и проверяем запуск. Например у меня C:\Users\Николай\PycharmProjects\lessons\tests - было C:\Users\Николай\PycharmProjects\lessons - стало Попробовать запустить. Если сработало, идем дальше

  4. Меняем теймплейт (шаблон) для создания конфигураций. Заходим в настройки конфигурации запуска

    • скрин

  5. Заходим в edit configuration templates

    • скрин

  6. Устанавливаем рабочую директорию для autodetect ОБЯЗАТЕЛЬНО жмем apply

    • скрин

  7. Теперь НОВЫЕ! созданные тесты будут смотреть в корневую директорию и найдут conftest при запуске


Повторим еще раз (или шпаргалка) по запуску Pytest

  • Запуск всех тестов в текущей директории и поддиректориях:

    pytest

    (Запускается из корня проекта my_project)

  • Запуск всех тестов в конкретной директории (папки):

    pytest tests/unit/
  • Запуск конкретного файла с тестами:

    pytest tests/unit/test_math.py
  • Запуск конкретного тестового класса:

    pytest tests/unit/test_math.py::TestMathOperations
  • Запуск конкретного тестового метода (функции или метода класса):

    pytest tests/unit/test_math.py::test_addition
    pytest tests/unit/test_math.py::TestMathOperations::test_multiplication

⬅️ Назад: 04.01 - Подготовка к авто-тестам | Далее: 04.03 - Основы Requests ➡️ Модуль: 04 - MOC