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

Фікстури

5 завдань

Використовуйте @pytest.fixture для повторного використання налаштування без зайвого коду.

Навіщо потрібні фікстури і як вони працюють

#
**Проблема, яку вирішують фікстури:** Уявіть п'ять тест-функцій, яким потрібен заповнений об'єкт користувача. Без фікстур вам довелося б або повторювати код налаштування в кожному тесті, або зберігати спільну змінну на рівні модуля -- обидва підходи ненадійні. Фікстури pytest вирішують це чисто. Фікстура -- це функція, декорована `@pytest.fixture`. Будь-який тест, що оголошує її як параметр, автоматично отримує повернуте значення -- без імпорту, без виклику, лише ім'я: ```python import pytest @pytest.fixture def user(): return {'name': 'Alice', 'email': '[email protected]', 'active': True} def test_user_is_active(user): # pytest бачить параметр 'user' → запускає фікстуру assert user['active'] is True def test_user_has_email(user): # та сама фікстура, окремий виклик assert '@' in user['email'] ``` pytest розв'язує імена фікстур, зіставляючи імена параметрів. Якщо тест має параметр `user`, pytest знаходить фікстуру з іменем `user` і впроваджує її. Це *dependency injection за іменем*. **Патерн yield -- налаштування і teardown:** Використовуйте `yield` замість `return`, коли потрібне очищення після завершення тесту: ```python @pytest.fixture def temp_file(tmp_path): path = tmp_path / 'data.txt' path.write_text('hello world') # ← налаштування виконується до тесту yield path # ← тест отримує шлях тут path.unlink(missing_ok=True) # ← teardown виконується після тесту, навіть при провалі ``` Все до `yield` -- налаштування. Все після -- teardown. Це еквівалент `try / finally`, але виражений лінійним потоком. **Scope фікстури -- як довго вона живе:** За замовчуванням фікстура виконується один раз на тест-функцію (`scope='function'`). Це можна змінити: ```python @pytest.fixture(scope='module') def db_connection(): conn = create_expensive_connection() yield conn conn.close() ``` З `scope='module'` фікстура виконується *один раз* для всього тестового файлу, і з'єднання спільне для всіх тестів у ньому. Це корисно для ресурсомісткого налаштування -- підключення до БД або завантаження великих файлів. | Scope | Фікстура виконується один раз на | |---|---| | `function` (за замовчуванням) | кожен тест | | `class` | кожен тест-клас | | `module` | кожен тестовий файл | | `session` | весь запуск `pytest` | **Одне важливе правило:** Ніколи не викликайте функцію-фікстуру напряму в тесті. Дозвольте pytest впровадити її: ```python # НЕПРАВИЛЬНО def test_something(): u = user() # це просто викликає звичайну функцію, обходячи pytest # ПРАВИЛЬНО def test_something(user): # pytest впроваджує фікстуру ... ```

Патерни фікстур, які ви будете використовувати щодня

#
**Проста фікстура -- повертає значення:** ```python import pytest @pytest.fixture def sample_scores(): return [88, 92, 75, 100, 63, 41] def test_highest_score(sample_scores): assert max(sample_scores) == 100 def test_passing_scores(sample_scores): passing = [s for s in sample_scores if s >= 60] assert len(passing) == 4 ``` Кожен виклик тесту запускає фікстуру заново. Зміна `sample_scores` в одному тесті не впливає на інший. **Фікстура з yield -- налаштування + teardown:** ```python @pytest.fixture def temp_config(tmp_path): config_file = tmp_path / 'config.json' config_file.write_text('{"debug": true, "timeout": 30}') yield config_file # тут виконується тест # teardown -- файл вже прибирається tmp_path, але тут можна зробити більше def test_config_readable(temp_config): content = temp_config.read_text() assert 'debug' in content def test_config_is_file(temp_config): assert temp_config.is_file() ``` **Фікстура, що залежить від іншої фікстури:** ```python @pytest.fixture def base_url(): return 'https://api.example.com' @pytest.fixture def api_headers(): return {'Content-Type': 'application/json'} @pytest.fixture def api_client(base_url, api_headers): return {'url': base_url, 'headers': api_headers, 'timeout': 10} def test_client_config(api_client): assert api_client['url'].startswith('https') assert 'Content-Type' in api_client['headers'] assert api_client['timeout'] == 10 ``` pytest будує граф залежностей автоматично. Якщо `api_client` потребує `base_url` і `api_headers`, вони виконуються першими. **Module-scoped фікстура для ресурсомісткого налаштування:** ```python @pytest.fixture(scope='module') def large_dataset(): # виконується один раз для всього файлу, а не для кожного тесту data = list(range(100_000)) return data def test_dataset_size(large_dataset): assert len(large_dataset) == 100_000 def test_dataset_max(large_dataset): assert max(large_dataset) == 99_999 ``` **Використання вбудованих фікстур:** pytest надає корисні фікстури безкоштовно. `tmp_path` дає унікальну тимчасову директорію на тест: ```python def test_write_and_read(tmp_path): f = tmp_path / 'output.txt' f.write_text('hello') assert f.read_text() == 'hello' ``` `monkeypatch` дозволяє безпечно замінювати атрибути або змінні середовища: ```python import os def test_with_env(monkeypatch): monkeypatch.setenv('API_KEY', 'test-key-123') assert os.environ['API_KEY'] == 'test-key-123' # оригінальне середовище відновлюється автоматично після тесту ```

Довідник: фікстури

#
```python # Повернути значення @pytest.fixture def my_fixture(): return value # Налаштування + teardown з yield @pytest.fixture def my_fixture(): resource = setup() yield resource # тут виконується тест cleanup(resource) # завжди виконується, навіть при провалі тесту # Фікстура зі scope (виконується один раз на модуль, спільна для всіх тестів) @pytest.fixture(scope='module') def expensive_data(): return load_big_dataset() ``` **Таблиця scope:** | Scope | Запускається один раз на... | |-------|-----------------------------| | `function` (за замовчуванням) | тест-функцію | | `class` | клас | | `module` | файл | | `session` | весь запуск | **Вбудовані фікстури:** ```python tmp_path # pathlib.Path до унікальної тимчасової директорії (function scope) tmp_path_factory # фабрика тимчасових директорій (для session-scoped фікстур) capsys # перехоплення та перевірка stdout/stderr monkeypatch # безпечна заміна об'єктів, змінних середовища, записів словників request # інформація про поточний тест (ім'я, модуль тощо) ```
01

Фікстура зі списком чисел

#

Напишіть фікстуру `numbers`, яка повертає `[3, 1, 4, 1, 5, 9, 2, 6]`. Напишіть дві тест-функції, що використовують цю фікстуру: `test_sum` перевіряє, що сума дорівнює `31`, і `test_max` -- що максимум дорівнює `9`.

import pytest


@pytest.fixture
def numbers():
    pass  # поверніть список


def test_sum(numbers):
    pass


def test_max(numbers):
    pass
Рішення
import pytest


@pytest.fixture
def numbers():
    return [3, 1, 4, 1, 5, 9, 2, 6]


def test_sum(numbers):
    assert sum(numbers) == 31


def test_max(numbers):
    assert max(numbers) == 9
02

Yield-фікстура з tmp_path

#

Напишіть фікстуру `temp_csv`, яка використовує вбудовану фікстуру `tmp_path` для створення CSV-файлу з двома рядками вмісту. Використовуйте `yield` для передачі шляху до файлу тестам. Напишіть тест, який перевіряє кількість рядків і заголовок.

import pytest


@pytest.fixture
def temp_csv(tmp_path):
    path = tmp_path / 'data.csv'
    # запишіть два рядки до path
    yield path
    # очищення


def test_csv_has_two_lines(temp_csv):
    pass
Рішення
import pytest


@pytest.fixture
def temp_csv(tmp_path):
    path = tmp_path / 'data.csv'
    path.write_text('name,age\nAlice,30\n')
    yield path
    if path.exists():
        path.unlink()


def test_csv_has_two_lines(temp_csv):
    lines = temp_csv.read_text().strip().splitlines()
    assert len(lines) == 2

def test_csv_header(temp_csv):
    first_line = temp_csv.read_text().splitlines()[0]
    assert first_line == 'name,age'
03

Фікстура зі словником конфігурації

#

Напишіть `@pytest.fixture` з назвою `app_config`, що повертає `{'base_url': 'https://api.example.com', 'timeout': 10, 'retries': 3}`. Напишіть функцію `build_url(config, path)`, яка об'єднує `base_url` і `path` без подвійних слешів. Напишіть три тести з використанням фікстури.

import pytest


@pytest.fixture
def app_config():
    pass  # поверніть словник конфігурації


def build_url(config, path):
    pass  # об'єднайте base_url і path


def test_build_url(app_config):
    pass
Рішення
import pytest


@pytest.fixture
def app_config():
    return {'base_url': 'https://api.example.com', 'timeout': 10, 'retries': 3}


def build_url(config, path):
    return config['base_url'].rstrip('/') + '/' + path.lstrip('/')


def test_build_url(app_config):
    url = build_url(app_config, '/users')
    assert url == 'https://api.example.com/users'

def test_build_url_no_double_slash(app_config):
    url = build_url(app_config, 'products')
    assert '//' not in url.split('https://')[-1]

def test_config_timeout(app_config):
    assert app_config['timeout'] == 10
04

Дві фікстури в одному тесті

#

Створіть дві фікстури: `base_url`, що повертає `'https://api.example.com'`, та `headers`, що повертає `{'Content-Type': 'application/json', 'Accept': 'application/json'}`. Напишіть тест, що використовує обидві та перевіряє, що URL є HTTPS і заголовки містять правильні ключі.

import pytest


@pytest.fixture
def base_url():
    pass


@pytest.fixture
def headers():
    pass


def test_request_setup(base_url, headers):
    pass
Рішення
import pytest


@pytest.fixture
def base_url():
    return 'https://api.example.com'


@pytest.fixture
def headers():
    return {'Content-Type': 'application/json', 'Accept': 'application/json'}


def test_request_setup(base_url, headers):
    assert base_url.startswith('https://')
    assert 'Content-Type' in headers
    assert headers['Content-Type'] == 'application/json'
    assert 'Accept' in headers
05

Тришарова ланцюжок фікстур

#

Напишіть три пов'язані фікстури: `db_config` повертає словник з `host` і `port`, `db_connection(db_config)` повертає `{'status': 'connected', 'host': ..., 'port': ...}`, `db_cursor(db_connection)` повертає `{'query': None, 'connection': db_connection}`. Напишіть тест, що запитує лише `db_cursor` і перевіряє значення через ланцюжок.

import pytest


@pytest.fixture
def db_config():
    pass


@pytest.fixture
def db_connection(db_config):
    pass


@pytest.fixture
def db_cursor(db_connection):
    pass


def test_cursor_ready(db_cursor):
    pass
Рішення
import pytest


@pytest.fixture
def db_config():
    return {'host': 'localhost', 'port': 5432}


@pytest.fixture
def db_connection(db_config):
    return {'status': 'connected', 'host': db_config['host'], 'port': db_config['port']}


@pytest.fixture
def db_cursor(db_connection):
    return {'query': None, 'connection': db_connection}


def test_cursor_ready(db_cursor):
    assert db_cursor['query'] is None
    assert db_cursor['connection']['status'] == 'connected'
    assert db_cursor['connection']['host'] == 'localhost'