Python · Тестирование с pytest · Начальный

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

5 задач

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

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

#
**Проблема:** При тестировании функции с несколькими входами обычно пишут отдельные тест-функции: ```python def test_square_positive(): assert square(3) == 9 def test_square_negative(): assert square(-4) == 16 def test_square_zero(): assert square(0) == 0 ``` Это повторение. При изменении функции `square` придётся обновлять одну и ту же логику в нескольких местах. `@pytest.mark.parametrize` решает эту проблему: одна тест-функция запускается несколько раз с разными входными данными: ```python import pytest @pytest.mark.parametrize("n, expected", [ (3, 9), (-4, 16), (0, 0), ]) def test_square(n, expected): assert square(n) == expected ``` Каждая строка создаёт независимый тест-кейс -- если `(3, 9)` упал, остальные всё равно выполняются. pytest выводит каждый отдельно. **Разбор синтаксиса:** ```python @pytest.mark.parametrize( "param_names", # строка с именами параметров через запятую [ # список значений (или кортежей для нескольких параметров) value1, value2, ] ) def test_something(param_names): ... ``` Для одного параметра: плоский список. Для нескольких: список кортежей, по одному на тест-кейс. **Как генерируются ID тестов:** pytest автоматически создаёт имя для каждого кейса из значений параметров: ``` test_square[3-9] <- из (3, 9) test_square[-4-16] <- из (-4, 16) test_square[0-0] <- из (0, 0) ``` Можно переопределить через `ids=`: ```python @pytest.mark.parametrize("n, expected", [ (3, 9), (-4, 16), ], ids=["positive", "negative"]) # -> test_square[positive], test_square[negative] ``` **Когда параметризованный тест падает:** pytest показывает точно, какой набор параметров это вызвал: ``` FAILED test_math.py::test_square[-4-16] AssertionError: assert 4 == 16 ``` Можно запустить только этот упавший кейс: ```bash pytest -k "test_square[-4-16]" ``` **Когда НЕ стоит использовать parametrize:** Если тест-кейсы имеют совершенно разную логику -- разные утверждения, разные вызовы функций -- они должны быть отдельными тест-функциями. Parametrize -- для одной и той же логики проверки с разными входными данными.

Паттерны параметризации

#
**Один параметр -- плоский список:** ```python import pytest @pytest.mark.parametrize("value", [-1, -100, -0.001]) def test_is_negative(value): assert value < 0 ``` **Несколько параметров -- список кортежей:** ```python @pytest.mark.parametrize("a, b, expected", [ (1, 2, 3), (0, 5, 5), (-1, 1, 0), (10, -3, 7), ]) def test_add(a, b, expected): assert a + b == expected ``` **Пользовательские ID для читаемости:** ```python @pytest.mark.parametrize("text, expected", [ ("hello world", "hello-world"), ("Python Tutorial", "python-tutorial"), (" spaces ", "spaces"), ], ids=["basic", "multi-word", "trim"]) def test_slugify(text, expected): assert slugify(text) == expected ``` Без пользовательских ID pytest выдаст `test_slugify[hello world-hello-world]` -- трудночитаемо. С ID: `test_slugify[basic]`. **Смешивание истинных и ложных ожидаемых значений:** ```python @pytest.mark.parametrize("s, expected", [ ("racecar", True), ("madam", True), ("hello", False), ("", True), # граничный случай: пустая строка -- палиндром ]) def test_is_palindrome(s, expected): assert is_palindrome(s) == expected ``` **Стекирование двух декораторов parametrize -- декартово произведение:** ```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("value", [None, "", 0, [], {}, False]) def test_falsy_values(value): assert not value ``` Перечисление граничных случаев как строк заставляет думать о них явно -- они становятся отслеживаемыми тест-кейсами, а не запоздалыми мыслями.

Краткий справочник по parametrize

#
```python # Один параметр @pytest.mark.parametrize("name", [v1, v2, v3]) def test_something(name): ... # Несколько параметров (строка через запятую или список) @pytest.mark.parametrize("a, b, expected", [ (in1, in2, out1), (in3, in4, out2), ]) def test_something(a, b, expected): ... # Пользовательские ID @pytest.mark.parametrize("x", [1, 2, 3], ids=["one", "two", "three"]) def test_something(x): ... ``` **Совместное использование с фикстурами:** ```python @pytest.fixture def multiplier(): return 2 @pytest.mark.parametrize("n, expected", [(3, 6), (5, 10)]) def test_double(n, expected, multiplier): assert n * multiplier == expected ``` **Запуск конкретного набора параметров:** ```bash pytest -k "test_add[1-2-3]" # по автоматически сгенерированному ID pytest -k "trim" # по подстроке пользовательского ID ``` **Формат автоматически генерируемых ID:** - Строки: значение как есть (усечённое, если длинное) - Числа: само число - `None`, `True`, `False`: их repr - Сложные объекты: `param0`, `param1`, ...
01

Параметризация square(n)

#

Напишите `square(n)`, которая возвращает `n * n`. Используйте `@pytest.mark.parametrize`, чтобы протестировать её минимум с 5 разными входными данными: положительное целое, отрицательное целое, ноль, число с плавающей точкой и большое число.

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),
    (0.5,  0.25),
    (100,  10000),
])
def test_square(n, expected):
    assert square(n) == expected
02

Параметризация is_palindrome

#

Напишите `is_palindrome(s)`, которая возвращает True, если строка одинаково читается в обоих направлениях. Используйте `@pytest.mark.parametrize` с минимум 4 палиндромами и 3 не-палиндромами, включая ожидаемое булево значение вторым параметром.

import pytest


def is_palindrome(s):
    pass


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


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


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

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

#

Используйте `@pytest.mark.parametrize` для тестирования `clamp(value, lo, hi)` минимум с 5 кейсами: значение внутри диапазона, на нижней границе, на верхней границе, ниже нижней границы и выше верхней. Используйте четыре параметра на строку: `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),
    (0,   0, 10, 0),
    (10,  0, 10, 10),
    (-5,  0, 10, 0),
    (15,  0, 10, 10),
    (-1,  0, 10, 0),
])
def test_clamp(value, lo, hi, expected):
    assert clamp(value, lo, hi) == expected
04

Добавление пользовательских ID

#

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

import pytest


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


@pytest.mark.parametrize("s, expected", [
    ("42",    42),
    ("  7 ",  7),
    ("-10",  -10),
    ("0",     0),
])
def test_parse_int(s, expected):
    assert parse_int(s) == expected
Решение
import pytest


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


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

Параметризация граничных случаев валидатора

#

Напишите `is_valid_username(s)`, которая возвращает True, только если строка содержит от 3 до 20 символов, состоит только из букв и цифр и не начинается с цифры. Используйте `@pytest.mark.parametrize` для проверки минимум 3 допустимых и 4 недопустимых имён пользователей.

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 s or not (3 <= len(s) <= 20):
        return False
    if s[0].isdigit():
        return False
    return all(c.isalnum() for c in s)


@pytest.mark.parametrize("username, expected", [
    ("alice",       True),
    ("user123",     True),
    ("A1b2C3",      True),
    ("ab",          False),
    ("a" * 21,      False),
    ("1alice",      False),
    ("alice!",      False),
    ("",            False),
])
def test_is_valid_username(username, expected):
    assert is_valid_username(username) == expected