Тестируем функции и границы

Как тестировать функции: типичные случаи, граничные значения и пустые входы.

Граничный случай (edge case) — вход на «краю» допустимого: пустая коллекция, ноль, отрицательное, очень большое значение, граница диапазона.

Чистые функции тестировать проще всего

Чистая функция зависит только от аргументов и не имеет побочных эффектов: те же входы — тот же результат. Её не нужно ничем окружать, не нужны моки — просто «вход → ожидаемый выход». С таких функций и стоит начинать.

Три группы случаев, которые стоит покрыть

  1. Типичные. Обычные, ожидаемые входы — «счастливый путь».
  2. Граничные. Пустота, ноль, минимум/максимум, переход через порог.
  3. Некорректные. То, что функция должна отвергнуть (об этом — отдельный урок про исключения).

Пример: функция со средним значением

Считаем среднее списка чисел. Думаем о краях: что если список пустой? что если одно число?

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, а может поднимать исключение — всё зависит от того, как договорились. Тест фиксирует это решение и делает его явным. Когда вы пишете тест на граничный случай, вы фактически проектируете поведение функции, а не просто проверяете уже написанный код. Поэтому полезно думать о краях ещё до реализации.

Итог

  • Чистые функции — самый простой объект для тестов: вход → выход, без окружения.
  • Покрывайте типичные, граничные и некорректные случаи.
  • Граничные: пустота, ноль, отрицательные, ровно порог, один элемент.
  • Тесты фиксируют договорённости о поведении, а не только ловят баги.
Проверьте себя
1. Что такое граничный случай (edge case)?
AСамый частый ввод
BВход на краю допустимого: пустота, ноль, минимум/максимум, граница диапазона
CОшибка в тесте
DСлучайное значение
2. Почему чистые функции тестировать проще всего?
AОни всегда возвращают None
BОни зависят только от аргументов и не имеют побочных эффектов — не нужно окружение и моки
CИх нельзя сломать
DДля них не нужны assert
3. Что важно проверить для функции с порогом «скидка от 1000»?
AТолько 0
BЗначения вокруг границы: 999, 1000, 1001
CТолько очень большие числа
DНичего, порог не важен
Поддержать проект