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)