Python · Синтаксис · Просунутий рівень
Тестування з pytest
Пишіть надійні тести за допомогою pytest. Охоплює тестові функції, фікстури, parametrize, monkeypatch та тестування винятків.
Короткий вступ до теми та пояснення перед вправами (вправи нижче):
Фікстури, scope, conftest.py, mock та monkeypatch
#Ізоляція тестів, фікстури-фабрики, marks, coverage, CLI
#Вправи:
Написати базові тестові функції
#Напишіть три тестові функції pytest для функції `add(a, b)`: `test_add_positive`, `test_add_negative` та `test_add_zero`. Кожна має викликати `add()` та перевіряти результат через `assert`.
def add(a: int, b: int) -> int:
return a + b
def test_add_positive():
pass
def test_add_negative():
pass
def test_add_zero():
pass
# Запуск: pytest this_file.py
Рішення
def add(a: int, b: int) -> int:
return a + b
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, -2) == -3
def test_add_zero():
assert add(0, 100) == 100
Перевірити, що винятки викидаються
#Напишіть тести для функції `divide(a, b)`. Перевірте, що `divide(10, 2)` повертає `5.0`, і що `divide(10, 0)` викидає `ZeroDivisionError` за допомогою `pytest.raises`.
import pytest
def divide(a: float, b: float) -> float:
return a / b
def test_divide_normal():
pass
def test_divide_by_zero():
pass
Рішення
import pytest
def divide(a: float, b: float) -> float:
return a / b
def test_divide_normal():
assert divide(10, 2) == 5.0
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
Створити фікстуру pytest
#Напишіть `@pytest.fixture` під назвою `sample_list`, що повертає `[3, 1, 4, 1, 5, 9]`. Напишіть два тести, що використовують її: `test_length` перевіряє `len == 6`, а `test_max` перевіряє `max == 9`.
import pytest
@pytest.fixture
def sample_list():
pass
def test_length(sample_list):
pass
def test_max(sample_list):
pass
Рішення
import pytest
@pytest.fixture
def sample_list():
return [3, 1, 4, 1, 5, 9]
def test_length(sample_list):
assert len(sample_list) == 6
def test_max(sample_list):
assert max(sample_list) == 9
Параметризувати тест
#Використайте `@pytest.mark.parametrize` для тестування `is_palindrome(s)` з п'ятьма випадками: `'racecar'` → True, `'hello'` → False, `'madam'` → True, `'a'` → True, `'ab'` → False. Запишіть всі п'ять як один параметризований тест.
import pytest
def is_palindrome(s: str) -> bool:
return s == s[::-1]
@pytest.mark.parametrize('s, expected', [
# заповніть 5 випадків
])
def test_is_palindrome(s, expected):
pass
Рішення
import pytest
def is_palindrome(s: str) -> bool:
return s == s[::-1]
@pytest.mark.parametrize('s, expected', [
('racecar', True),
('hello', False),
('madam', True),
('a', True),
('ab', False),
])
def test_is_palindrome(s, expected):
assert is_palindrome(s) == expected
monkeypatch змінної середовища
#Напишіть функцію `get_debug_mode() -> bool`, що читає змінну середовища `DEBUG` і повертає `True`, якщо вона дорівнює `'1'`, інакше `False`. Напишіть два тести з `monkeypatch.setenv`: `DEBUG=1` → True, `DEBUG=0` → False.
import os
import pytest
def get_debug_mode() -> bool:
return os.environ.get('DEBUG', '0') == '1'
def test_debug_on(monkeypatch):
pass
def test_debug_off(monkeypatch):
pass
Рішення
import os
import pytest
def get_debug_mode() -> bool:
return os.environ.get('DEBUG', '0') == '1'
def test_debug_on(monkeypatch):
monkeypatch.setenv('DEBUG', '1')
assert get_debug_mode() is True
def test_debug_off(monkeypatch):
monkeypatch.setenv('DEBUG', '0')
assert get_debug_mode() is False
Замінити функцію через monkeypatch.setattr
#Функція `get_username()` викликає `os.getlogin()`, що може не працювати в CI. Використайте `monkeypatch.setattr`, щоб замінити `os.getlogin` лямбдою, що повертає `'testuser'`. Перевірте, що `get_username()` повертає `'testuser'`.
import os
import pytest
def get_username() -> str:
return os.getlogin()
def test_get_username(monkeypatch):
pass
Рішення
import os
import pytest
def get_username() -> str:
return os.getlogin()
def test_get_username(monkeypatch):
monkeypatch.setattr(os, 'getlogin', lambda: 'testuser')
assert get_username() == 'testuser'
Фікстура з налаштуванням та очищенням
#Напишіть фікстуру `temp_list`, що створює список `[1, 2, 3]`, повертає його через `yield`, потім очищає після тесту (teardown через `yield`). Напишіть тест `test_append`, що додає `4` і перевіряє `len == 4`.
import pytest
@pytest.fixture
def temp_list():
data = [1, 2, 3]
yield data
# teardown: очищення після тесту
pass
def test_append(temp_list):
temp_list.append(4)
assert len(temp_list) == 4
Рішення
import pytest
@pytest.fixture
def temp_list():
data = [1, 2, 3]
yield data
data.clear()
def test_append(temp_list):
temp_list.append(4)
assert len(temp_list) == 4
Тестування файлового I/O з tmp_path
#Напишіть функцію `write_and_read(path, text)`, що записує `text` до `path`, читає його назад та повертає вміст. Використайте вбудовану фікстуру `tmp_path` для тестування без реальної файлової системи.
from pathlib import Path
def write_and_read(path: Path, text: str) -> str:
path.write_text(text)
return path.read_text()
def test_write_and_read(tmp_path):
file = tmp_path / 'test.txt'
result = write_and_read(file, 'hello')
assert result == 'hello'
Рішення
from pathlib import Path
def write_and_read(path: Path, text: str) -> str:
path.write_text(text)
return path.read_text()
def test_write_and_read(tmp_path):
file = tmp_path / 'test.txt'
result = write_and_read(file, 'hello')
assert result == 'hello'
Тестування класу з фікстурою
#Маючи клас `Counter` з `increment()`, `decrement()` та властивістю `value`, напишіть фікстуру `counter`, що повертає свіжий `Counter()`. Напишіть тести: `test_initial_value` (value == 0), `test_increment` (value == 1), `test_decrement` (value == -1 після одного decrement).
import pytest
class Counter:
def __init__(self):
self._value = 0
def increment(self) -> None:
self._value += 1
def decrement(self) -> None:
self._value -= 1
@property
def value(self) -> int:
return self._value
@pytest.fixture
def counter():
pass
def test_initial_value(counter):
pass
def test_increment(counter):
pass
def test_decrement(counter):
pass
Рішення
import pytest
class Counter:
def __init__(self):
self._value = 0
def increment(self) -> None:
self._value += 1
def decrement(self) -> None:
self._value -= 1
@property
def value(self) -> int:
return self._value
@pytest.fixture
def counter():
return Counter()
def test_initial_value(counter):
assert counter.value == 0
def test_increment(counter):
counter.increment()
assert counter.value == 1
def test_decrement(counter):
counter.decrement()
assert counter.value == -1
Тестування значень з плаваючою точкою через pytest.approx
#Напишіть тести для функції `circle_area(r)` (повертає `math.pi * r ** 2`). Використайте `pytest.approx` для порівняння — звичайне `==` для float не спрацює. Тестуйте з `r=1` (~3.14159), `r=2` (~12.566) та `r=0` (0).
import math
import pytest
def circle_area(r: float) -> float:
return math.pi * r ** 2
def test_area_r1():
pass
def test_area_r2():
pass
def test_area_r0():
pass
Рішення
import math
import pytest
def circle_area(r: float) -> float:
return math.pi * r ** 2
def test_area_r1():
assert circle_area(1) == pytest.approx(math.pi)
def test_area_r2():
assert circle_area(2) == pytest.approx(4 * math.pi)
def test_area_r0():
assert circle_area(0) == pytest.approx(0)