Python · Тестування з pytest · Просунутий

Фабрики фікстур та тестові дані

5 завдань

Будуйте гнучкі тестові дані за допомогою фабричних фікстур та бібліотеки Faker.

Фабричний шаблон: фікстури, що створюють дані на вимогу

#
## Обмеження фікстур з фіксованими значеннями Звичайна фікстура повертає єдине фіксоване значення. Кожен тест, що її запитує, отримує той самий об'єкт: ```python @pytest.fixture def user(): return {'username': 'alice', 'email': '[email protected]', 'role': 'admin'} ``` Це добре працює, коли одному тесту потрібен один користувач. Але ламається, щойно виникає потреба у варіаціях: ```python def test_duplicate_username(user): user_a = user # {'username': 'alice', ...} user_b = user # той самий словник -- немає способу отримати іншого користувача assert user_a['username'] != user_b['username'] # FAILS: 'alice' == 'alice' ``` Можна написати окремі фікстури (`user_alice`, `user_bob`, `user_guest`), але це призводить до розростання фікстур -- десятки майже однакових фікстур для кожної комбінації полів. ## Фабричний шаблон: фікстура, що повертає callable Замість повернення значення фікстура повертає *фабричну функцію*. Тести викликають фабрику з аргументами, щоб створити скільки завгодно налаштованих об'єктів: ```python @pytest.fixture def make_user(): def _factory(username='alice', email=None, role='user'): if email is None: email = f'{username}@example.com' return {'username': username, 'email': email, 'role': role} return _factory ``` ```python def test_duplicate_username(make_user): user_a = make_user(username='alice') user_b = make_user(username='bob') assert user_a['username'] != user_b['username'] # PASSES ``` Фікстура (`make_user`) виконується один раз і повертає фабричну функцію. Кожен виклик `make_user(...)` всередині тесту створює свіжий словник із запитаними полями, використовуючи розумні дефолти для всього, що не вказано. ## Чому дефолти важливі Хороші дефолти фабрики дозволяють коду тесту виражати лише *відповідну* варіацію: ```python def test_admin_can_delete(make_user): admin = make_user(role='admin') # тут важлива лише роль assert can_delete(admin) is True def test_guest_cannot_delete(make_user): guest = make_user(role='guest') assert can_delete(guest) is False ``` Поля `username` і `email` нерелевантні для цих тестів. З дефолтами їх не потрібно вказувати. Якби потрібно було 50 тест-кейсів, кожен з різною роллю, -- не довелося б писати 50 фікстур, достатньо параметризувати виклик фабрики. ## Faker: реалістичні тестові дані Бібліотека `faker` генерує реалістичні фейкові дані -- імена, email-адреси, поштові адреси, номери телефонів, lorem ipsum: ```bash pip install faker ``` ```python from faker import Faker fake = Faker() print(fake.name()) # 'Emily Rodriguez' print(fake.email()) # '[email protected]' print(fake.user_name()) # 'jsmith42' print(fake.pyint(min_value=1, max_value=1000)) # 847 ``` Інтеграція Faker у фабричну фікстуру дає унікальні реалістичні значення за замовчуванням: ```python from faker import Faker import pytest @pytest.fixture def make_user(): fake = Faker() def _factory(**kwargs): defaults = { 'username': fake.user_name(), 'email': fake.email(), 'first_name': fake.first_name(), 'last_name': fake.last_name(), } defaults.update(kwargs) return defaults return _factory ``` Тепер кожен виклик `make_user()` виробляє унікального користувача з різними згенерованими іменем і email, уникаючи випадкових колізій даних між тестами. ## Відтворюваність: seeding Faker Вивід Faker за замовчуванням є випадковим. Якщо тест провалюється, відтворення збою потребує тих самих даних. Встановіть seed Faker на початку тестового сеансу для детермінованого виводу: ```python @pytest.fixture(scope='session', autouse=True) def seed_faker(): Faker.seed(42) # однаковий seed → однакова послідовність фейкових даних при кожному запуску ``` З фіксованим seed послідовність згенерованих значень є детермінованою: 3-й виклик `make_user()` у вашому тест-сьюті завжди виробляє те саме ім'я користувача. Без seed випадкові дані в тестах можуть спричиняти нестабільні збої, які важко відтворити. ## Коли використовувати фабрики, а коли звичайні фікстури | Ситуація | Використовуйте | |---|---| | Кожному тесту потрібен той самий об'єкт | Звичайний `@pytest.fixture` | | Тестам потрібні об'єкти з незначними варіаціями | Фабрична фікстура | | Тести створюють кілька об'єктів одного типу | Фабрична фікстура | | Складні вкладені структури з багатьма дефолтами | Фабрична фікстура | | Просте скалярне значення (URL, конфіг, число) | Звичайний `@pytest.fixture` | Фабричний шаблон додає рівень непрямості. Використовуйте його, коли ця непрямість виправдана -- коли тестам справді потрібні різні екземпляри, а не просто різні посилання на той самий. ## Композиція фабрик Фабрики можуть викликати інші фабрики для побудови вкладених структур: ```python @pytest.fixture def make_order(make_user, make_product): def _factory(num_items=1, **kwargs): defaults = { 'user': make_user(), 'items': [make_product() for _ in range(num_items)], 'status': 'pending', } defaults.update(kwargs) return defaults return _factory ``` ```python def test_order_total(make_order, make_product): product = make_product(price=10.00) order = make_order(items=[product, product]) assert calculate_total(order) == 20.00 ``` Фікстура `make_order` залежить від `make_user` і `make_product` -- стандартне ланцюжкування фікстур. pytest автоматично розв'язує граф залежностей.

Фабричні фікстури на практиці: користувачі, продукти та Faker

#
## Налаштування проекту ```bash pip install faker pytest ``` ## Приклад 1: базова фабрика make_user ```python # conftest.py import pytest @pytest.fixture def make_user(): created = [] def _factory(username='testuser', email=None, role='user', age=25): if email is None: email = f'{username}@example.com' user = {'username': username, 'email': email, 'role': role, 'age': age} created.append(user) return user return _factory ``` Фабрика відстежує всіх створених користувачів у `created` -- корисно, якщо потрібно очистити їх у teardown (наприклад, видалити з БД після тесту). Для простих словників teardown не потрібен, але шаблон відстеження стає необхідним, коли фабрики створюють реальні рядки БД або API-ресурси. ```python # test_users.py def test_user_defaults(make_user): user = make_user() assert user['username'] == 'testuser' assert user['email'] == '[email protected]' assert user['role'] == 'user' def test_user_custom_role(make_user): admin = make_user(username='admin', role='admin') assert admin['role'] == 'admin' def test_two_users_in_one_test(make_user): buyer = make_user(username='buyer') seller = make_user(username='seller') assert buyer['username'] != seller['username'] ``` ## Приклад 2: інтеграція Faker для реалістичних даних ```python # conftest.py import pytest from faker import Faker @pytest.fixture(scope='session', autouse=True) def seed_faker(): Faker.seed(0) # відтворюваність між запусками @pytest.fixture def make_user(): fake = Faker() def _factory(**kwargs): username = fake.user_name() defaults = { 'username': username, 'email': fake.email(), 'first_name': fake.first_name(), 'last_name': fake.last_name(), 'bio': fake.text(max_nb_chars=100), } defaults.update(kwargs) return defaults return _factory ``` ```python # test_faker_users.py def test_users_have_unique_usernames(make_user): users = [make_user() for _ in range(5)] usernames = [u['username'] for u in users] assert len(set(usernames)) == len(usernames) # всі унікальні def test_email_override(make_user): user = make_user(email='[email protected]') assert user['email'] == '[email protected]' assert 'username' in user # інші поля ще генеруються Faker ``` Session-фікстура з `autouse=True` встановлює seed Faker один раз на початку запуску. Кожен наступний виклик `fake.user_name()` виробляє ту саму послідовність при кожному запуску -- критично для відтворення провалів тестів. ## Приклад 3: фабрика make_product ```python # conftest.py (продовження) @pytest.fixture def make_product(): fake = Faker() def _factory(**kwargs): defaults = { 'name': fake.catch_phrase(), 'slug': fake.slug(), 'price': round(fake.pyfloat(min_value=1, max_value=999, right_digits=2), 2), 'in_stock': True, } defaults.update(kwargs) return defaults return _factory ``` ```python # test_products.py def test_three_products(make_product): products = [ make_product(name='Widget A', price=9.99), make_product(name='Widget B', price=19.99), make_product(name='Widget C', price=4.99), ] assert len(products) == 3 assert all(p['in_stock'] for p in products) total = sum(p['price'] for p in products) assert round(total, 2) == 34.97 def test_out_of_stock(make_product): product = make_product(in_stock=False) assert product['in_stock'] is False ``` ## Приклад 4: фабрика у поєднанні з parametrize ```python # test_roles.py import pytest @pytest.mark.parametrize('role,can_admin', [ ('admin', True), ('editor', False), ('viewer', False), ]) def test_admin_permission(make_user, role, can_admin): user = make_user(role=role) assert check_admin_permission(user) is can_admin def check_admin_permission(user): return user['role'] == 'admin' ``` `parametrize` забезпечує варіацію; фабрика -- об'єкт. Це поєднання читабельніше, ніж фікстура, що намагається вбудувати всі варіації всередину себе. ## Приклад 5: вкладена фабрика (замовлення з позиціями) ```python # conftest.py (продовження) @pytest.fixture def make_order(make_user, make_product): def _factory(num_items=1, **kwargs): defaults = { 'user': make_user(), 'items': [make_product() for _ in range(num_items)], 'status': 'pending', 'discount': 0.0, } defaults.update(kwargs) return defaults return _factory ``` ```python # test_orders.py def test_order_item_count(make_order): order = make_order(num_items=3) assert len(order['items']) == 3 def test_order_with_custom_item(make_order, make_product): expensive = make_product(price=999.99) order = make_order(items=[expensive]) assert order['items'][0]['price'] == 999.99 ```

Довідникова картка: фабричні фікстури та Faker

#
## Шаблон фабричної фікстури ```python import pytest from faker import Faker @pytest.fixture(scope='session', autouse=True) def seed_faker(): Faker.seed(0) # видаліть для випадкових даних при кожному запуску @pytest.fixture def make_<model>(): fake = Faker() def _factory(**kwargs): defaults = { 'field1': fake.some_provider(), 'field2': 'sensible_default', } defaults.update(kwargs) # перевизначення від викликача мають пріоритет return defaults return _factory ``` ## Faker: найбільш вживані провайдери | Провайдер | Приклад виводу | |---|---| | `fake.name()` | `'Emily Rodriguez'` | | `fake.first_name()` | `'Emily'` | | `fake.last_name()` | `'Rodriguez'` | | `fake.user_name()` | `'emily_r42'` | | `fake.email()` | `'[email protected]'` | | `fake.slug()` | `'fast-blue-widget'` | | `fake.text(max_nb_chars=100)` | речення lorem ipsum | | `fake.url()` | `'https://example.com/path'` | | `fake.pyfloat(min_value=1, max_value=100, right_digits=2)` | `47.83` | | `fake.pyint(min_value=1, max_value=1000)` | `342` | | `fake.boolean()` | `True` / `False` | | `fake.uuid4()` | `'3d...a1'` | ## Шаблони використання ```python # Один об'єкт user = make_user() # З перевизначеннями admin = make_user(role='admin') # Кілька об'єктів products = [make_product() for _ in range(5)] # Вкладений order = make_order(num_items=3) # У поєднанні з parametrize @pytest.mark.parametrize('role', ['admin', 'editor', 'viewer']) def test_role(make_user, role): user = make_user(role=role) ... ``` ## Коли який підхід використовувати | Потреба | Рішення | |---|---| | Один фіксований об'єкт для всіх тестів | Звичайний `@pytest.fixture` | | Кілька об'єктів з варіаціями | Фабрична фікстура | | Реалістично виглядаючі дані | Фабрика + Faker | | Відтворювані випадкові дані | `Faker.seed(n)` у autouse session-фікстурі | | Вкладені структури | Компонуйте фабрики (фабрика викликає іншу фабрику) |
01

Написання фабричної фікстури make_user

#

Напишіть pytest-фікстуру `make_user`, що повертає фабричну функцію. Фабрика має: - Приймати аргументи-ключові слова: `username`, `email`, `role`, `age` - Використовувати розумні дефолти: `username='testuser'`, `email` виводиться з username якщо не задано, `role='user'`, `age=25` - Повертати словник з усіма чотирма полями Напишіть два тести, що її використовують: 1. `test_default_user` -- створіть користувача без аргументів, перевірте `role == 'user'` і що email містить username 2. `test_two_users` -- створіть двох користувачів з різними username, перевірте, що вони різні

# test_factory_basic.py
import pytest


@pytest.fixture
def make_user():
    def _factory(username='testuser', email=None, role='user', age=25):
        # якщо email is None, виведіть його з username
        ...
        return ...
    return _factory


def test_default_user(make_user):
    user = make_user()
    assert user['role'] == 'user'
    assert ...  # email містить username


def test_two_users(make_user):
    a = make_user(username='alice')
    b = make_user(username='bob')
    assert ...
Рішення
# test_factory_basic.py
import pytest


@pytest.fixture
def make_user():
    def _factory(username='testuser', email=None, role='user', age=25):
        if email is None:
            email = f'{username}@example.com'
        return {'username': username, 'email': email, 'role': role, 'age': age}
    return _factory


def test_default_user(make_user):
    user = make_user()
    assert user['role'] == 'user'
    assert user['username'] in user['email']


def test_two_users(make_user):
    a = make_user(username='alice')
    b = make_user(username='bob')
    assert a['username'] != b['username']
02

Додавання Faker для реалістично згенерованих даних

#

Встановіть `faker` і оновіть `make_user` для генерації реалістичних фейкових даних за замовчуванням: - `username` з `fake.user_name()` - `email` з `fake.email()` - `first_name` і `last_name` з `fake.first_name()` / `fake.last_name()` Всі поля мають бути перевизначуваними через аргументи-ключові слова. Додайте session-scoped `autouse`-фікстуру, що викликає `Faker.seed(0)` для відтворюваного виводу. Напишіть тест `test_users_have_unique_emails`, що створює 5 користувачів без аргументів і перевіряє, що всі їхні email-адреси різні.

# test_faker_factory.py
import pytest
from faker import Faker


@pytest.fixture(scope='session', autouse=True)
def seed_faker():
    Faker.seed(0)


@pytest.fixture
def make_user():
    fake = Faker()

    def _factory(**kwargs):
        defaults = {
            'username': ...,
            'email': ...,
            'first_name': ...,
            'last_name': ...,
        }
        defaults.update(kwargs)
        return defaults

    return _factory


def test_users_have_unique_emails(make_user):
    users = [make_user() for _ in range(5)]
    emails = [u['email'] for u in users]
    assert len(set(emails)) == len(emails)
Рішення
# test_faker_factory.py
import pytest
from faker import Faker


@pytest.fixture(scope='session', autouse=True)
def seed_faker():
    Faker.seed(0)


@pytest.fixture
def make_user():
    fake = Faker()

    def _factory(**kwargs):
        defaults = {
            'username': fake.user_name(),
            'email': fake.email(),
            'first_name': fake.first_name(),
            'last_name': fake.last_name(),
        }
        defaults.update(kwargs)
        return defaults

    return _factory


def test_users_have_unique_emails(make_user):
    users = [make_user() for _ in range(5)]
    emails = [u['email'] for u in users]
    assert len(set(emails)) == len(emails)
03

Написання фабрики make_product та створення кількох продуктів

#

Напишіть фабричну фікстуру `make_product`. Кожен продукт має мати: - `name` -- рядок (використайте Faker або простий дефолт `'Product'`) - `slug` -- виводиться з name, якщо не задано (нижній регістр, пробіли → дефіси) - `price` -- float, дефолт `9.99` - `in_stock` -- bool, дефолт `True` Напишіть `test_three_products(make_product)`, що: - Створює 3 продукти з цінами `5.00`, `10.00`, `20.00` - Перевіряє, що всі три є в наявності - Перевіряє, що сума їхніх цін дорівнює `35.00`

# test_products_factory.py
import pytest


@pytest.fixture
def make_product():
    def _factory(name='Test Product', slug=None, price=9.99, in_stock=True):
        if slug is None:
            slug = name.lower().replace(' ', '-')
        return {'name': name, 'slug': slug, 'price': price, 'in_stock': in_stock}
    return _factory


def test_three_products(make_product):
    products = [
        make_product(price=...),
        make_product(price=...),
        make_product(price=...),
    ]
    assert all(...)
    assert sum(p['price'] for p in products) == ...
Рішення
# test_products_factory.py
import pytest


@pytest.fixture
def make_product():
    def _factory(name='Test Product', slug=None, price=9.99, in_stock=True):
        if slug is None:
            slug = name.lower().replace(' ', '-')
        return {'name': name, 'slug': slug, 'price': price, 'in_stock': in_stock}
    return _factory


def test_three_products(make_product):
    products = [
        make_product(price=5.00),
        make_product(price=10.00),
        make_product(price=20.00),
    ]
    assert all(p['in_stock'] for p in products)
    assert sum(p['price'] for p in products) == pytest.approx(35.00)
04

Написання фабрики для вкладеної структури

#

Напишіть фабричну фікстуру `make_order`, що залежить від `make_user` і `make_product`. Замовлення має бути словником з: - `user` -- словник користувача (з `make_user()`) - `items` -- список словників продуктів (дефолт: один продукт з `make_product()`) - `status` -- рядок, дефолт `'pending'` Напишіть два тести: 1. `test_order_default_has_one_item` -- створіть замовлення з дефолтами, перевірте `len(order['items']) == 1` 2. `test_order_custom_items` -- створіть два продукти явно, передайте їх як `items`, перевірте, що замовлення має 2 елементи

# test_order_factory.py
import pytest


@pytest.fixture
def make_user():
    def _factory(**kwargs):
        defaults = {'username': 'testuser', 'email': '[email protected]', 'role': 'user'}
        defaults.update(kwargs)
        return defaults
    return _factory


@pytest.fixture
def make_product():
    def _factory(**kwargs):
        defaults = {'name': 'Widget', 'price': 9.99, 'in_stock': True}
        defaults.update(kwargs)
        return defaults
    return _factory


@pytest.fixture
def make_order(make_user, make_product):
    def _factory(**kwargs):
        defaults = {
            'user': ...,
            'items': [...],
            'status': 'pending',
        }
        defaults.update(kwargs)
        return defaults
    return _factory


def test_order_default_has_one_item(make_order):
    order = make_order()
    assert len(order['items']) == 1


def test_order_custom_items(make_order, make_product):
    p1 = make_product(name='A')
    p2 = make_product(name='B')
    order = make_order(items=[p1, p2])
    assert len(order['items']) == 2
Рішення
# test_order_factory.py
import pytest


@pytest.fixture
def make_user():
    def _factory(**kwargs):
        defaults = {'username': 'testuser', 'email': '[email protected]', 'role': 'user'}
        defaults.update(kwargs)
        return defaults
    return _factory


@pytest.fixture
def make_product():
    def _factory(**kwargs):
        defaults = {'name': 'Widget', 'price': 9.99, 'in_stock': True}
        defaults.update(kwargs)
        return defaults
    return _factory


@pytest.fixture
def make_order(make_user, make_product):
    def _factory(**kwargs):
        defaults = {
            'user': make_user(),
            'items': [make_product()],
            'status': 'pending',
        }
        defaults.update(kwargs)
        return defaults
    return _factory


def test_order_default_has_one_item(make_order):
    order = make_order()
    assert len(order['items']) == 1


def test_order_custom_items(make_order, make_product):
    p1 = make_product(name='A')
    p2 = make_product(name='B')
    order = make_order(items=[p1, p2])
    assert len(order['items']) == 2
05

Поєднання фабричної фікстури з parametrize

#

Використовуючи фабричну фікстуру `make_user`, напишіть параметризований тест, що перевіряє функцію `has_access(user, resource)`. Реалізуйте `has_access(user, resource)`: - Повертає `True`, якщо `user['role'] == 'admin'` або `resource == 'public'` - Повертає `False` в іншому випадку Параметризуйте `test_access` цими кейсами (role, resource, expected): - `('admin', 'secret', True)` - `('user', 'public', True)` - `('user', 'secret', False)` - `('guest', 'private', False)` Використовуйте `make_user(role=role)` всередині тіла тесту для створення користувача.

# test_access.py
import pytest


def has_access(user, resource):
    ...


@pytest.fixture
def make_user():
    def _factory(**kwargs):
        defaults = {'username': 'testuser', 'role': 'user'}
        defaults.update(kwargs)
        return defaults
    return _factory


@pytest.mark.parametrize('role,resource,expected', [
    ('admin', 'secret', True),
    ('user',  'public', True),
    ('user',  'secret', False),
    ('guest', 'private', False),
])
def test_access(make_user, role, resource, expected):
    user = make_user(role=role)
    assert has_access(user, resource) is expected
Рішення
# test_access.py
import pytest


def has_access(user, resource):
    return user['role'] == 'admin' or resource == 'public'


@pytest.fixture
def make_user():
    def _factory(**kwargs):
        defaults = {'username': 'testuser', 'role': 'user'}
        defaults.update(kwargs)
        return defaults
    return _factory


@pytest.mark.parametrize('role,resource,expected', [
    ('admin', 'secret', True),
    ('user',  'public', True),
    ('user',  'secret', False),
    ('guest', 'private', False),
])
def test_access(make_user, role, resource, expected):
    user = make_user(role=role)
    assert has_access(user, resource) is expected