Функція, яка підіймає виняток при некоректному вводі, поводиться правильно -- підняття є частиною її контракту. Тестування такої поведінки так само важливе, як тестування щасливого шляху.
**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 # також перевіряємо значення, що повертається
```
**Повний приклад: перевірка кількох типів винятків:**
Розглянемо функцію `withdraw(balance, amount)`, яка може підіймати кілька різних винятків:
```python
class InsufficientFundsError(Exception):
pass
def withdraw(balance, amount):
if not isinstance(amount, (int, float)):
raise TypeError(f"Amount must be numeric, got {type(amount).__name__}")
if amount < 0:
raise ValueError("Amount must be non-negative")
if amount == 0:
raise ValueError("Amount must be greater than zero")
if amount > balance:
raise InsufficientFundsError(f"Cannot withdraw {amount}, only {balance} available")
return balance - amount
```
Тестуємо кожен тип винятку окремо:
```python
def test_non_numeric_amount():
with pytest.raises(TypeError):
withdraw(100, "fifty")
def test_negative_amount():
with pytest.raises(ValueError):
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"Cannot withdraw"):
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_valid_withdrawal():
result = withdraw(100, 30)
assert result == 70
def test_withdraw_exact_balance():
result = withdraw(50, 50)
assert result == 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) # виконується після блоку
```
```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`. Python підіймає `ZeroDivisionError` природно при `b == 0`. Напишіть два тести: `test_divide_by_zero` -- перевіряє підняття `ZeroDivisionError`, і `test_divide_normal` -- перевіряє правильний результат для коректного вводу.
import pytest
def divide(a, b):
return a / b
def test_divide_by_zero():
pass
def test_divide_normal():
pass
Рішення
import pytest
def divide(a, b):
return a / b
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_divide_normal():
assert divide(10, 2) == 5.0
assert divide(9, 3) == 3.0
Напишіть `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(200)
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):
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_parse_positive_type_error():
with pytest.raises(TypeError):
parse_positive('five')
with pytest.raises(TypeError):
parse_positive(None)
def test_parse_positive_value_error():
with pytest.raises(ValueError):
parse_positive(-3)
with pytest.raises(ValueError):
parse_positive(0)
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` -- жодного винятку в жодному випадку. Напишіть тести для обох шляхів, перевіряючи правильні значення, що повертаються.
Ми використовуємо необхідні 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 с
Щоб тренувати сліпий друк, не підглядайте на фізичну клавіатуру.