Функция, выбрасывающая исключение на некорректных входных данных, ведёт себя верно -- выброс является частью её контракта. Тестирование такого поведения столь же важно, как тестирование основного сценария.
**pytest.raises -- базовый паттерн:**
```python
import pytest
def divide(a, b):
return a / b
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
```
`pytest.raises` -- это контекстный менеджер. Внутри блока `with` вызывается код, который *должен* выбросить исключение. Три возможных исхода:
| Что происходит | Результат теста |
|---|---|
| Ожидаемое исключение выброшено | PASSED |
| Исключение не выброшено | FAILED -- `DID NOT RAISE` |
| Выброшено другое исключение | FAILED -- непредвиденное исключение всплывает |
Третий случай важен: если `divide(10, 0)` выбросит `ValueError` вместо `ZeroDivisionError`, тест упадёт с неожиданным `ValueError` в выводе. Нельзя случайно пройти с неверным типом исключения.
**Проверка сообщения исключения через `match`:**
Параметр `match` принимает **regex**-шаблон, сопоставляемый с `str(exception)`. Не нужно совпадать со всем сообщением -- достаточно характерной подстроки:
```python
def set_age(age):
if age < 0:
raise ValueError(f"Age {age} is invalid: must be non-negative")
return age
def test_negative_age():
with pytest.raises(ValueError, match=r"must be non-negative"):
set_age(-5)
```
Используйте сырые строки (`r"..."`) для regex-шаблонов. Частые паттерны:
- `match=r"must be"` -- совпадение по подстроке
- `match=r"\d+ is invalid"` -- число, за которым следует текст
- `match=r"^Age"` -- сообщение должно начинаться с "Age"
**Инспекция объекта исключения:**
Сохраните объект `ExceptionInfo` после блока `with`:
```python
with pytest.raises(ValueError) as excinfo:
set_age(-5)
excinfo.value # экземпляр исключения
excinfo.type # класс исключения (ValueError)
str(excinfo.value) # полное строковое представление
excinfo.value.args # кортеж args, переданный исключению
```
**Подклассы исключений:**
`pytest.raises(Exception)` поймает и `ValueError`, и `TypeError` и т.д. -- любой подкласс `Exception`. Используйте наиболее конкретный ожидаемый тип:
```python
# слишком широко -- принимает любое исключение, скрывает баги
with pytest.raises(Exception):
risky()
# правильно -- принимает только тот тип, для которого вы проектировали
with pytest.raises(ValueError):
risky()
```
**Проверка отсутствия исключения:**
Чтобы убедиться, что код *не* выбрасывает, специальный синтаксис не нужен. Просто вызовите функцию обычно -- если она выбросит неожиданное исключение, pytest поймает его и провалит тест:
```python
def test_valid_input():
result = set_age(25) # не должна выбрасывать
assert result == 25 # проверьте и возвращаемое значение
```
**Полный пример -- функция снятия денег с пользовательским исключением:**
```python
import pytest
class InsufficientFundsError(Exception):
pass
def withdraw(balance, amount):
if not isinstance(amount, (int, float)):
raise TypeError(f"Amount must be a number, got {type(amount).__name__}")
if amount <= 0:
raise ValueError(f"Amount must be positive, got {amount}")
if amount > balance:
raise InsufficientFundsError(
f"Cannot withdraw {amount}: balance is only {balance}"
)
return balance - amount
```
```python
# Тестируем каждый тип исключения отдельно
def test_non_numeric_amount():
with pytest.raises(TypeError):
withdraw(100, "fifty")
def test_negative_amount():
with pytest.raises(ValueError, match=r"must be positive"):
withdraw(100, -10)
def test_zero_amount():
with pytest.raises(ValueError):
withdraw(100, 0)
def test_insufficient_funds():
with pytest.raises(InsufficientFundsError):
withdraw(50, 100)
def test_insufficient_funds_message():
with pytest.raises(InsufficientFundsError, match=r"balance is only 50"):
withdraw(50, 100)
```
**Инспекция исключения после блока:**
```python
def test_exception_details():
with pytest.raises(InsufficientFundsError) as excinfo:
withdraw(50, 100)
assert excinfo.type is InsufficientFundsError
assert "100" in str(excinfo.value) # запрошенная сумма в сообщении
assert "50" in str(excinfo.value) # доступный баланс в сообщении
```
**Проверка, что корректные данные работают (без исключения):**
```python
def test_successful_withdrawal():
new_balance = withdraw(100, 40)
assert new_balance == 60
def test_withdraw_entire_balance():
new_balance = withdraw(100, 100)
assert new_balance == 0
```
**Частая ошибка -- утверждения внутри блока with:**
```python
# НЕПРАВИЛЬНО: если withdraw() немедленно выбрасывает, assert никогда не выполняется
with pytest.raises(InsufficientFundsError):
result = withdraw(50, 100)
assert result is None # эта строка недостижима
# ПРАВИЛЬНО: утверждения о результатах выносятся за пределы блока with
with pytest.raises(InsufficientFundsError) as excinfo:
withdraw(50, 100)
assert "100" in str(excinfo.value) # выполняется после блока
```
Блок `with` завершается, как только выброшено исключение. Любой код после строки, выбросившей исключение, но ещё внутри блока `with`, недостижим.
```python
import pytest
# Базовый: проверка типа исключения
with pytest.raises(SomeException):
code_that_should_raise()
# Проверка сообщения через regex
with pytest.raises(SomeException, match=r"pattern"):
code_that_should_raise()
# Инспекция объекта исключения после блока
with pytest.raises(SomeException) as excinfo:
code_that_should_raise()
excinfo.value # экземпляр исключения
excinfo.type # класс исключения
str(excinfo.value) # строковое представление
# Принять несколько типов исключений
with pytest.raises((ValueError, TypeError)):
ambiguous_function()
```
**Частая ошибка -- утверждение внутри блока with:**
```python
# НЕПРАВИЛЬНО: assert никогда не выполняется, если исключение выброшено первым
with pytest.raises(ValueError):
result = risky_call()
assert result == 42 # пропускается!
# ПРАВИЛЬНО: утверждения выносятся за пределы блока with
with pytest.raises(ValueError):
risky_call()
# утверждения о результатах здесь, после подтверждения исключения
```
**Тестирование подклассов исключений:**
`pytest.raises(Exception)` совпадает с `ValueError`, `TypeError` и т.д. (любым подклассом).
Используйте точный тип, когда важно, какое именно исключение было выброшено.
Напишите `divide(a, b)`, которая возвращает `a / b`. Напишите тест, проверяющий, что при `b=0` выбрасывается `ZeroDivisionError`. Также напишите тест, проверяющий корректный результат при ненулевом `b`.
import pytest
def divide(a, b):
return a / b
def test_divide_by_zero():
pass
def test_divide_normal():
pass
Напишите `set_age(age)`, которая выбрасывает `ValueError` с сообщением `'Age must be between 0 and 150'` при `age < 0` или `age > 150`. Напишите тесты, проверяющие, что `ValueError` выбрасывается, а его сообщение содержит `'between 0 and 150'` через параметр `match`.
import pytest
def set_age(age):
pass # выбросить ValueError с конкретным сообщением для некорректного возраста
def test_negative_age():
pass
def test_age_too_large():
pass
Решение
import pytest
def set_age(age):
if age < 0 or age > 150:
raise ValueError('Age must be between 0 and 150')
return age
def test_negative_age():
with pytest.raises(ValueError, match=r'between 0 and 150'):
set_age(-1)
def test_age_too_large():
with pytest.raises(ValueError, match=r'between 0 and 150'):
set_age(151)
def test_valid_age():
assert set_age(25) == 25
assert set_age(0) == 0
assert set_age(150) == 150
Напишите `parse_positive(value)`, которая выбрасывает `TypeError`, если `value` не является числом, и `ValueError`, если `value <= 0`. Напишите отдельные тесты для каждого типа исключения, каждый из которых проверяет, что нужное исключение выбрасывается при нужных входных данных.
import pytest
def parse_positive(value):
pass # TypeError для не-чисел, ValueError для неположительных
def test_non_number_raises_type_error():
pass
def test_non_positive_raises_value_error():
pass
Решение
import pytest
def parse_positive(value):
if not isinstance(value, (int, float)):
raise TypeError(f'Expected a number, got {type(value).__name__}')
if value <= 0:
raise ValueError(f'Value must be positive, got {value}')
return value
def test_non_number_raises_type_error():
with pytest.raises(TypeError):
parse_positive('five')
with pytest.raises(TypeError):
parse_positive(None)
def test_non_positive_raises_value_error():
with pytest.raises(ValueError):
parse_positive(0)
with pytest.raises(ValueError):
parse_positive(-10)
def test_valid_input():
assert parse_positive(5) == 5
assert parse_positive(0.1) == pytest.approx(0.1)
Определите `InsufficientFundsError(Exception)` и `withdraw(balance, amount)`, которая выбрасывает его при `amount > balance`. Напишите тест, проверяющий выброс пользовательского исключения, и ещё один с `match`, проверяющий, что сообщение содержит запрошенную сумму.
Напишите `safe_divide(a, b)`, которая возвращает `a / b` при `b != 0` и возвращает `0` при `b == 0` -- без исключения в обоих случаях. Напишите тесты для обоих путей, проверяющие корректные возвращаемые значения.
import pytest
def safe_divide(a, b):
pass # вернуть a/b обычно, вернуть 0 если b равно 0
def test_normal_division():
pass
def test_zero_denominator_returns_zero():
pass
Решение
def safe_divide(a, b):
if b == 0:
return 0
return a / b
def test_normal_division():
assert safe_divide(10, 2) == 5.0
assert safe_divide(-6, 3) == -2.0
def test_zero_denominator_returns_zero():
result = safe_divide(10, 0)
assert result == 0
def test_zero_numerator():
assert safe_divide(0, 5) == 0.0
No split tab
Настройки cookies
Мы используем необходимые cookies для работы сайта. С вашего разрешения мы также можем сохранять настройки сайта и использовать аналитические и рекламные cookies, чтобы понимать использование сайта и поддерживать развитие проекта.
* Вы всегда можете изменить свой выбор в настройках сайта.
Выберите категории cookies
Настройки аналитики
Можно отключить аналитику использования платформы. Также можно отправить в Google Analytics запрос на удаление данных об использовании этого сайта, связанных с этим браузером.
Учебный workspace
Учитесь, читая, запуская код и решая задачи.
Практикуйте программирование с пояснениями тем, упражнениями, инструментами browser IDE, проверкой regex и тренировкой печати кода в одном workspace.
Открывайте инструменты во вкладках.Упражнения, IDE-инструменты и тренажеры остаются доступными как вкладки сайта.
Переключайтесь без потери контекста.Переходите между пояснениями, кодом и утилитами, сохраняя свое место.
Используйте sidebar как карту.Левые панели содержат навигацию, настройки, файлы, libraries и управление инструментами.
PythonJavaScriptSQLite
Одна IDE, три практичных режима
Python в браузере.Запускайте небольшие скрипты, пробуйте библиотеки и тренируйте API-запросы без установки.
JavaScript для быстрых экспериментов.Проверяйте код для браузера и сравнивайте идеи рядом с учебными материалами.
SQLite для практики с данными.Открывайте обозреватель базы данных, изучайте таблицы, пишите запросы и учитесь SQL локально.
ТемаIDE
Работайте рядом в split tabs
Держите инструкции перед глазами.Откройте упражнение или справочную страницу рядом с IDE, вместо постоянных переключений.
Сравнивайте инструменты во время обучения.Размещайте проверки regex, пояснения и эксперименты с кодом рядом, когда это нужно для задачи.
Закройте split, когда закончите.Workspace вернётся к одной сфокусированной вкладке, а открытые вкладки сайта останутся доступны.
Тренажер слепой печати кода
Или просто текста
Тренажер рассчитан на физическую клавиатуру.Откройте этот раздел на ноутбуке или компьютере с широким экраном. На телефоне тренировка слепой печати не будет корректной.
Скорость: 0 зн/мин
0 слов/мин
Лучшая скорость (60с): 0 зн/мин
0 слов/мин
Ошибки: 0
Общее время: 0.0 с
Для активации режима слепого набора не подсматривайте на физическую клавиатуру.