Параметризация через subTest

Параметризация через subTest и принципы того, что вообще стоит покрывать тестами.

subTest — способ прогнать одну проверку на множестве входных данных так, чтобы падение на одном наборе не останавливало остальные.

Проблема: много похожих случаев

Нередко нужно проверить функцию на десятке пар «вход → ожидание». Писать десять почти одинаковых test_* методов — много шума. Прогнать их в обычном цикле тоже плохо: первый же упавший assert остановит цикл, и про остальные входы вы не узнаете.

Решение: subTest

with self.subTest(...) оборачивает каждую итерацию. Если одна падает, unittest запоминает это и продолжает остальные. В отчёте видно, на каком именно наборе данных провал — для этого в subTest передают подписывающие параметры.

import unittest

def is_even(n):
    return n % 2 == 0

class TestIsEven(unittest.TestCase):
    def test_even_numbers(self):
        cases = [0, 2, 4, 10, 100]
        for n in cases:
            with self.subTest(n=n):     # подпись: какое n проверяем
                self.assertTrue(is_even(n))

    def test_odd_numbers(self):
        for n in [1, 3, 5, 99]:
            with self.subTest(n=n):
                self.assertFalse(is_even(n))

unittest.main(argv=[''], exit=False, verbosity=2)

Вывод:

test_even_numbers (__main__.TestIsEven.test_even_numbers) ... ok
test_odd_numbers (__main__.TestIsEven.test_odd_numbers) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Параметризация «вход → ожидание»

Удобный приём — список кортежей (вход, ожидаемое). Таблицу легко расширять новыми случаями, не дублируя код:

import unittest

def fizzbuzz(n):
    if n % 15 == 0:
        return "FizzBuzz"
    if n % 3 == 0:
        return "Fizz"
    if n % 5 == 0:
        return "Buzz"
    return str(n)

class TestFizzBuzz(unittest.TestCase):
    def test_cases(self):
        table = [
            (1, "1"),
            (3, "Fizz"),
            (5, "Buzz"),
            (15, "FizzBuzz"),
            (30, "FizzBuzz"),
        ]
        for n, expected in table:
            with self.subTest(n=n):
                self.assertEqual(fizzbuzz(n), expected)

unittest.main(argv=[''], exit=False, verbosity=2)

Вывод:

test_cases (__main__.TestFizzBuzz.test_cases) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Пять наборов проверены внутри одного теста. Если, скажем, для 15 функция вернёт неверное значение, unittest укажет в отчёте именно (n=15) — остальные наборы при этом всё равно отработают.

Что покрывать тестами в первую очередь

Тестировать всё подряд дорого и не нужно. Расставляйте приоритеты:

Покрывать обязательноМожно реже
бизнес-логику и расчётытривиальные геттеры/сеттеры
граничные случаи и ветвленияобёртки над сторонним кодом
места, где находили багиодноразовые скрипты
публичный API модулячистый UI без логики

Ориентир: тест полезен там, где есть логика, которая может сломаться, и где поломка дорого обойдётся. Код без ветвлений и решений (просто присвоение полю) тестировать почти бессмысленно.

subTest и читаемость отчёта

Без subTest у вас есть выбор из двух плохих вариантов: либо десяток почти одинаковых методов (много шума), либо обычный цикл, который молча остановится на первом же провале. subTest — золотая середина: один компактный метод, но при этом каждый набор проверяется независимо. Когда какой-то набор падает, в сообщении об ошибке видна именно та подпись, что вы передали (например, (n=15)), — вы сразу знаете проблемный вход, не гадая. Это превращает таблицу случаев в наглядный, самодокументированный тест.

Покрывать ли каждую строку

Соблазн «покрыть всё на 100%» приводит к бессмысленным тестам на тривиальный код и к ложному чувству безопасности. Полезнее держать в голове вопрос: «если эта строка сломается, заметит ли это хоть кто-то и насколько дорого обойдётся?». Расчёт цены, правило скидки, парсинг даты — да, обязательно. Геттер, который просто возвращает поле, — почти никогда. Тесты — это инвестиция: вкладывайте усилия туда, где риск и цена ошибки выше, а не размазывайте поровну по всему коду.

Итог

  • subTest прогоняет одну проверку на многих входах, не останавливаясь на первом провале.
  • Передавайте в subTest подписи (n=n) — они укажут на конкретный упавший набор.
  • Таблица (вход, ожидание) делает добавление случаев тривиальным.
  • В приоритете — бизнес-логика, ветвления, границы и места прошлых багов.
Проверьте себя
1. Чем subTest лучше обычного цикла с assert?
AРаботает быстрее
BПадение на одном наборе не останавливает проверку остальных, и видно, какой набор упал
CНе требует TestCase
DОтключает assert
2. Зачем передавать в subTest подпись, например n=n?
AДля скорости
BЧтобы в отчёте было видно, на каком именно наборе данных произошёл провал
CЭто обязательный синтаксис
DЧтобы пропустить набор
3. Что стоит покрывать тестами в первую очередь?
AТривиальные геттеры/сеттеры
BБизнес-логику, ветвления, границы и места прошлых багов
CИмпорты
DКомментарии
Поддержать проект