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

Первый тест

5 задач

Напишите и запустите первый pytest-тест. Узнайте, как pytest находит тесты и читает вывод.

Что такое тестирование и почему pytest

#
Тестирование -- это практика написания кода, который проверяет корректность другого кода. И это нечто большее: тесты дают уверенность в том, что код *можно менять*. При рефакторинге, исправлении бага или добавлении новой функции тест-сьют немедленно сигнализирует, если что-то сломалось где-то ещё. Без тестов каждое изменение несёт в себе скрытый риск. Простейший тест состоит из трёх шагов: 1. Вызвать производственную функцию с конкретными входными данными 2. Получить результат 3. Утверждать, что результат равен ожидаемому Если утверждение верно -- тест проходит. Если нет -- pytest точно показывает, в чём расхождение. Ключевое слово `assert` -- встроенный механизм Python для этого: оно вычисляет выражение и выбрасывает `AssertionError`, если результат ложный. В тесте пишется `assert result == expected` -- вот и весь фундамент. Следующая тема (*Утверждения в деталях*) объясняет, как pytest кардинально улучшает вывод `assert`, и рассматривает паттерны для сравнения чисел с плавающей точкой, списков, словарей и другого. **Почему pytest, а не unittest?** Стандартная библиотека Python содержит `unittest`, но он многословен. Нужно наследоваться от `TestCase`, использовать `self.assertEqual()` вместо обычного `assert` и заворачивать всё в классы. pytest ничего этого не требует: ```python # unittest -- много церемоний import unittest class TestMath(unittest.TestCase): def test_add(self): self.assertEqual(add(2, 3), 5) # pytest -- просто функция def test_add(): assert add(2, 3) == 5 ``` Помимо синтаксиса, сообщения об ошибках pytest гораздо информативнее -- он показывает фактические значения, которые сравнивались. А фикстуры, parametrize и экосистема плагинов pytest несравнимо лучше. **Установка:** ```bash pip install pytest ``` **Как работает обнаружение тестов:** pytest обходит директорию проекта и собирает тесты по следующим правилам именования: - Файлы: `test_*.py` или `*_test.py` - Функции на уровне модуля: начинаются с `test_` - Классы: начинаются с `Test` (без метода `__init__`) - Методы внутри классов `Test*`: начинаются с `test_` Всё остальное игнорируется. `helper.py`, `utils.py`, `main.py` -- не собираются. Функция `check_result()` -- не собирается. Только паттерн `test_*` запускает сбор. **Запуск тестов:** ```bash pytest # запустить все найденные тесты pytest -v # подробный вывод: показывать каждое имя теста и результат pytest -s # показывать вывод print() (не подавлять stdout) pytest --tb=short # короткие трассировки при падении pytest --collect-only # показать, что будет запущено, без запуска ```

Полный первый тестовый файл

#
Напишем небольшой модуль и полный файл тестов к нему -- затем посмотрим, как выглядит вывод pytest для проходящих и падающих тестов. ```python # string_utils.py def reverse(s): return s[::-1] def truncate(text, max_len): if len(text) <= max_len: return text return text[:max_len] def slugify(text): return text.lower().strip().replace(' ', '-') ``` ```python # test_string_utils.py from string_utils import reverse, truncate, slugify def test_reverse_basic(): assert reverse('hello') == 'olleh' def test_reverse_empty(): assert reverse('') == '' def test_truncate_longer_than_limit(): assert truncate('hello world', 5) == 'hello' def test_truncate_within_limit(): result = truncate('hi', 10) assert result == 'hi' assert len(result) == 2 def test_slugify_spaces(): assert slugify('Hello World') == 'hello-world' def test_slugify_strips_whitespace(): assert slugify(' python ') == 'python' ``` Запуск `pytest -v` выдаст: ``` test_string_utils.py::test_reverse_basic PASSED test_string_utils.py::test_reverse_empty PASSED test_string_utils.py::test_truncate_longer_than_limit PASSED test_string_utils.py::test_truncate_within_limit PASSED test_string_utils.py::test_slugify_spaces PASSED test_string_utils.py::test_slugify_strips_whitespace PASSED 6 passed in 0.04s ``` Теперь намеренно сломаем `reverse` -- изменим `s[::-1]` на `s` -- и запустим снова: ``` FAILED test_string_utils.py::test_reverse_basic AssertionError: assert 'hello' == 'olleh' ``` pytest показывает фактическое значение (`'hello'`) и ожидаемое (`'olleh'`). Никаких print-отладчиков не нужно -- вывод утверждения и есть диагностика. **Три состояния результата теста:** | Символ | Значение | |--------|---------| | `.` | PASSED (прошёл) | | `F` | FAILED (упал -- утверждение не выполнено) | | `E` | ERROR (исключение вне assert -- сбой в инициализации или завершении) |

Справочник: pytest CLI

#
```bash pytest # запустить все тесты pytest -v # подробный вывод: показывать имя каждого теста pytest -s # показывать вывод print() (не перехватывать stdout) pytest test_foo.py # запустить один файл pytest -k "add" # запустить тесты, в имени которых есть "add" pytest --tb=short # короткие трассировки pytest --tb=no # без трассировок (только счётчик пройденных/упавших) ``` **Правила именования, которым следует pytest:** - Файлы: `test_*.py` или `*_test.py` - Функции и методы: `test_*` - Классы: `Test*` (без метода `__init__`) **Что выводит pytest:** ``` collected 4 items test_math_utils.py::test_add_positive_numbers PASSED test_math_utils.py::test_add_negative PASSED test_math_utils.py::test_clamp_within_range PASSED test_math_utils.py::test_clamp_below_lower PASSED 4 passed in 0.05s ``` **Когда добавлять `import pytest`:** Простые тесты требуют только `assert` и собственных импортов -- `import pytest` не нужен. Он добавляется при использовании специфичных для pytest возможностей: `pytest.approx` (сравнение чисел с плавающей точкой), `pytest.raises` (тестирование исключений), `@pytest.fixture` или меток вроде `@pytest.mark.parametrize`. Они рассматриваются в следующих темах.
01

Тест функции add

#

Напишите тест-функцию `test_add`, которая вызывает `add(a, b)` как минимум с тремя разными парами входных данных и проверяет правильный результат для каждой.

# math_utils.py
def add(a, b):
    return a + b


# test_math_utils.py
from math_utils import add

def test_add():
    # проверить хотя бы три пары входных данных
    pass
Решение
# math_utils.py
def add(a, b):
    return a + b


# test_math_utils.py
from math_utils import add

def test_add():
    assert add(2, 3) == 5
    assert add(0, 0) == 0
    assert add(-1, 1) == 0
    assert add(100, -50) == 50
02

Тест функции is_even

#

Напишите тест-функции для `is_even(n)`, которые проверяют, что функция возвращает `True` для чётных чисел и `False` для нечётных. Напишите как минимум две тест-функции -- одну для чётных входных данных, другую для нечётных.

def is_even(n):
    return n % 2 == 0


def test_even_numbers():
    pass

def test_odd_numbers():
    pass
Решение
def is_even(n):
    return n % 2 == 0


def test_even_numbers():
    assert is_even(0) is True
    assert is_even(2) is True
    assert is_even(100) is True

def test_odd_numbers():
    assert is_even(1) is False
    assert is_even(-3) is False
    assert is_even(99) is False
03

Чтение вывода упавшего теста

#

Тест ниже содержит намеренный баг -- ожидаемое значение неправильное. Запустите pytest и прочитайте вывод падения, затем исправьте ожидаемое значение, чтобы тест прошёл.

def multiply(a, b):
    return a * b


def test_multiply():
    assert multiply(3, 4) == 11   # неправильное ожидаемое значение
    assert multiply(0, 99) == 0
    assert multiply(-2, 5) == -10
Решение
def multiply(a, b):
    return a * b


def test_multiply():
    assert multiply(3, 4) == 12   # исправлено: 3 * 4 = 12
    assert multiply(0, 99) == 0
    assert multiply(-2, 5) == -10
04

Тест функции clamp

#

Напишите три отдельные тест-функции для `clamp(value, lo, hi)`: одну для значений внутри диапазона, одну для значений ниже нижней границы и одну для значений выше верхней границы.

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


def test_within_range():
    pass

def test_below_lower_bound():
    pass

def test_above_upper_bound():
    pass
Решение
def clamp(value, lo, hi):
    return max(lo, min(value, hi))


def test_within_range():
    assert clamp(5, 0, 10) == 5
    assert clamp(0, 0, 10) == 0
    assert clamp(10, 0, 10) == 10

def test_below_lower_bound():
    assert clamp(-5, 0, 10) == 0
    assert clamp(-100, 0, 10) == 0

def test_above_upper_bound():
    assert clamp(15, 0, 10) == 10
    assert clamp(1000, 0, 10) == 10
05

Тест функции greet

#

Напишите тесты для `greet(name)`, которая возвращает `'Hello, {name}!'`. Проверьте, что возвращаемое значение начинается с 'Hello', заканчивается на '!' и содержит переданное имя.

def greet(name):
    return f'Hello, {name}!'


def test_greet_alice():
    pass

def test_greet_empty_name():
    pass
Решение
def greet(name):
    return f'Hello, {name}!'


def test_greet_alice():
    result = greet('Alice')
    assert result == 'Hello, Alice!'
    assert result.startswith('Hello')
    assert result.endswith('!')
    assert 'Alice' in result

def test_greet_empty_name():
    result = greet('')
    assert result == 'Hello, !'