Параметризация через 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) — они укажут на конкретный упавший набор. - Таблица
(вход, ожидание)делает добавление случаев тривиальным. - В приоритете — бизнес-логика, ветвления, границы и места прошлых багов.