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

Параметризовані тести

5 завдань

Запускайте одну тест-функцію з кількома вхідними даними за допомогою @pytest.mark.parametrize.

Перестаньте повторюватися: параметризовані тести

#
Патерн написання трьох окремих тест-функцій, що викликають одну функцію з різними вхідними даними, настільки поширений, що має назву: *дублювання тестів*. pytest пропонує кращий спосіб: `@pytest.mark.parametrize`. Без parametrize: ```python def test_square_two(): assert square(2) == 4 def test_square_three(): assert square(3) == 9 def test_square_negative(): assert square(-2) == 4 ``` З parametrize: ```python import pytest @pytest.mark.parametrize("n, expected", [ (2, 4), (3, 9), (-2, 4), ]) def test_square(n, expected): assert square(n) == expected ``` Три тести стають одним. Додайте більше кейсів, додавши рядки до списку. **Розбір синтаксису:** ```python @pytest.mark.parametrize( "param_names", # рядок з іменами параметрів через кому [ # список значень (або кортежів для кількох параметрів) value1, value2, ] ) def test_function(param_names): ... ``` **Автоматично згенеровані IDs тестів:** pytest створює унікальний ID для кожного кейсу. Запуск `pytest -v` показує: ``` test_math.py::test_square[2-4] PASSED test_math.py::test_square[3-9] PASSED test_math.py::test_square[-2-4] PASSED ``` Формат -- `[param1-param2-...]`. Якщо параметр є рядком, pytest використовує значення рядка безпосередньо. **Власні IDs:** ```python @pytest.mark.parametrize( "n, expected", [ (2, 4), (3, 9), (-2, 4), ], ids=["positive-small", "positive-larger", "negative"], ) def test_square(n, expected): assert square(n) == expected ``` Тепер `pytest -v` показує `test_square[positive-small]` замість `test_square[2-4]`. Використовуйте власні IDs, коли значення параметрів незрозумілі як назви тестів. **Коли тест провалюється:** Якщо `test_square[3-9]` провалився, ви можете перезапустити лише цей кейс: ```bash pytest -k "test_square[3-9]" ``` **Коли НЕ використовувати parametrize:** Parametrize призначений для *одної логіки*, застосованої до *різних вхідних даних і очікуваних результатів*. Не використовуйте його для групування непов'язаних сценаріїв -- замість цього пишіть окремі тест-функції. Провал параметризованого тесту каже: "цей вхід зламав функцію"; провал окремого тесту каже: "цей конкретний кейс неправильний.

Патерни parametrize

#
**Один параметр:** ```python @pytest.mark.parametrize("value", [0, -1, 1000, None]) def test_is_valid_id(value): result = is_valid_id(value) assert isinstance(result, bool) ``` **Кілька параметрів (рядок через кому):** ```python @pytest.mark.parametrize("a, b, expected", [ (1, 2, 3), (0, 0, 0), (-1, 1, 0), (100, -50, 50), ]) def test_add(a, b, expected): assert add(a, b) == expected ``` **Власні IDs (коли значення не читаються як назви):** ```python @pytest.mark.parametrize("text, expected", [ ("Hello World", "hello-world"), (" leading spaces ", "leading-spaces"), ("already-slugged", "already-slugged"), ], ids=["normal-case", "strips-spaces", "already-slug"]) def test_slugify(text, expected): assert slugify(text) == expected ``` Запуск `pytest -v` показує `test_slugify[normal-case]` замість `test_slugify[Hello World-hello-world]`. **Хибні значення (порожній рядок, None, 0) -- включайте їх явно:** ```python @pytest.mark.parametrize("text, expected", [ ("racecar", True), ("level", True), ("hello", False), ("", True), # порожній рядок є паліндромом ]) def test_is_palindrome(text, expected): assert is_palindrome(text) == expected ``` **Стекінг декораторів (декартовий добуток):** ```python @pytest.mark.parametrize("x", [1, 2]) @pytest.mark.parametrize("y", [10, 20]) def test_multiply(x, y): assert multiply(x, y) == x * y # генерує: (1,10), (1,20), (2,10), (2,20) -- 4 тести загалом ``` **Граничні випадки як рядки таблиці:** ```python @pytest.mark.parametrize("items, expected_len", [ ([], 0), # порожній список ([1], 1), # один елемент ([1, 2, 3], 3), # звичайний список ([None], 1), # None є валідним елементом ([0, False, ''], 3), # хибні значення, але вони є ]) def test_list_length(items, expected_len): assert len(items) == expected_len ```

Довідник: parametrize

#
```python # Один параметр @pytest.mark.parametrize("x", [1, 2, 3]) def test_something(x): ... # Кілька параметрів (рядок через кому або список) @pytest.mark.parametrize("a, b, expected", [ (1, 2, 3), (4, 5, 9), ]) def test_add(a, b, expected): assert add(a, b) == expected # Власні IDs @pytest.mark.parametrize("value", [0, -1, 999], ids=["zero", "negative", "large"]) def test_value(value): ... ``` **Комбінування з фікстурами:** Параметризований тест може також приймати фікстури як параметри. pytest впроваджує фікстури за іменем, незалежно від параметризованих аргументів. **Запуск конкретного набору:** ```bash pytest -k "test_add[1-2-3]" # за ID pytest -k "test_add" --collect-only # переглянути всі кейси ``` **Правила формування ID:** - Числа: `1`, `-3`, `0.5` - Рядки: `hello`, `empty` (без лапок) - `None`, `True`, `False`: виводяться безпосередньо - Кортежі: `1-hello-True` (компоненти через дефіс)
01

Параметризоване тестування square(n)

#

Реалізуйте `square(n)`, яка повертає `n * n`. Параметризуйте `test_square` принаймні з п'ятьма кейсами: додатне ціле, від'ємне ціле, нуль, float і велике число.

import pytest


def square(n):
    pass  # реалізуйте тут


@pytest.mark.parametrize("n, expected", [
    # додайте 5 рядків
])
def test_square(n, expected):
    assert square(n) == expected
Рішення
import pytest


def square(n):
    return n * n


@pytest.mark.parametrize("n, expected", [
    (3,    9),
    (-4,   16),
    (0,    0),
    (2.5,  6.25),
    (1000, 1_000_000),
])
def test_square(n, expected):
    assert square(n) == expected
02

Параметризоване тестування is_palindrome

#

Реалізуйте `is_palindrome(s)`, яка повертає `True`, якщо `s` читається однаково з обох боків. Параметризуйте принаймні чотирма паліндромами (повертають `True`) і трьома не-паліндромами (повертають `False`). Включіть порожній рядок (має повертати `True`).

import pytest


def is_palindrome(s):
    pass  # реалізуйте тут


@pytest.mark.parametrize("text, expected", [
    # паліндроми (True) і не-паліндроми (False)
])
def test_is_palindrome(text, expected):
    assert is_palindrome(text) == expected
Рішення
import pytest


def is_palindrome(s):
    return s == s[::-1]


@pytest.mark.parametrize("text, expected", [
    ("racecar", True),
    ("level",   True),
    ("madam",   True),
    ("",        True),
    ("hello",   False),
    ("python",  False),
    ("abcba",   True),
    ("abcde",   False),
])
def test_is_palindrome(text, expected):
    assert is_palindrome(text) == expected
03

Параметризоване тестування з двома аргументами

#

`clamp(value, lo, hi)` вже реалізовано. Параметризуйте `test_clamp` принаймні шістьма кейсами -- включіть значення у межах діапазону, нижче нижньої межі, вище верхньої та граничні значення. Кожен рядок має містити чотири елементи: `value, lo, hi, expected`.

import pytest


def clamp(value, lo, hi):
    return max(lo, min(value, hi))


@pytest.mark.parametrize("value, lo, hi, expected", [
    # додайте рядки тут
])
def test_clamp(value, lo, hi, expected):
    assert clamp(value, lo, hi) == expected
Рішення
import pytest


def clamp(value, lo, hi):
    return max(lo, min(value, hi))


@pytest.mark.parametrize("value, lo, hi, expected", [
    (5,   0,  10, 5),    # у межах діапазону
    (-5,  0,  10, 0),    # нижче нижньої межі
    (15,  0,  10, 10),   # вище верхньої межі
    (0,   0,  10, 0),    # на нижній межі
    (10,  0,  10, 10),   # на верхній межі
    (50, 40,  60, 50),   # інший діапазон
])
def test_clamp(value, lo, hi, expected):
    assert clamp(value, lo, hi) == expected
04

Власні IDs для параметризованого тесту

#

Параметризований тест нижче автоматично генерує малозрозумілі ID на кшталт `test_parse_int[42-42]`. Додайте `ids=[...]` до декоратора з описовими назвами. Запустіть `pytest -v` до і після, щоб побачити різницю.

import pytest


def parse_int(s):
    return int(s.strip())


@pytest.mark.parametrize("text, expected", [
    ("42",   42),
    ("  7 ", 7),
    ("-3",   -3),
    ("0",    0),
])
def test_parse_int(text, expected):
    assert parse_int(text) == expected
Рішення
import pytest


def parse_int(s):
    return int(s.strip())


@pytest.mark.parametrize("text, expected", [
    ("42",   42),
    ("  7 ", 7),
    ("-3",   -3),
    ("0",    0),
], ids=["plain-integer", "with-whitespace", "negative", "zero"])
def test_parse_int(text, expected):
    assert parse_int(text) == expected
05

Параметризовані граничні випадки для валідатора

#

Реалізуйте `is_valid_username(s)` з трьома правилами: довжина від 3 до 20 символів, лише алфанумерні символи, не може починатися з цифри. Параметризуйте принаймні трьома коректними і чотирма некоректними іменами користувача.

import pytest


def is_valid_username(s):
    pass  # реалізуйте три правила валідації


@pytest.mark.parametrize("username, expected", [
    # коректні (True) і некоректні (False) кейси
])
def test_is_valid_username(username, expected):
    assert is_valid_username(username) == expected
Рішення
import pytest


def is_valid_username(s):
    if not isinstance(s, str):
        return False
    if not (3 <= len(s) <= 20):
        return False
    if not s.isalnum():
        return False
    if s[0].isdigit():
        return False
    return True


@pytest.mark.parametrize("username, expected", [
    ("alice",     True),
    ("User123",   True),
    ("abc",       True),     # мінімальна довжина
    ("ab",        False),    # задовго коротко
    ("1alice",    False),    # починається з цифри
    ("user name", False),    # пробіл
    ("a" * 21,    False),    # задовго довго
])
def test_is_valid_username(username, expected):
    assert is_valid_username(username) == expected