Python · Тестирование с pytest · Начальный

Утверждения в деталях

5 задач

Освойте перезапись утверждений pytest для чётких и информативных сообщений об ошибках.

Как pytest переписывает утверждения

#
Оператор `assert` в Python сам по себе минималистичен: если выражение ложно, он выбрасывает `AssertionError` без какой-либо полезной информации: ```python # обычный assert Python assert result == expected # -> AssertionError (только исключение, ничего больше) ``` pytest решает эту проблему с помощью **переписывания утверждений**. Когда pytest импортирует ваш тестовый файл, он преобразует байткод так, чтобы каждый `assert` захватывал обе стороны сравнения до его вычисления. При падении вы видите точно, что различалось: ``` # сравнение списков: AssertionError: assert [1, 2, 3] == [1, 2, 4] At index 2 diff: 3 != 4 # сравнение словарей: AssertionError: assert {'name': 'Alice', 'age': 31} == {'name': 'Alice', 'age': 30} Differing items: {'age': 31} != {'age': 30} # сравнение строк: AssertionError: assert 'hello earth' == 'hello world' At position 6 diff: 'e' != 'w' ``` Это делает падения самодиагностирующимися. Вы видите *что именно* различалось, а не просто *что* что-то различалось. **`==` против `is`** `==` проверяет *равенство* (одинаковое значение). `is` проверяет *идентичность* (один и тот же объект в памяти). Используйте `is` для булевых значений и `None`: ```python assert result is True # result является булевым True assert result == True # тоже работает, но 1 == True тоже True (менее строго) assert result is not None # понятно и идиоматично ``` Для чисел, строк и коллекций всегда используйте `==`. Проверка `is` для строк и целых чисел может давать неожиданные результаты из-за интернирования объектов в Python. **Отрицание:** ```python assert x != y # не равно assert x not in lst # не в коллекции assert not condition # общее отрицание ``` **Пользовательские сообщения при падении:** Добавьте сообщение после утверждения -- pytest покажет его вместе с самим утверждением: ```python assert score >= 50, f"Expected score >= 50, got {score}" # -> AssertionError: Expected score >= 50, got 42 # assert 42 >= 50 ``` **Сравнение чисел с плавающей точкой:** `0.1 + 0.2` в Python не равно точно `0.3` из-за двоичного представления с плавающей точкой. Используйте `pytest.approx`: ```python import pytest assert 0.1 + 0.2 == pytest.approx(0.3) # по умолчанию: относительная погрешность 1e-6 assert result == pytest.approx(1.5, rel=1e-3) # с точностью 0.1% assert result == pytest.approx(0.0, abs=1e-9) # абсолютная погрешность (лучше около нуля) ``` Используйте `rel` (относительная), когда величина большая; используйте `abs` (абсолютная), когда ожидаемое значение равно или близко к нулю (деление на ноль в относительной проверке было бы проблемой).

Паттерны утверждений на практике

#
**Равенство, неравенство и порядок:** ```python def test_equality(): assert 2 + 2 == 4 assert 'hello' != 'world' assert 10 > 5 assert 3 <= 3 def test_negation(): assert not False assert not [] # пустой список -- ложный assert not '' # пустая строка -- ложная assert not 0 # ноль -- ложный ``` **Проверка вхождения и типа:** ```python def test_membership(): items = ['apple', 'banana', 'cherry'] assert 'banana' in items assert 'grape' not in items def test_types(): items = ['apple', 'banana', 'cherry'] assert isinstance(items, list) assert isinstance(42, int) assert isinstance(3.14, float) # isinstance принимает кортеж типов: assert isinstance(42, (int, float)) # совпадает с любым ``` **Проверки словарей и строк:** ```python def test_dict_assertions(): user = {'name': 'Alice', 'age': 30, 'active': True} assert 'name' in user # ключ существует assert user['name'] == 'Alice' # значение правильное assert 'email' not in user # ключ отсутствует assert len(user) == 3 # точный размер def test_string_assertions(): msg = 'Hello, World!' assert msg.startswith('Hello') assert msg.endswith('!') assert 'World' in msg assert len(msg) == 13 assert msg.upper() == 'HELLO, WORLD!' ``` **Числа с плавающей точкой через pytest.approx:** ```python import math, pytest def test_sqrt(): assert math.sqrt(2) == pytest.approx(1.41421, rel=1e-4) def test_float_arithmetic(): # 0.1 + 0.2 = 0.30000000000000004 в Python assert 0.1 + 0.2 == pytest.approx(0.3) def test_near_zero(): result = 1e-10 - 1e-10 assert result == pytest.approx(0.0, abs=1e-15) ``` **Пользовательские сообщения при падении для контекста:** ```python # process_batch -- заглушка; иллюстрирует паттерн, не запускаемый фрагмент def test_with_context(): results = process_batch([1, 2, 3, 4, 5]) assert len(results) == 5, f"Expected 5 results, got {len(results)}: {results}" ```

Шпаргалка по утверждениям

#
```python # Равенство / неравенство assert x == y assert x != y # Порядок assert x > y assert x >= y assert x < y assert x <= y # Вхождение assert item in collection assert item not in collection # Тип assert isinstance(obj, SomeClass) # принимает подклассы assert type(obj) is int # только точный тип # Истинность assert value # истинное (ненулевое, непустое, не None) assert not value # ложное # Числа с плавающей точкой assert x == pytest.approx(y) # относительная погрешность по умолчанию 1e-6 assert x == pytest.approx(y, rel=1e-3) # погрешность 0.1% assert x == pytest.approx(y, abs=0.01) # абсолютная погрешность +-0.01 # Строки assert s.startswith('prefix') assert s.endswith('suffix') assert 'substring' in s # Пользовательское сообщение при падении assert condition, f"Описательное сообщение: получено {value}" ```
01

Утверждения для конвертера температуры

#

Напишите тесты для `to_celsius(f)`, которая конвертирует градусы Фаренгейта в Цельсий по формуле `(f - 32) * 5 / 9`. Проверьте результат для как минимум трёх значений: 32°F (0°C), 212°F (100°C) и 98.6°F (37°C). Используйте `pytest.approx` для сравнения чисел с плавающей точкой.

import pytest


def to_celsius(f):
    return (f - 32) * 5 / 9


def test_to_celsius():
    pass
Решение
import pytest


def to_celsius(f):
    return (f - 32) * 5 / 9


def test_freezing_point():
    assert to_celsius(32) == pytest.approx(0.0)

def test_boiling_point():
    assert to_celsius(212) == pytest.approx(100.0)

def test_body_temperature():
    assert to_celsius(98.6) == pytest.approx(37.0, rel=1e-3)
02

pytest.approx для площади круга

#

Реализуйте `circle_area(r)`, которая возвращает `pi * r^2`, используя `math.pi`. Напишите тесты, проверяющие, что площадь при `r=3` приблизительно равна `28.274` (с погрешностью 0.1%), при `r=1` -- приблизительно `3.14159`. Также протестируйте `r=0`.

import math
import pytest


def circle_area(r):
    pass  # реализуйте здесь


def test_circle_area():
    pass
Решение
import math
import pytest


def circle_area(r):
    return math.pi * r ** 2


def test_circle_area_r3():
    assert circle_area(3) == pytest.approx(28.274, rel=1e-3)

def test_circle_area_r1():
    assert circle_area(1) == pytest.approx(3.14159, rel=1e-4)

def test_circle_area_r0():
    assert circle_area(0) == pytest.approx(0.0)
03

Утверждения для содержимого списка

#

Напишите `top_scores(scores, n)`, которая возвращает топ `n` очков из списка, отсортированных по убыванию. Напишите тесты, проверяющие, что возвращаемый тип -- список, его длина равна `n`, и конкретные значения присутствуют или отсутствуют.

def top_scores(scores, n):
    pass  # реализуйте здесь


def test_top_scores():
    pass
Решение
def top_scores(scores, n):
    return sorted(scores, reverse=True)[:n]


def test_top_scores_returns_list():
    result = top_scores([5, 1, 8, 3, 9], 3)
    assert isinstance(result, list)

def test_top_scores_length():
    result = top_scores([5, 1, 8, 3, 9], 3)
    assert len(result) == 3

def test_top_scores_correct_values():
    result = top_scores([5, 1, 8, 3, 9], 3)
    assert 9 in result
    assert 8 in result
    assert 5 in result
    assert 1 not in result
04

Утверждения для ключей и значений словаря

#

Напишите `parse_user(s)`, которая разбирает строку вроде `'name:Alice,age:30'` в словарь. Напишите тесты, проверяющие, что конкретные ключи существуют, значения правильные, а лишние ключи отсутствуют.

def parse_user(s):
    pass  # "name:Alice,age:30" -> {'name': 'Alice', 'age': '30'}


def test_parse_user():
    pass
Решение
def parse_user(s):
    result = {}
    for pair in s.split(','):
        key, value = pair.split(':')
        result[key.strip()] = value.strip()
    return result


def test_parse_user_name():
    user = parse_user('name:Alice,age:30')
    assert 'name' in user
    assert user['name'] == 'Alice'

def test_parse_user_age():
    user = parse_user('name:Alice,age:30')
    assert 'age' in user
    assert user['age'] == '30'

def test_parse_user_no_extra_keys():
    user = parse_user('name:Alice,age:30')
    assert 'email' not in user
    assert len(user) == 2
05

Утверждения для свойств строки

#

Напишите `format_greeting(name, title)`, которая возвращает `'Dear {title}. {name},'`. Напишите тесты, проверяющие, что результат начинается с 'Dear', заканчивается на ',', содержит имя и не имеет двойных пробелов.

def format_greeting(name, title):
    pass  # реализуйте здесь


def test_format_greeting():
    pass
Решение
def format_greeting(name, title):
    return f'Dear {title}. {name},'


def test_greeting_starts_with_dear():
    result = format_greeting('Smith', 'Dr')
    assert result.startswith('Dear')

def test_greeting_ends_with_comma():
    result = format_greeting('Smith', 'Dr')
    assert result.endswith(',')

def test_greeting_contains_name():
    result = format_greeting('Smith', 'Dr')
    assert 'Smith' in result

def test_greeting_no_double_spaces():
    result = format_greeting('Smith', 'Dr')
    assert '  ' not in result