Что такое pytest и как его использовать? Или как новичку начать автоматизировать тестирование?
Привет, друзья!
Сегодня я хочу рассказать о 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