Привет, друзья!
Сегодня я хочу рассказать о pytest и о том, как с ним начать работать. Сам когда-то начинал и столкнулся со множеством сложностей, но теперь я готов поделиться своим опытом.
Pytest
Pytest — это первое, с чем сталкивается любой тестировщик, который хочет начать автоматизировать и развиваться в этой области. Многие компании строят автоматизацию на Pytest, и на собеседованиях требуют его понимания. Поэтому, чтобы устроиться в такую компанию, нужно изучить Pytest.
Давайте разберёмся, что умеет этот фреймворк, на примере простых конструкций, которые часто советуют тестировщикам.
Вот код, с которым мы будем работать. Писать и запускать его будем в GigaIDE.
# myProject/cashReceipt/cashReceipt.py
class Receipt:
    """Класс для создания чека и наполнения его данными о покупках"""
    __instance = None
    __receipt_id = 0
    def __new__(cls, *args, **kwargs):
        cls.__instance = super().__new__(cls)
        cls.__receipt_id += 1
        return cls.__instance
    def __init__(self, name, surname, patronymic, date, time, total):
        self.name = name
        self.surname = surname
        self.patronymic = patronymic
        self.date = date
        self.time = time
        self.total = total
        self.items = []
    @classmethod
    def get_receipt_id(cls):
        return cls.__receipt_id
    @property
    def id(self):
        return self.get_receipt_id()
    def add_item(self, item):
        self.items.append(item)
    def print_receipt(self):
        print("Чек №", self.id)
        print("Покупатель:", self.name, self.surname, self.patronymic)
        print("Дата:", self.date)
        print("Время:", self.time)
        print("Итого:", self.total)
        for item in self.items:
            print(item.name, item.price, item.quantity)
class Item:
    """Класс для создания товара, его цены за штуку или 100г и количество"""
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity
if __name__ == "__main__":
    receipt = Receipt("Иван", "Иванов", "Иванович", "12.12.2020", "12:00", 100)
    receipt.add_item(Item("Молоко", 50, 2))
    receipt.add_item(Item("Хлеб", 30, 1))
    receipt.print_receipt()
    receipt2 = Receipt("Петр", "Петров", "Петрович", "12.12.2020", "12:00", 100)
    receipt2.add_item(Item("Молоко", 50, 2))
    receipt2.add_item(Item("Хлеб", 30, 1))
    receipt2.print_receipt()
    Первый запуск Pytest
Есть несколько способов запустить программу на Python. Один из них:
pytest cachReceipt/cachReceipt.pyМагия в том, что при таком запуске интерпретация кода меняется, и результат запуска тоже меняется:
============================== test session starts ==============================
platform win32 -- Python 3.12.0, pytest-8.3.4, pluggy-1.5.0
rootdir: C:\Users\Всеволод\IdeaProjects\pyTest
collected 0 items
============================= no tests ran in 0.01s =============================Тесты не найдены.
Второй запуск Pytest
Для второго запуска нам нужно создать файл, в котором мы реализуем проверку по всем правилам pytest. Для этого нужно импортировать cashReceipt.py в файл с проверкой. В этом примере я использовал универсальный способ импорта, который не зависит от IDE и зависит только от ОС (различия только в том, как будут прописаны пути).
# myProject/tests/testCashReceipt.py
import pytest
import sys
sys.path.append("/home/user/IdeaProjects/myProject/cashReceipt")
from cashReceipt.cashReceipt import Receipt, Item
receipt = Receipt("Иван", "Иванов", "Иванович", "12.12.2020", "12:00", 100)
receipt.add_item(Item("Молоко", 50, 2))
receipt.add_item(Item("Хлеб", 30, 1))
items = receipt.items
@pytest.mark.parametrize("item", receipt.items)
def test_cashReceipt(item):
    assert item.price <= 100
    Чтобы такой импорт заработал, нужно настроить файл _ _init_ _.py, который делает из обычного каталога python-пакет.
# myProject/cashReceipt/__init__.py
from cashReceipt import *
Теперь становится понятно, как можно тестировать объекты с разными параметрами на входе. Мы можем протестировать (покрыть) все атрибуты и методы класса, который создаёт чеки. А если мы будем использовать несколько классов (например, класс «кошелёк»), то сможем делать интеграционные проверки (проверять взаимодействия разных классов между собой).
Конструкция @pytest.mark.parametrize освоена. Более подробно о ней в интернете много доступной информации.
Результат запуска:
============================= test session starts =============================
collecting ... collected 1 item
testCashReceipt.py::test_cash_receipt[receipt0] PASSED                   [100%]
============================== 1 passed in 0.01s ==============================Но код в листинге выше не в стиле pytest, ему не хватает фикстур.
Третий запуск Pytest
Давайте создадим рядом с файлом тестирования файл conftest.py. Этот файл будет хранить объекты, которые мы тестируем.
# myProject/tests/conftest.py
import pytest
import sys
sys.path.append("/home/user/IdeaProjects/myProject/cashReceipt")
from cashReceipt.cashReceipt import Receipt, Item
@pytest.fixture
def receipt(request):
    return Receipt(*request.param)
@pytest.fixture
def item(request):
    return Item(*request.param)
И уберём лишний код из testCashReceipt
# myProject/tests/testCashReceipt.py
import pytest
@pytest.mark.parametrize("receipt", [("Иван", "Иванов", "Иванович", "12.12.2020", "12:00", 100)], indirect=True)
def test_cash_receipt(receipt):
    assert receipt is not None
@pytest.mark.parametrize("item", [("Молоко", 50, 2), ("Хлеб", 30, 1)], indirect=True)
def test_cash_item(item):
    assert item is not None
Не буду останавливаться на фикстурах, потому что в интернете и так полно о них информации.
А вот и результат нашего третьего запуска:
============================= test session starts =============================
collecting ... collected 3 items
testCashReceipt.py::test_cash_receipt[receipt0] PASSED                   [ 33%]
testCashReceipt.py::test_cash_item[item0] PASSED                         [ 66%]
testCashReceipt.py::test_cash_item[item1] PASSED                         [100%]
============================== 3 passed in 0.01s ==============================Итог
В результате, всего за три запуска мы поняли, как сделать проверки на pytest, сохранить принципы ООП и всё структурировать. Класс!
Бонус
Структура проекта:
myProject
|-- cashReceipt
|   |-- __init__.py
|   |-- cashReceipt.py
|-- tests
    |-- __init__.py
    |-- conftest.py
    |-- testCashReceipt.pyКомментарии (8)
 - edta_ff26.01.2025 03:41- Pytest — это первое, с чем сталкивается любой тестировщик, который хочет начать автоматизировать и развиваться в этой области. - Очень спорное утверждение. Столкнулся с Pytest уже после автоматизации на Ruby, Java, C#. Правильнее было бы написать, что это было первое , с чем вы столкнулись. - Магия в том, что при таком запуске интерпретация кода меняется, и результат запуска тоже меняется. Тесты не найдены. - Очень плохая идея объяснять чего-то новичку с помощью магии. Тем более на таком неочевидном примере. Можно взять самый просто тест без кучи классов, свойств и сразу показать как его запустить. Начинать же пример с того, как тесты не запустятся - сомнительная идея. - Добавление init.py в каждую директорию плохая практика. - Теперь становится понятно, как можно тестировать объекты с разными параметрами на входе. - Нет. Не становится. - В результате, всего за три запуска мы поняли, как сделать проверки на pytest, сохранить принципы ООП и всё структурировать. Класс! - Мы не поняли.  - mrmcmva Автор26.01.2025 03:41- Этот туториал для самостоятельных людей, которые не ищут готовых решений, а хотят разобраться. Поэтому и не стал разжевывать вещи, которые не относятся к сути содержимого. - Если последовательно выполнить всё, что написано, то должно прийти понимание, если понимание не приходит, то можно задать конкретный вопрос в комментарии или разобраться самому. - Но спасибо за критику, она тоже полезна, как для авторов, так и для читателей. 
 
 
           
 
rexer
Мне кажется, вариант с абсолютным путем зашитым в коде практически никогда не должен использоваться: в разработке многие используют разные ОС, плюс при переносе в CI/CD надо будет менять снова код.
mrmcmva Автор
Согласен, но в контексте данной статьи это не важно. Можно вынести путь в проперти, или прописать относительные пути, но это никак не меняет суть изложенного материала.
rexer
Не соглашусь, так как вы же показываете новичку это и явно то, что максимально не будет применяться и по сути является ошибочным показывать не стоит - чтобы не смущать. Лучше показать правильные и используемые варианты (кмк).
mrmcmva Автор
Согласен с вами, учту на будущее. Мастерством нужно прирастать и в этом компоненте. Спасибо за замечания!