Лекция 1. Качество кода и тестирование (Code Quality and Testing)¶
Трудно представить современный программный проект без тестирования. Тестирование осуществляется практически на всех этапах разработки: от unit-тестов (а иногда и раньше — при TDD) до функционального и нагрузочного тестирования готового продукта.
В этой лекции остановимся на автономном тестировании (unit-тестировании).
Автономное тестирование. Основные понятия¶
Автономный тест — автоматизированная часть кода, которая вызывает тестируемую единицу работы и затем проверяет некоторые предположения о её конечном результате. — Рой Ошероув, «Искусство автономного тестирования»
В качестве тестируемой единицы может выступать как отдельная функция/метод, так и совокупность классов. Идея — единица представляет логически законченную сущность программы.
Автономное тестирование называют также модульным или unit-тестированием. Далее «тест» = unit-тест.
Важная характеристика unit-теста — повторяемость. Результат не должен зависеть от окружения. Если приходится обращаться к внешнему миру (БД, сеть, файловая система), нужно подменять «мир» заглушкой (mock/fake/stub).
Фреймворки для unit-тестирования в Python¶
Самые распространённые:
unittest— стандартный, входит в библиотеку Python. Архитектура в стиле xUnit (JUnit, NUnit).pytest— мощный сторонний фреймворк, ближе к «духу» Python. Тесты — обычные функции сassert, нет необходимости создавать классы.nose2— продолжениеnose. Расширяетunittest.
В современной индустрии де-факто стандарт — pytest.
Без фреймворка — наивный подход¶
Простой модуль для калькулятора:
# calc.py
def add(a, b): return a + b
def sub(a, b): return a - b
def mul(a, b): return a * b
def div(a, b): return a / b
Тесты «вручную»:
# test_calc.py
import calc
def test_add():
if calc.add(1, 2) == 3:
print("test_add OK")
else:
print("test_add FAIL")
def test_sub():
if calc.sub(4, 2) == 2:
print("test_sub OK")
else:
print("test_sub FAIL")
test_add()
test_sub()
Проблемы такого подхода:
- неунифицированная выходная информация;
- громоздкий код;
- нужно думать про архитектуру тестов;
- нет инструментов фильтрации, пропуска, сборки в группы.
Это приводит к мысли о том, что нужен фреймворк.
unittest — стандартный фреймворк¶
# tests/test_calc.py
import unittest
import calc
class CalcTest(unittest.TestCase):
def test_add(self):
self.assertEqual(calc.add(1, 2), 3)
def test_sub(self):
self.assertEqual(calc.sub(4, 2), 2)
def test_mul(self):
self.assertEqual(calc.mul(2, 5), 10)
def test_div(self):
self.assertEqual(calc.div(8, 4), 2)
def test_div_zero(self):
with self.assertRaises(ZeroDivisionError):
calc.div(1, 0)
if __name__ == "__main__":
unittest.main()
Запуск:
python -m unittest tests/test_calc.py # минимум информации
python -m unittest -v tests/test_calc.py # подробно
python -m unittest # test discovery — найдёт все test_*.py
Структурные элементы unittest¶
- Test fixture — подготовка окружения для тестов и очистка после (
setUp,tearDown). - Test case — элементарная единица тестирования. Класс-наследник
TestCase. - Test suite — коллекция тестов или других suite.
- Test runner — компонент, оркестрирующий запуск и предоставляющий результат.
Методы при запуске тестов¶
| Метод | Когда вызывается | Декоратор |
|---|---|---|
setUp() |
Перед каждым тестом | — |
tearDown() |
После каждого теста | — |
setUpClass(cls) |
Один раз перед всеми тестами класса | @classmethod |
tearDownClass(cls) |
Один раз после всех тестов класса | @classmethod |
setUpModule() / tearDownModule() |
На уровне модуля | — |
class CalcTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
print("setUpClass — один раз")
@classmethod
def tearDownClass(cls):
print("tearDownClass — один раз")
def setUp(self):
print(f"Set up for [{self.shortDescription()}]")
def tearDown(self):
print(f"Tear down for [{self.shortDescription()}]")
def test_add(self):
"""Add operation test"""
self.assertEqual(calc.add(1, 2), 3)
Основные assert-методы¶
| Метод | Эквивалент |
|---|---|
assertEqual(a, b) |
a == b |
assertNotEqual(a, b) |
a != b |
assertTrue(x) |
bool(x) is True |
assertFalse(x) |
bool(x) is False |
assertIs(a, b) |
a is b |
assertIsNone(x) |
x is None |
assertIn(a, b) |
a in b |
assertIsInstance(a, cls) |
isinstance(a, cls) |
assertRaises(exc) |
контекстный менеджер для проверки исключения |
assertAlmostEqual(a, b) |
round(a-b, 7) == 0 (для float) |
assertGreater(a, b) |
a > b |
assertRegex(s, r) |
регулярка r находит в строке s |
Пропуск тестов¶
import sys
class CalcTest(unittest.TestCase):
@unittest.skip("причина пропуска")
def test_skip(self):
self.fail("не должен выполниться")
@unittest.skipIf(sys.platform == "win32", "только не Windows")
def test_unix(self):
...
@unittest.expectedFailure
def test_known_bug(self):
self.assertEqual(1, 2)
pytest — рекомендуемая альтернатива¶
# tests/test_calc.py
import pytest
import calc
def test_add():
assert calc.add(1, 2) == 3
def test_div_zero():
with pytest.raises(ZeroDivisionError):
calc.div(1, 0)
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300),
])
def test_add_param(a, b, expected):
assert calc.add(a, b) == expected
Запуск:
pip install pytest
pytest # найдёт и запустит все test_*.py
pytest -v # подробно
pytest -k "div" # только тесты, содержащие "div" в имени
pytest --lf # повторить только упавшие
Преимущества pytest:
- обычный
assert(безself.assertEqual(...)); - параметризация (
@pytest.mark.parametrize) — один тест на N наборов данных; - fixtures — мощная замена
setUp/tearDown; - богатая экосистема плагинов (
pytest-covдля покрытия,pytest-asyncio,pytest-mock).
Pytest fixtures¶
import pytest
@pytest.fixture
def sample_data():
"""Подготовка данных для теста — переиспользуется в нескольких тестах."""
return [1, 2, 3, 4, 5]
def test_sum(sample_data):
assert sum(sample_data) == 15
def test_max(sample_data):
assert max(sample_data) == 5
Тестирование в Go (testing)¶
Встроенный пакет — никаких сторонних фреймворков не нужно (хотя есть testify для удобных assert).
// calc.go
package calc
func Add(a, b int) int { return a + b }
func Sub(a, b int) int { return a - b }
func Mul(a, b int) int { return a * b }
func Div(a, b int) int { return a / b }
// calc_test.go
package calc
import "testing"
func TestAdd(t *testing.T) {
got := Add(1, 2)
if got != 3 {
t.Errorf("Add(1, 2) = %d; want 3", got)
}
}
func TestSub(t *testing.T) {
got := Sub(4, 2)
if got != 2 {
t.Errorf("Sub(4, 2) = %d; want 2", got)
}
}
Запуск:
go test # тесты в текущем пакете
go test -v # подробно
go test ./... # все пакеты рекурсивно
go test -run TestAdd # только тесты с именем, содержащим TestAdd
go test -race # с обнаружением гонок
go test -cover # с покрытием
Табличные тесты (table-driven tests) — идиома Go¶
Аналог parametrize:
func TestAddTable(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"simple", 1, 2, 3},
{"zero", 0, 0, 0},
{"negative", -1, 1, 0},
{"large", 100, 200, 300},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
Бенчмарки в Go¶
Запуск: go test -bench=. — b.N подбирается автоматически.
Покрытие кода (coverage)¶
# Python с pytest
pytest --cov=calc --cov-report=term-missing
# Go
go test -cover ./...
go test -coverprofile=cover.out && go tool cover -html=cover.out
Высокое покрытие — не гарантия качества. Покрытие 80%+ ≠ нет багов. Качество тестов важнее количества.
Что должен тестировать unit-тест¶
- ✅ Логику одной функции / метода / класса.
- ✅ Граничные случаи (пустой ввод, нули, максимумы, отрицательные числа).
- ✅ Ожидаемые исключения на некорректных входных данных.
- ❌ Не должен ходить в сеть, БД, файловую систему без моков.
- ❌ Не должен зависеть от порядка запуска других тестов.
Что использовать в больших проектах¶
- Python:
pytest+pytest-cov+pytest-mock+tox(для матрицы версий) + линтеры (ruff,mypy). - Go: встроенный
testing+testify(опционально) + race detector (-race) +golangci-lint.
Контрольные вопросы¶
- Что такое unit-тест? Почему важна повторяемость?
- В чём ключевые отличия
unittestиpytest? - Что такое test fixture? Какие методы есть в
unittest.TestCaseдля подготовки и очистки? - Что такое параметризованный тест? Покажите пример.
- Что такое табличные тесты в Go? В чём их преимущество?
- Что такое покрытие кода? Гарантирует ли 100 %-ное покрытие отсутствие багов?
- Какие данные не должны быть в unit-тестах (что нужно мокать)?