Что такое pytest и как его использовать? Или как новичку начать автоматизировать тестирование?

80b8fcfecb950542fe57f0d75f4cad88.png

Привет, друзья!

Сегодня я хочу рассказать о 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 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 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
    |-- testCashReceipt.py

© Habrahabr.ru