камплюхтеры
September 5, 2021

➡️ Введение в PyTest

Я уже как-то пытался сделать небольшой цикл про PyTest на другой платформе, но застрял, кажется, на втором. Попробуем ещё разок.


В проекте, на котором я сейчас работаю как Software Test AutomationEngineer в качестве основы для тестового фреймворка используется PyTest. Фреймворк отличный и очень мощный, но, на первый взгляд, может показаться достаточно сложным, и, с, опять же, на первый взгляд, отвратительной, совершенно непонятной документацией.

Сначала расскажу в двух словах о структуре PyTest-проекта. Можно считать, что минимально он состоит из файла conftest.py, в котором по умолчанию хранятся т.н. "фикстуры" и, собственно, файла с тестами, например test_intro.py. Файлы __init__.py пустые и нужны в основном чтобы “поддерживать” структуру фреймворка.

pytest_intro
├── __init__.py
└── tests
    ├── __init__.py
    ├── conftest.py
    └── test_intro.py

Фикстуры — это функции, выполняемые pytest до (а иногда и после) фактических тестовых функций. Код в фикстуре может делать все, что вам необходимо. Вы можете использовать фикстуры, чтобы получить набор данных для тестирования. Вы можете использовать фикстуры, чтобы получить систему в известном состоянии перед запуском теста. Фикстуры также используются для получения данных для нескольких тестов.

Итак, рассмотрим наш conftest.py

import time
import pytest


@pytest.fixture(scope='class', autouse=True)
def suite_data():
    print("\n> Suite setup")
    yield
    print("> Suite teardown")


@pytest.fixture(scope='function')
def case_data():
    print("   > Case setup")
    yield time.time()
    print("\n   > Case teardown")

Здесь мы видим две функции (suite_data() и case_data()), которые и являются фикстурами. Об этом нам говорит декоратор @pytest.fixture. У этого декоратора есть довольно много параметров, но главным является scope. Scope говорит об "уровне", на котором будет выполнена фикстура. Всего их 4:

  • "session" — означает, что данная фикстура будет выполняться для всей тестовой сессии;
  • "module" — означает, что данная фикстура будет выполняться для каждого тестового модуля (читай "файла с тестами"), который мы будем запускать;
  • "class" — означает, что данная фикстура будет выполняться для каждого сьюта (читай "класса"), который мы будем запускать;
  • "function" — означает, что данная фикстура будет выполняться для каждого конкретного теста. Это значение является значением по умолчанию и его можно не указывать.

В нашем примере, фикстура “suite_data()” будет выполнена для каждого тест сьюта. Внутри сьютов содержатся тест кейсы, для которых будет выполнена фикстура “case_data()” если она указана как входной параметр для кейса.

Из фикстуры можно передать значение в сьют или кейс с помощью оператора yield. При этом после yield можно добавить ещё код, который будет выполнен после кейса. Таким образом можно сказать, что всё, что идёт до оператора yield является “setup” частью (подготовительной), а всё, что после — “teardown” (пост-часть). yield, к слову, может ничего и не возвращать (к в suite_data()), а просто быть разделителем, отделяющим сетап от тирдауна и это потрясающе удобно на практике.

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

Ну и напоследок напишем кейс, который будет забирать таймстемп из фикстуры case_data, печатать его и проверять чётность числа. Если число чётное результат будет PASSED, если нет — FAILED. Отступы с символами “>” добавлены специально чтобы было лучше видно как “иерархически” выполняются фикстуры.

Ниже приведено содержимое файла test_intro.py

import pytest


class TestSuite():

    def test_case_1(self, case_data):
        timestamp = int(case_data)
        print(f"      > Received from fixture timestamp is: {timestamp}")
        assert(timestamp % 2 == 0)

В тесте мы сначала приводим к целочисленному значению то, что нам возвращает фикстура case_data и присваиваем это значение переменной timestamp, затем печатаем строку, которая содержит этот timestamp для наглядности и в конце проверяем является ли timestamp чётным числом.

Запускается это всё из консоли командой

pytest -v -s tests/testfile_name.py 

где -v — “verbose” режим, а -s — перенаправляет аутпут в консоль.

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

pytest -vs tests/testfile_name.py::TestSuiteName::test_case_name
Пример результата выполнения

В аутпуте хорошо видно в какой очерёдности выводятся строки из suite_data(), case_data() и нашего теста. Получается, что фикстуры как матрёшки "оборачивают" тест и друг друга. Сначала фикстура со скоупом function "оборачивает" тест кейс, потом фикстура со скоупом class "оборачивает" тест сьют и так далее до session.


На этом, кажется, всё. В следующем посте я постараюсь рассказать про передачу параметров командной строки в pytest.

Stay tuned, так сказать