Тестируем функции и границы
Как тестировать функции: типичные случаи, граничные значения и пустые входы.
Граничный случай (edge case) — вход на «краю» допустимого: пустая коллекция, ноль, отрицательное, очень большое значение, граница диапазона.
Чистые функции тестировать проще всего
Чистая функция зависит только от аргументов и не имеет побочных эффектов: те же входы — тот же результат. Её не нужно ничем окружать, не нужны моки — просто «вход → ожидаемый выход». С таких функций и стоит начинать.
Три группы случаев, которые стоит покрыть
- Типичные. Обычные, ожидаемые входы — «счастливый путь».
- Граничные. Пустота, ноль, минимум/максимум, переход через порог.
- Некорректные. То, что функция должна отвергнуть (об этом — отдельный урок про исключения).
Пример: функция со средним значением
Считаем среднее списка чисел. Думаем о краях: что если список пустой? что если одно число?
import unittest
def average(numbers):
if not numbers:
return 0 # договорённость: среднее пустого списка = 0
return sum(numbers) / len(numbers)
class TestAverage(unittest.TestCase):
# Типичный случай
def test_typical(self):
self.assertEqual(average([2, 4, 6]), 4)
# Граница: один элемент
def test_single(self):
self.assertEqual(average([10]), 10)
# Граница: пустой список
def test_empty(self):
self.assertEqual(average([]), 0)
# Граница: отрицательные числа
def test_negatives(self):
self.assertEqual(average([-2, -4]), -3)
# float — через assertAlmostEqual
def test_fractional(self):
self.assertAlmostEqual(average([1, 2]), 1.5)
unittest.main(argv=[''], exit=False, verbosity=2)
Вывод:
test_empty (__main__.TestAverage.test_empty) ... ok test_fractional (__main__.TestAverage.test_fractional) ... ok test_negatives (__main__.TestAverage.test_negatives) ... ok test_single (__main__.TestAverage.test_single) ... ok test_typical (__main__.TestAverage.test_typical) ... ok ---------------------------------------------------------------------- Ran 5 tests in 0.000s OK
Обратите внимание: тест test_empty не просто проверяет код — он фиксирует договорённость, что среднее пустого списка равно 0. Без теста кто-нибудь мог бы «поправить» функцию, и поведение бы поменялось незаметно.
Как искать граничные случаи
Полезные вопросы к каждой функции:
- Что будет на пустом входе (пустая строка, список, словарь)?
- Что на нуле и на отрицательных числах?
- Где порог в логике (например, скидка от 1000 рублей — проверьте 999, 1000, 1001)?
- Что на одном элементе и на очень многих?
Пример с порогом
import unittest
def shipping_cost(total):
# Бесплатная доставка от 1000 включительно
return 0 if total >= 1000 else 300
class TestShipping(unittest.TestCase):
def test_below_threshold(self):
self.assertEqual(shipping_cost(999), 300)
def test_exactly_threshold(self):
self.assertEqual(shipping_cost(1000), 0) # граница включительно
def test_above_threshold(self):
self.assertEqual(shipping_cost(1001), 0)
unittest.main(argv=[''], exit=False, verbosity=2)
Вывод:
test_above_threshold (__main__.TestShipping.test_above_threshold) ... ok test_below_threshold (__main__.TestShipping.test_below_threshold) ... ok test_exactly_threshold (__main__.TestShipping.test_exactly_threshold) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.000s OK
Тест test_exactly_threshold проверяет ровно границу. Именно на ней чаще всего прячутся ошибки «больше» против «больше-или-равно».
Не тестируйте реализацию через случайные примеры
Подбирайте входные данные осмысленно, а не «лишь бы какие». Каждый тест должен покрывать отдельный класс ситуаций: один типичный случай, по одному на каждую границу, по одному на каждую ветвь логики. Три теста с числами 5, 6 и 7 почти бесполезны — они все про «обычное положительное число» и проверяют одно и то же. А вот 0, -1, пустой список и значение ровно на пороге — это четыре разных класса ситуаций, и каждый ловит свой потенциальный баг.
Договорённости важнее «как получится»
Иногда поведение на краю — это вопрос решения, а не объективной истины. Среднее пустого списка может быть нулём, может быть None, а может поднимать исключение — всё зависит от того, как договорились. Тест фиксирует это решение и делает его явным. Когда вы пишете тест на граничный случай, вы фактически проектируете поведение функции, а не просто проверяете уже написанный код. Поэтому полезно думать о краях ещё до реализации.
Итог
- Чистые функции — самый простой объект для тестов: вход → выход, без окружения.
- Покрывайте типичные, граничные и некорректные случаи.
- Граничные: пустота, ноль, отрицательные, ровно порог, один элемент.
- Тесты фиксируют договорённости о поведении, а не только ловят баги.