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

Перший тест

5 завдань

Напишіть і запустіть перший pytest-тест. Дізнайтеся, як pytest знаходить тести і читає вивід.

Що таке тестування і чому pytest

#
Тестування -- це практика написання коду, який перевіряє, чи правильно працює ваш інший код. Більше того -- тести дають вам впевненість *змінювати* код. Коли ви рефакторите, виправляєте баг або додаєте функцію, тест-сьют одразу скаже, якщо щось зламалося деінде. Без тестів кожна зміна несе прихований ризик. Найпростіший тест складається з трьох кроків: 1. Викликати вашу функцію з конкретним вхідним значенням 2. Зафіксувати результат 3. Перевірити, що результат дорівнює очікуваному Якщо твердження істинне -- тест проходить. Якщо ні -- pytest покаже, що саме відрізнялось. Ключове слово `assert` -- це вбудований механізм Python для цього: воно обчислює вираз і підіймає `AssertionError`, якщо результат хибний. У тесті ви пишете `assert result == expected` -- ось і весь фундамент. Наступна тема (*Assertions in Depth*) пояснює, як pytest драматично покращує вивід `assert` і охоплює патерни для порівняння float, списків, словників тощо. **Чому 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 (виняток поза твердженням -- налаштування або teardown впали) |

Довідник: 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` (порівняння float), `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, !'