Юнит-тестирование
Проверка в БД — SQL для тестировщика, Основы БД, транзакции, PostgreSQL. Карта — о разделе.
Юнит-тестирование появилось для того, чтобы автоматизировать проверку мелких частей кода и мгновенно находить баги при внесении изменений, избавляя разработчиков от долгого ручного тестирования. Первым языком программирования, в котором юнит-тестирование оформилось в привычный нам автоматизированный вид, стал Smalltalk, для которого Кент Бек в 1989 году написал фреймворк SUnit. До появления автоматизированных тестов программисты проверяли код вручную. С ростом сложности программ этот подход привел к серьезным проблемам:
- разработчики боялись трогать старый работающий код, так как любое изменение могло незаметно сломать программу в другом месте.
- ошибки находили на этапе ручного тестирования или уже у пользователей. Чем позже найдена ошибка, тем дороже её исправление.
- программисту приходилось запускать всё приложение целиком, кликать по интерфейсу и вводить данные вручную, чтобы проверить одну строчку кода.
Юнит-тесты решили это, превратив проверку в нажатие одной кнопки, которая за доли секунды выполняет тысячи тестов и сразу показывает, где именно упал код.
Сам термин «unit test» пришел из электронной инженерии, где перед сборкой платы тестировали отдельные независимые детали (юниты). В 1950-60-х годах во время проектов NASA и Mercury применялись похожие концепции изолированных проверок. Однако современное автоматизированное юнит-тестирование, каким мы его знаем сегодня, родилось в Smalltalk - Кент Бек (Kent Beck), один из создателей методологии экстремального программирования (XP) и манифеста Agile, в 1989 году он написал миниатюрный фреймворк SUnit для Smalltalk. Позже Кент Бек вместе со своим коллегой Эрихом Гаммой перенес эту идею в Java, создав культовый фреймворк JUnit (1997 год). Он стал настолько популярным, что по его образу и подобию были созданы тестовые движки практически для всех языков программирования (их называют семейством xUnit: NUnit для .NET, pytest для Python, phpUnit и т.д.).
Unit — самый дешёвый уровень обратной связи: секунды, без браузера. Здесь — структура AAA, примеры на JS и Python, принцип изоляции и связь с тестовыми дублёрами. Практика с pytest — в Подготовка среды и создание первого теста. Что такое функция, метод, класс как объект теста — Код — о разделе; изоляция зависимостей — Зависимости — о разделе.
Play ITЗагрузка интерактивного демо…
Юнит-тестирование
Что такое юнит-тест и как он работает
Юнит-тест - это программа, которая проверяет другую программу (маленький кусочек).
Берём конкретную функцию, даём ей входные данные и смотрим, что вернулось. Если вернулось то, что ожидали - тест зелёный, если нет - красный.
Отличительная черта юнит-тестов — это строгая изоляция (Solitary Testing). Особенность заключается в правилах:
- Тестируется не программа и не экран, а один изолированный кирпичик кода — обычно конкретный метод или функция.
- Настоящий юнит-тест не имеет права ходить в реальную базу данных, в интернет по API или читать файлы с диска. Вместо реальных тяжелых зависимостей используются «заглушки» и поддельные объекты (моки/стабы), которые имитируют их поведение.
- Благодаря изоляции от сети и диска, тысячи юнит-тестов выполняются за несколько секунд. Они всегда выдают одинаковый результат (детерминированы), что позволяет запускать их перед каждым сохранением или отправкой кода на сервер.
Именно Кент Бек продвинул идею TDD (Test-Driven Development) сначала пишется падающий юнит-тест, под него пишется минимальный рабочий код, а затем код улучшается (рефакторится) под защитой этого теста.
Любой юнит-тест логически делится на три блока — паттерн AAA (Arrange — Act — Assert, «подготовка — действие — проверка»). Это не требование фреймворка, а соглашение о читаемости, при падении теста сразу видно, на каком этапе искать ошибку.
| Блок | По-русски | Что происходит |
|---|---|---|
| Arrange | Подготовка | Настройка окружения, создание объектов, подготовка входных данных |
| Act | Действие | Вызов тестируемого метода, выполнение целевой операции, сохранение результата |
| Assert | Проверка | Сравнение результата с ожидаемым, проверка исключений, подтверждение корректности |
Arrange (подготовка)
Это этап настройки тестового окружения. Здесь вы приводите систему в то состояние, которое требуется для проведения проверки. Создаются объекты (моки, стабы), подготавливаются тестовые данные, настраиваются начальные условия, запускается необходимый контекст (например, подключение к БД in-memory), чтобы создать предусловия для теста. Именно здесь вы "собираете сцену" перед вызовом тестируемого метода.
На этом этапе тест не вызывает проверяемую логику — он готовит сцену:
- создаёт экземпляр класса или импортирует функцию (система под тестом, SUT);
- задаёт входные данные: аргументы, тестовые записи, JSON-файл;
- подменяет внешние зависимости заглушками и моками (тестовые дублёры);
- при необходимости сбрасывает состояние — очистка коллекции, фиксация «текущего времени» через clock-fake.
Чем меньше кода в Arrange, тем проще читать тест. Общую подготовку для группы кейсов выносят в beforeEach / setUp, но каждый тест должен оставаться понятным без прыжков по файлу.
Act (действие)
Это этап выполнения тестируемого сценария. Здесь вы вызываете тот метод или функцию, которую на самом деле хотите протестировать. Происходит одно действие (как правило, один вызов метода) над подготовленными в Arrange объектами, с целью стимулировать систему (SUT — System Under Test) так, чтобы она выдала результат, который вы собираетесь проверять. Действие должно быть максимально простым и занимать в коде 1–3 строки (обычно присваивание результата вызова переменной).
Одна целевая операция — то, что именно вы проверяете:
- вызов метода
calculateDiscount(cart, user); - отправка HTTP-запроса в тестируемый handler (без реальной сети, если это unit);
- повторный вызов после изменения состояния.
Результат сохраняют в переменную (result, response, exception). В Act не смешивают проверки — иначе при падении непонятно: сломалась логика или неверное ожидание в assert.
Assert (проверка)
Это этап верификации. Здесь вы сравниваете фактический результат выполнения действия с ожидаемым результатом. Используются встроенные или библиотечные функции проверки (assertions). Проверяется не только возвращаемое значение, но и побочные эффекты (изменилось ли состояние объекта, был ли вызван определенный метод мока), убедиться, что поведение системы соответствует спецификации (пройти тест) или указать на ошибку (упасть).
Здесь тест отвечает на вопрос «получили ли мы то, что ожидали?»:
- сравнение возвращаемого значения с эталоном (
expect(result).toBe(5),assert result == 5); - проверка изменённого состояния объекта (
user.balanceуменьшился на 100); - ожидание исключения (
pytest.raises,assertThrows); - для моков — что зависимость вызвана нужное число раз с нужными аргументами.
Один тест — один смысловой вопрос. Несколько assert допустимы, если они проверяют одно поведение (например, и код ответа, и тело JSON при создании заказа).
Как оформлять в коде
Каждый этап должен быть отделен от других пустой строкой для улучшения читаемости. В блоке Act должно быть строго одно действие (один вызов метода). Если вы вызываете два метода подряд — это уже два теста. В блоке Assert не должно быть сложной условной логики (if). Только прямые сравнения.
Плохой пример (все смешано):
# Смешаны Arrange и Act
user = User("John")
result = user.get_name()
# Act и Assert в одной строке с подготовкой
assert user.get_age() == 30
Хороший пример (AAA):
# Arrange
user = User("John", birth_year=1990)
expected_age = 36
# Act
current_age = user.calculate_age(current_year=2026)
# Assert
assert current_age == expected_age
Блоки визуально разделяют пустой строкой и коротким комментарием // Arrange / # Act — так проще сканировать файл глазами:
Arrange → подготовка данных
(пустая строка)
Act → вызов тестируемой функции
(пустая строка)
Assert → проверка результата
Тот же шаблон встречается под именем Given — When — Then (BDD): дано ≈ Arrange, когда ≈ Act, тогда ≈ Assert.
Пример ниже — псевдокод для иллюстрации AAA: схема «подготовили → вызвали → сравнили». Дальше — те же шаги на настоящем Jest и pytest.
Тестируем такую функцию:
функция сложения(a, b) {
вернуть a + b
}
Тест выглядит так:
тест "2 + 3 = 5" {
// Arrange
a = 2
b = 3
// Act
результат = сложение(a, b)
// Assert
если результат == 5 → тест пройден
иначе → тест упал
}
В реальном коде, как-то так. Давайте поглядим на JavaScript.
Тестируемый код - math.js:
function add(a, b) {
return a + b;
}
module.exports = { add };
Тест - math.test.js:
const { add } = require('./math');
test('сложение 2 и 3 возвращает 5', () => {
// Arrange
const a = 2;
const b = 3;
// Act
const result = add(a, b);
// Assert
expect(result).toBe(5);
});
Или на Python.
Тестируемый код: math.py:
def add(a, b):
return a + b
Тест (test_math.py):
from math import add
def test_add_returns_sum():
# Arrange
a = 2
b = 3
# Act
result = add(a, b)
# Assert
assert result == 5
Границы возраста — один тест, много входов (pytest)
Pytest — это самый популярный фреймворк для автоматического тестирования программного обеспечения на языке Python. Он позволяет разработчикам и QA-инженерам писать как простые модульные тесты (юнит-тесты), так и сложные интеграционные или сквозные (E2E) тесты для проверки корректности работы кода. Он очень распространён именно потому что фреймворк стал стандартом в индустрии благодаря минимуму шаблонного кода, простому синтаксису, информационным отчетам, автоматическим поискам тестов, огромной экосистеме.
Для запуска этого теста достаточно установить библиотеку через ваш пакетный менеджер (например, pip install pytest) и ввести в терминале короткую команду:
pytest
Чтобы понять разницу, посмотрите на пример тестирования простой функции, которая складывает два числа:
# Код вашей программы (main.py)
def add(a, b):
return a + b
# Код теста (test_main.py)
def test_add_positive_numbers():
assert add(2, 3) == 5 # Обычная проверка через assert
Анализ граничных значений — это техника тест-дизайна, основанная на проверке поведения программы на границах допустимых диапазонов данных.
Ошибки в коде чаще всего возникают именно там, где меняются условия (например, вместо < разработчик случайно написал <=).
Представьте форму регистрации, где поле «Возраст» принимает значения от 18 до 60 лет.
- Корректный класс (внутри границы). от 18 до 60.
- Некорректные классы (за границами). меньше 18 и больше 60.
Граничными значениями здесь являются 18 и 60. При тестировании по этой технике проверяют сами границы, а также значения на один шаг в обе стороны от них.
Для диапазона 18–60 тестируют следующие точки:
- 17 (сразу за нижней границей — отказ)
- 18 (точно на нижней границе — успех)
- 19 (сразу внутри границы — успех)
- 59 (сразу внутри границы — успех)
- 60 (точно на верхней границе — успех)
- 61 (сразу за верхней границей — отказ)
Параметризация — это способ запуска одного и того же теста с разными наборами входных данных без дублирования кода. Вместо того чтобы писать 6 отдельных функций для проверки возраста, вы пишете один тест, а pytest автоматически запускает его 6 раз, подставляя нужные числа.
Техника анализа граничных значений в коде удобно выражается через параметризацию: один тест, несколько пар "вход → ожидание".
Код ITЗагрузка примера кода…
Так проще читать отчёт — видно, какая граница сломалась, а не "упал тест с именем test_everything".
Если тест ходит в базу данных, вызывает реальный API, создаёт файлы на диске, проверяет две несвязанные функции в одном кейсе или зависит от sleep(5) — это уже не юнит-тест, а интеграционный или E2E. Граница не всегда чёткая, но ориентир простой: изолирован ли кусок логики и быстрый ли прогон?
Типичные ошибки в первых unit-тестах
У начинающих разработчиков и QA повторяется один и тот же набор проблем:
- В одном тесте проверяется сразу несколько правил. При падении сложно понять причину.
- Проверяются внутренние детали реализации вместо внешнего поведения функции.
- Используются "магические" значения без объяснения, из-за чего падает читаемость.
- Нет граничных значений, хотя именно они чаще всего ломаются в продакшене.
- Случайные данные (
random) применяются без фиксированного seed, и тест становится нестабильным.
Каждый unit-тест отвечает на один вопрос формата "при условии X функция возвращает Y". Для системных стыков лучше перейти к интеграционному тестированию, а для выбора входных наборов открыть тест-дизайн.
Разбор кейса — "зелёные" тесты и реальный дефект
Зеленый тест — это тест, который успешно выполнился и не обнаружил ошибок в коде. В консоли Pytest успешные тесты подсвечиваются зеленым цветом.
Дефект (баг) — это несоответствие между фактическим поведением программы и тем, как она должна работать согласно требованиям. Если тест находит дефект, он падает и становится красным. Дефект — это официальный термин в тестировании. В разговорной речи его называют багом (bug) или ошибкой (error).
В автоматизации тестирования действует цветовая кодировка («светофор»):
- Зеленый (Passed) - программа работает правильно, тест пройден.
- Красный (Failed) - тест упал, в коде или в самом тесте есть ошибка.
Главное правило разработчиков: «Держи сборку зеленой». Это значит, что перед отправкой кода в общую систему все тесты должны успешно проходить.
Ситуация:
- В сервисе подписок была функция расчёта даты окончания пробного периода.
- Набор unit-тестов показывал 100% прохождение.
- В продакшене у части пользователей пробный период завершался на день раньше.
Причина:
- В тестах использовали только "обычные" даты, без перехода через конец месяца и часовой пояс.
- Не было фиксации временной зоны в тестовой среде.
Что изменили:
- Добавили параметризованные тесты на граничные случаи — конец месяца, високосный год, переход на летнее время.
- Подменили системное время через clock abstraction и убрали зависимость от локальной машины.
- Разделили тесты на группы: вычисление даты и форматирование ответа.
Результат:
- Мутант с заменой
plusDays(14)наplusDays(13)стал убиваться. - Ошибка воспроизводилась стабильно в тестах и была исправлена до следующего релиза.
Вывод:
Сильные unit-тесты проверяют контракт поведения на границах, а не только "счастливый путь". Для усиления таких наборов полезно дополнительно применять мутационное тестирование.
Юнит-тесты живут в папке проекта:
проект/
├── src/ # основной код
│ └── math.js
└── tests/ # тесты (отдельная папка)
└── test_math.js
Или рядом, в зависимости от соглашения:
проект/
├── math.js
└── math.test.js # тест лежит рядом
Что такое юнит-тестирование
Юнит-тестирование (или модульное тестирование) — это процесс проверки отдельных, минимально возможных компонентов программы (функций, методов или классов) в полной изоляции от остальной системы.
В отличие от самого юнит-теста, это именно процесс. Он обычно безопасен, вы можете полностью переписать внутренности функции, чтобы она работала быстрее. Если после этого юнит-тест остался зеленым — вы ничего не сломали. А если код упал, юнит-тест сразу указывает точную строчку и функцию, где засели дефекты.
Юнит-тестирование — это уровень проверки программного обеспечения, направленный на верификацию отдельных единиц кода — отдельных функций, методов, классов или структур, рассматриваемых как автономные логические компоненты. Это наименьший из всех уровней тестирования по гранулярности; выше располагаются интеграционное, системное и приёмочное тестирование. Юнит-тестирование выполняется разработчиком на этапе написания кода и, в идеале, становится неотъемлемой частью цикла разработки — одновременным с написанием основной логики.
Слово юнит (от англ. unit — "единица") не имеет строгого формального определения, зависящего от языка или архитектуры. В процедурных языках юнитом часто является функция. В объектно-ориентированных — отдельный публичный метод класса или весь класс при условии его замкнутости. В функциональных языках — чистая функция. Ключевое требование: юнит должен быть изолируемым. Изоляция означает, что его поведение может быть проверено без необходимости запускать всю систему или обращаться к внешним ресурсам.
Цель юнит-тестирования — подтверждение ожидаемого поведения модуля для заданных входных условий. Тест отвечает на вопрос: "при таких входных данных (и таком состоянии, если нужно) результат и побочные эффекты соответствуют контракту?"
Юнит-тест — форма исполняемой спецификации: он фиксирует что должно происходить с точки зрения вызывающего кода (публичный API класса, экспортируемая функция). Как устроены private-методы и внутренние вызовы — предмет рефакторинга; тесты не должны ломаться при перестановке внутренностей, если внешнее поведение то же. Исключение — когда контрактом явно является взаимодействие (например, "после оплаты обязательно вызывается sendReceipt"); тогда уместны моки — см. объекты в тестировании.
Эффективное юнит-тестирование снижает стоимость сопровождения кодовой базы. Когда модуль покрыт тестами, разработчик получает уверенность в том, что изменения, вносимые в код, не нарушают уже существующей функциональности. Это особенно важно при рефакторинге: без тестов каждое изменение сопряжено с необходимостью ручной проверки всех сценариев, включая граничные и исключительные. С тестами — достаточно запустить набор и убедиться в отсутствии регрессий.
Основные принципы юнит-тестирования
Главные принципы юнит-тестирования описываются международным акронимом F.I.R.S.T.
- Fast (Быстрые). Тесты должны выполняться мгновенно — за миллисекунды. Если тысяча юнит-тестов идет дольше пары секунд, разработчики перестанут запускать их перед каждым сохранением кода. Скорость достигается за счет отказа от работы с реальными базами данных, файлами или сетью.
- Independent (Изолированные / Независимые). Тесты не должны зависеть друг от друга. Порядок их запуска не должен иметь значения.
- Repeatable (Повторяемые). Тест должен выдавать один и тот же результат при каждом запуске в любых условиях. Он должен быть одинаково зеленым на компьютере разработчика, на сервере автоматической сборки (CI/CD) или на ноутбуке коллеги без доступа к интернету. В тестах нельзя завязываться на текущее системное время или случайные числа без контроля над ними.
- Self-Validating (Самопроверяемые). Тест сам знает, пройден он или нет. Результатом теста должен быть простой статус: Passed (зеленый) или Failed (красный). Человеку не нужно открывать логи, базы данных или файлы, чтобы глазами сверять, правильный ли ответ выдала функция. Для этого внутри теста пишется
assert. - Thorough / Timely (Тщательные / Своевременные).
- Thorough (Тщательные): Тесты должны проверять не только идеальный сценарий работы («счастливый путь»). Нужно проверять крайние значения, пустые строки, некорректные типы данных и обработку ошибок.
- Timely (Своевременные): Тесты должны писаться вовремя. Идеально — одновременно с написанием самого кода программы (или даже до него, как в методологии TDD). Писать тесты спустя месяц после релиза проекта — неэффективно.
Изоляция
В юнит-тестировании изоляция означает, что проверяемая функция запускается в абсолютно стерильной среде. Она полностью отрезана от внешнего мира и остальной системы. Если тест зависит от интернета, базы данных или системного времени, то это не юнит-тест. Это уже интеграционный тест.
От чего изолируют юнит-тест?
- От внешних зависимостей (Инфраструктуры): базы данных, файлы на диске, сторонние API, сетевые запросы.
- От других тестов: выполнение одного теста никак не должно влиять на результаты другого.
- От глобального состояния: настроек системы, переменных окружения и текущего времени (например, тест не должен падать только потому, что сегодня выходной).
Каждый юнит-тест проверяет одну логическую единицу в контролируемом окружении. Зависимости (БД, HTTP, почта, время) подменяют тестовыми двойниками — не всегда нужен полный мок всего подряд, но внешний мир не должен решать, зелёный тест или красный.
Для изоляции реальные тяжелые объекты заменяют «двойниками» (Test Doubles). В Python для этого используют встроенный модуль unittest.mock или плагин pytest-mock.
| Тип | Зачем | Пример |
|---|---|---|
| Stub (заглушка) | Возвращает заранее заданные данные | "репозиторий" всегда отдаёт список из двух заказов |
| Mock (мок) | Проверяет, как вызывали зависимость | "уведомление отправлено ровно один раз" |
| Fake (подделка) | Упрощённая рабочая реализация | in-memory БД вместо PostgreSQL |
| Spy | Записывает вызовы реального объекта | обёртка над сервисом с логом вызовов |
В Python для stub/mock часто используют unittest.mock или pytest-mock; в Java — Mockito. Пример: метод считает скидку и читает тариф из PricingService. В unit-тесте подставляют stub, который возвращает фиксированный тариф — и проверяют только формулу скидки.
Изоляция гарантирует: упал тест → смотрим логику юнита, а не "упала тестовая БД в Docker".
Детерминированность
Тест должен выдавать один и тот же результат при каждом запуске при неизменном коде и начальных условиях. Недопустимо, чтобы тест проходил "иногда" из-за случайных факторов — времени суток, порядка итерации по хеш-таблице, состояния глобальной переменной, случайного числа и так далее. Недетерминированные тесты быстро теряют доверие — их приходится перезапускать вручную, а в CI они становятся источником ложных срабатываний.
Детерминированность в тестировании — это свойство теста всегда возвращать один и тот же результат (строго зеленый или строго красный) при неизменном исходном коде программы. Если вы запустите детерминированный тест 100 раз подряд на своем компьютере, на сервере или на ноутбуке коллеги, он все 100 раз выдаст одинаковый результат. Противоположность детерминированности — «мигающие» тесты (Flaky Tests). Это тесты, которые без видимых причин то проходят, то падают. Они подрывают доверие к автоматизации: разработчики начинают игнорировать ошибки, думая, что «это просто опять тест мигает».
Причиной недетерминированного поведения теста всегда является скрытая зависимость от внешних факторов.
- Текущее время и дата. Тест проверяет скидку по пятницам. В пятницу он зеленый, а в субботу падает.
- Случайные числа. Код генерирует случайный ID, и тест падает только тогда, когда выпадает определенное число.
- Порядок выполнения тестов. Тест Б проходит, только если перед ним запустился Тест А, который записал данные в общую переменную или базу. Если запустить только Тест Б — он упадет.
- Внешние сервисы и сеть. Тест ходит по сети на внешний сайт. Сайт завис, упал или долго отвечает — тест окрасился в красный.
- Асинхронность и многопоточность. ва потока одновременно пытаются изменить один и тот же объект в памяти. Кто первый успел — тот и победил (состояние гонки / Race Condition). Результат теста зависит от миллисекундной задержки процессора.
Быстрота выполнения
Быстрота выполнения — это критически важное свойство юнит-тестов. Один модульный тест должен выполняться в течение нескольких миллисекунд.Если запуск всей тестовой базы (сотен или тысяч тестов) занимает больше нескольких секунд, разработчики перестают запускать её перед каждым сохранением кода. Это приводит к тому, что дефекты обнаруживаются слишком поздно, а процесс разработки сильно замедляется.
Высокая скорость юнит-тестов — это не магия, а строгое соблюдение правил архитектуры:
- Работа только в оперативной памяти. Тесты взаимодействуют исключительно с объектами в памяти процессора.
- Никаких дисковых операций (I/O). Тесты не создают, не читают и не удаляют реальные файлы на жестком диске.
- Никаких сетевых запросов. Любые обращения к внешним API, сайтам или микросервисам полностью исключены.
- Никаких реальных баз данных. Вместо тяжелых СУБД (PostgreSQL, MySQL) используются легковесные базы в памяти (например, SQLite :memory:) или моки.
Юнит-тесты должны выполняться за минимальное время — в идеале, суммарное время всех юнит-тестов в проекте не должно превышать нескольких секунд. Если тест требует подключения к сети, создания файла, запуска контейнера или ожидания таймера — это уже не юнит-тест. Такие проверки относятся к интеграционному уровню. Быстрота позволяет запускать тесты постоянно — после каждого сохранения файла, перед коммитом, в рамках pre-push хука. Это создаёт "немедленную обратную связь", критически важную для поддержания качества.
Если тестов стало действительно много, в pytest есть встроенные механизмы и плагины для оптимизации времени выполнения:
- Параллельный запуск;
- Запуск только упавших тестов;
- Поиск «тормозов».
Независимость тестов
Независимость тестов — это принцип, согласно которому каждый тест должен быть абсолютно автономен. Результат выполнения конкретного теста не должен зависеть от того, какие тесты запускались до него, запустятся ли после и выполняются ли они вообще. Если нарушить этот принцип, тесты станут «мигающими» (недетерминированными), а их поддержка превратится в кошмар.
Каждый тест должен быть независим от других: порядок их выполнения не должен влиять на результат. Запрещено, чтобы один тест изменял глобальное состояние (например, статическую переменную), от которого зависит другой. После завершения каждого теста окружение должно быть приведено в исходное состояние — явно (через методы tearDown / AfterEach) или неявно (благодаря изоляции и отсутствию побочных эффектов). Это требование особенно актуально при параллельном запуске тестов.
Главные правила независимости:
- Порядок запуска не имеет значения. Тесты должны успешно проходить как при запуске по алфавиту, так и в случайном порядке (например, с помощью плагина pytest-randomly).
- Одиночный запуск. Вы должны иметь возможность запустить один-единственный тест из середины файла, и он обязан стать зеленым.
- Никакого общего состояния. Тесты не должны передавать друг другу данные через глобальные переменные или внешние файлы.
Если запустить эти тесты в обратном порядке или параллельно на разных ядрах процессора, они упадут, хотя сам код программы может быть абсолютно исправен.
Для создания чистой, независимой среды перед каждым тестом используют фикстуры (@pytest.fixture). По умолчанию фикстуры имеют область видимости scope="function". Это значит, что для каждого теста создается свежая, изолированная копия данных.
import pytest
# ХОРОШИЙ ПРИМЕР: Фикстура создает чистый список для КАЖДОГО теста отдельно
@pytest.fixture
def clean_list():
return []
def test_add_item(clean_list):
clean_list.append("товар")
assert len(clean_list) == 1 # Всегда успешно
def test_check_empty(clean_list):
assert len(clean_list) == 0 # Всегда успешно, список гарантированно пустой
Зачем нужна независимость?
- Параллельный запуск. Независимые тесты можно легко запускать на 4, 8 или 16 ядрах процессора через
pytest-xdist. Если тесты зависят друг от друга, параллельный запуск разрушит их логику. - Простая локализация дефектов. Если падает зависимый тест, вам приходится расследовать цепочку из пяти предыдущих тестов, чтобы понять, кто именно испортил данные. Если падает независимый тест — сломался именно он.
- Безопасное удаление и добавление. Вы можете в любой момент удалить старый тест или написать десять новых, не боясь, что из-за этого «поломаются» соседние проверки.
Читаемость и сопровождаемость
Читаемость и сопровождаемость (Readability & Maintainability) — это важнейшие свойства тестов, определяющие, насколько легко другим разработчикам понимать их логику, локализовать дефекты и изменять тесты при обновлении требований программы.
Код тестов нужно писать так же качественно, как и основной код приложения. Тесты читают гораздо чаще, чем пишут, особенно когда они ломаются и становятся красными.
Главные правила читаемости и сопровождаемости:
- Понятные и говорящие имена. Имя тестовой функции должно четко описывать: что тестируется, при каких условиях и какой ожидается результат. Не бойтесь длинных названий.
- Соблюдение структуры AAA (Arrange, Act, Assert). Разделяйте блоки кода пустой строкой, чтобы с первого взгляда было видно, где подготавливаются данные, где вызывается функция, а где идет проверка.
- Избегайте логики внутри тестов. В тестах не должно быть циклов (
for,while) и ветвлений (if,else). Логика усложняет чтение и сама может содержать скрытые дефекты. Если вам нужно проверить много условий, используйте параметризацию. - Один тест — одна концепция. Тест должен проверять только одно логическое бизнес-правило. Не стоит пытаться в одном тесте проверить регистрацию, оформление заказа и оплату. Если такой тест упадет в середине, вы не узнаете, работают ли последующие шаги.
Pytest предоставляет встроенные механизмы, которые делают код тестов чистым и декларативным:
- Вместо явного вызова функций создания объектов, вы просто передаете имя фикстуры в аргументы теста. Это убирает визуальный шум.
- Позволяют дать текстовое описание каждому набору данных в параметризованном тесте. В отчете pytest вместо безликих аргументов вы увидите понятные сценарии.
Когда тест падает в системе автоматической сборки (CI/CD), разработчик по одному лишь названию теста должен сразу понять, какая бизнес-логика сломалась. Если требования к программе меняются (например, минимальный возраст регистрации изменился с 18 лет на 21 год), хорошо структурированные тесты можно обновить за пару минут в одном месте, не переписывая сотни строк кода.
Код теста — такой же продукт, как и основной код. Он должен быть написан с учётом будущего сопровождения: имена тестовых методов должны точно отражать проверяемое поведение, а структура — следовать стандартному шаблону Given-When-Then ("дано — когда — тогда") или Arrange-Act-Assert ("подготовка — действие — проверка"). Плохо написанный тест — хуже, чем его отсутствие: он создаёт ложное чувство защищённости и затрудняет диагностику при падении.
Границы применимости юнит-тестирования
Границы применимости юнит-тестирования — это четкие рамки, определяющие, для каких задач модульные тесты идеальны, где они бесполезны, а где их применение технически невозможно. Юнит-тесты не являются серебряной пулей. Они проверяют лишь правильность работы изолированных кирпичиков кода, но не гарантируют, что из этих кирпичиков построится работающее здание.
Юнит-тестирование не является универсальным решением. Оно эффективно для проверки:
- чистой логики — вычислений, преобразований, условных ветвлений, циклов;
- корректности обработки входных данных — валидных, граничных, ошибочных;
- соблюдения контрактов — предусловий, постусловий, инвариантов;
- поведения в исключительных ситуациях: выброса ожидаемых исключений.
Оно неэффективно или нецелесообразно для проверки:
- взаимодействия с внешними системами (БД, API, файловая система) — здесь уместны интеграционные тесты;
- пользовательского интерфейса — для этого применяются end-to-end-тесты;
- производительности, масштабируемости, отказоустойчивости — это предмет нагрузочного и стресс-тестирования;
- глобальной согласованности состояния системы — требует сквозных сценариев.
Важно понимать, что юнит-тесты не заменяют другие виды проверок. Они формируют первый и самый надёжный слой защиты, но без верхних слоёв (интеграционных, системных) нельзя говорить о полноценной проверке программного обеспечения.
Юнит-тест по определению изолирован. Он не может проверить, правильно ли ваше приложение отправляет SQL-запросы в реальную базу данных PostgreSQL, отвечает ли сторонний платежный шлюз (API) и правильно ли настроены права доступа к сетевым папкам. Здесь нужны интеграционные тесты.
Проверить юнит-тестом, ровно ли отображается кнопка на экране смартфона, не съехал ли шрифт в браузере или удобно ли пользователю кликать по меню, невозможно. Для этого применяются сквозные (E2E) тесты с инструментами вроде Selenium или Playwright, а также ручное тестирование.
Юнит-тесты выполняются в изолированной памяти. Они не обнаружат ошибку, если на боевом сервере установлена не та версия Python, не хватает переменной окружения .env, закрыт сетевой порт или закончилось место на жестком диске.
Если вы неправильно написали мок (заглушку) для базы данных, ваш юнит-тест будет всегда зеленым, утверждая, что код работает. Но при запуске на реальном сервере программа упадет с ошибкой базы данных.
Процесс написания юнит-теста
Типичный юнит-тест проходит несколько фаз, поддерживаемых фреймворком:
-
Подготовка (Arrange / Setup)
На этом этапе создаются необходимые для теста объекты — тестируемый экземпляр (Система under test), заглушки зависимостей, входные данные. Подготовка может выполняться один раз на весь набор тестов (@BeforeAll/[OneTimeSetUp]) или перед каждым тестом (@BeforeEach/[SetUp]). Рекомендуется минимизировать объём подготовки, чтобы избежать скрытых зависимостей между тестами. -
Действие (Act)
Выполняется вызов тестируемого метода или функции с заданными аргументами. Этот этап должен быть максимально лаконичным — обычно одна строка. Любые побочные эффекты, возникающие в результате вызова, должны быть зафиксированы для последующей проверки. -
Проверка (Assert)
Сравнивается фактический результат (возвращённое значение, изменённое состояние объекта, выброшенное исключение) с ожидаемым. Утверждения должны быть точными: "равно 42"; "строка равна "OK"". Использование неточных проверок снижает ценность теста. -
Завершение (Teardown / Cleanup)
Освобождаются ресурсы, восстанавливается окружение. В современных фреймворках эта фаза часто не требуется благодаря автоматическому управлению памятью и изоляции, но может быть полезна при работе с внешними ресурсами (например, временными файлами в интеграционных тестах).
Даже в простейшем случае все эти фазы присутствуют — явно или неявно. Пропуск одной из них (например, многократное использование одного и того же мока без сброса его состояния) приводит к хрупким, ненадёжным тестам.
Связь с практиками разработки
Юнит-тестирование органично вписывается в современные методологии:
-
Test-Driven Разработка (TDD) предполагает написание теста до реализации логики. Это заставляет разработчика сначала чётко сформулировать ожидаемое поведение, а затем "довести" код до прохождения теста. Цикл "красный → зелёный → рефакторинг" создаёт естественный темп работы и минимизирует избыточность кода.
-
Refactoring (рефакторинг) становится безопасным только при наличии хорошего покрытия юнит-тестами. Без них изменение структуры кода без изменения поведения — рискованная операция.
-
Continuous Integration (CI) полагается на быстрые и надёжные юнит-тесты как на "стражей ворот" — если они не проходят, сборка отклоняется, и проблема обнаруживается на ранней стадии.
Фреймворки
JUnit 5 (Java)
JUnit 5 — это самый популярный современный фреймворк для модульного (юнит) тестирования Java-приложений. Он адаптирован под особенности Java 8 и более поздних версий, поддерживает лямбда-выражения и предоставляет гибкую архитектуру для написания и запуска тестов.
JUnit остаётся стандартом де-факто в экосистеме Java благодаря глубокой интеграции в инструментальную цепочку и устойчивому сообществу. JUnit 5 (Jupiter) представляет собой перепроектирование с нуля. Архитектура разделена на три независимых модуля:
- JUnit Platform — основа выполнения тестов, независимая от конкретного стиля тестирования. Позволяет запускать не только Jupiter-тесты, но и, например, Spock-спецификации через адаптеры.
- JUnit Jupiter — собственно API и движок для написания и выполнения тестов. Поддерживает параметризованные тесты (
@ParameterizedTest), условное выполнение (@EnabledOnOs,@DisabledIf), вложенные классы (@Nested) для логической группировки, а также кастомные расширения черезExtensionAPI. - JUnit Vintage — совместимость с JUnit 3 и 4, необходимая для постепенной миграции.
Ключевое отличие от JUnit 4 — отказ от статических методов жизненного цикла в пользу инстансных (но с сохранением @BeforeAll как static). Это позволяет инъектировать зависимости в методы, а не только в поля. JUnit 5 также вводит понятие dynamic tests — тестов, генерируемых во время выполнения, что полезно при тестировании наборов данных, читаемых из внешнего источника.
Важно: JUnit 5 не предоставляет встроенного механизма мокинга. Для этого используются сторонние библиотеки, наиболее распространённая — Mockito, которая тесно интегрируется с Jupiter через @ExtendWith(MockitoExtension.class).
Тестовые классы и методы в JUnit 5 больше не обязаны быть публичными (public), их можно делать пакетно-приватными.
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
@DisplayName("Тестирование математических операций") // Красивое имя для отчетов
class MathTest {
@BeforeAll
static void initAll() {
// Выполняется ОДИН раз перед всеми тестами класса (метод должен быть static)
}
@BeforeEach
void init() {
// Выполняется перед КАЖДЫМ тестом (подготовка данных)
}
@Test
@DisplayName("Проверка сложения двух чисел")
void additionTest() {
int result = 2 + 2;
assertEquals(4, result, "2 + 2 должно быть равно 4");
}
@Test
@Disabled("Тест временно отключен")
void skippedTest() {
// Этот тест не запустится
}
@AfterEach
void tearDown() {
// Выполняется после КАЖДОГО теста (очистка ресурсов)
}
@AfterAll
static void tearDownAll() {
// Выполняется ОДИН раз после всех тестов класса (метод должен быть static)
}
}
NUnit и xUnit.net (.NET)
Эти два фреймворка — результат рефлексии над недостатками предыдущих поколений. NUnit (3.x) сохраняет традиционную модель с атрибутами [SetUp], [TearDown], [OneTimeSetUp], [OneTimeTearDown], [TestCase] для параметризации. Он гибок, но допускает потенциальные анти-паттерны — например, использование [SetUp] для инициализации моков, что может приводить к их неявному совместному использованию между тестами.
xUnit.net — реакция на эти проблемы. Его авторы (включая создателей NUnit) сформулировали чёткий принцип: каждый тест — независимый экземпляр класса. Конструктор используется для подготовки (Arrange), а освобождение ресурсов — через IDisposable. Атрибуты [SetUp] и [TearDown] отсутствуют — их наличие в NUnit, по мнению разработчиков xUnit, поощряет написание тестов с побочными эффектами и скрытыми зависимостями.
xUnit вводит понятие theory — теста, который должен проходить для любого входного набора, соответствующего заданным условиям ([Theory], [InlineData], [MemberData]). Это ближе к математическому определению инварианта, чем к проверке конкретных примеров.
Для мокинга в .NET-экосистеме доминирует Moq — библиотека, построенная на выражениях (Expression<T>), что даёт преимущество в типобезопасности и отладке по сравнению с рефлексией. NSubstitute предлагает более лаконичный синтаксис, но менее строгий контроль.
NUnit и xUnit.net — два самых популярных фреймворка для модульного тестирования в экосистеме .NET. Оба являются наследниками классического JUnit, но развивались по-разному. NUnit предлагает богатый встроенный синтаксис утверждений (Assertions), в то время как xUnit.net ориентирован на максимальную лаконичность, изоляцию тестов и современный дизайн API.
Вариант на NUnit:
using NUnit.Framework;
namespace DotNetTests.NUnit;
[TestFixture]
public class CalculatorNUnitTests
{
private Calculator _calc;
[SetUp]
public void Init()
{
// Выполняется ПЕРЕД каждым тестом
_calc = new Calculator();
}
[Test]
public void Add_TwoNumbers_ReturnsSum()
{
var result = _calc.Add(2, 3);
// Два стиля написания проверок на выбор:
Assert.AreEqual(5, result); // Классический
Assert.That(result, Is.EqualTo(5)); // Constraint-based (Fluent)
}
[TestCase(2, 4, true)]
[TestCase(3, 5, false)]
public void IsEven_Values_ReturnExpectedResult(int number, int expected)
{
Assert.That(_calc.IsEven(number), Is.EqualTo(expected));
}
[TearDown]
public void Clean()
{
// Выполняется ПОСЛЕ каждого теста
}
}
Вариант на xUnit.net:
using Xunit;
namespace DotNetTests.XUnit;
public class CalculatorXUnitTests : IDisposable
{
private readonly Calculator _calc;
public CalculatorXUnitTests()
{
// Выполняется ПЕРЕД каждым тестом (вместо [SetUp])
_calc = new Calculator();
}
[Fact]
public void Add_TwoNumbers_ReturnsSum()
{
var result = _calc.Add(2, 3);
Assert.Equal(5, result);
}
[Theory]
[InlineData(2, 4, true)]
[InlineData(3, 5, false)]
public void IsEven_Values_ReturnExpectedResult(int number, int expected)
{
Assert.Equal(expected, _calc.IsEven(number));
}
public void Dispose()
{
// Выполняется ПОСЛЕ каждого теста (вместо [TearDown])
}
}
Выбирайте xUnit.net, если вы создаете новый проект на .NET Core / .NET 5+. Он является стандартом де-факто в команде разработки самого .NET. Его подход со сбросом состояния класса (new на каждый тест) минимизирует побочные эффекты и скрытые зависимости между тестами.
Выбирайте NUnit, если вы мигрируете старый проект (например, с .NET Framework), привыкли к синтаксису Assert.That, или вам критически важна производительность создания тестовых классов (NUnit не пересоздает объект класса для каждого метода, экономя память).
PyTest (Python)
PyTest — независимая реализация, ставшая фактическим стандартом благодаря своей выразительности и расширяемости. Главное отличие — отказ от наследования от TestCase. Тесты пишутся как обычные функции, что упрощает композицию и повторное использование. В отличие от встроенного модуля unittest, PyTest не требует оборачивать тесты в классы, минимизирует количество шаблонного кода (boilerplate) и использует стандартное ключевое слово assert вместо десятков специализированных методов.
Ключевые особенности:
- Простой синтаксис: Тестом является любая функция, имя которой начинается с test_.
- Магия assert: Не нужно запоминать assertEqual или assertTrue. PyTest сам перехватывает стандартный assert и выдает подробный отчет о причинах падения.
- Фикстуры (Fixtures): Мощная система управления зависимостями и состоянием (заменяет классические Setup/Teardown).
- Параметризация: Встроенная поддержка запуска одного теста с разными входными данными.
Центральный механизм — фикстуры (@pytest.fixture). Это функции, возвращающие объекты, жизненный цикл которых управляется фреймворком (создание, кэширование, очистка). Фикстуры могут иметь разную область действия (scope="function", scope="class", scope="module", scope="session"), что позволяет избежать избыточной инициализации. В отличие от setUp() в unittest, фикстуры объявляются декларативно как параметры тестовой функции — это делает зависимости явными.
PyTest автоматически обнаруживает тесты по соглашению — функции, имена которых начинаются с test_, и файлы, имена которых начинаются или заканчиваются на test. Параметризация реализуется через @pytest.mark.parametrize, где каждый параметр задаётся как кортеж входных данных и ожидаемого результата — это стимулирует тестирование через таблицы эквивалентности.
Встроенная поддержка assert rewriting позволяет использовать стандартный оператор assert вместо специфичных методов (self.assertEqual). При падении теста PyTest показывает детальное сравнение структур, включая diff для строк и деревьев.
Вам не нужно импортировать сам pytest для написания простых проверок. Достаточно использовать стандартные операторы сравнения Python.
# test_math.py
def add(x, y):
return x + y
def test_add_success():
assert add(2, 3) == 5 # Простое и понятное утверждение
def test_string_contains():
text = "pytest ис awesome"
assert "awesome" in text
При падении теста PyTest покажет точные значения переменных (например: AssertionError: assert 6 == 5), что сильно облегчает отладку.
Фикстуры — это функции, которые подготавливают данные, настраивают базу данных или создают объекты перед тестом, а также могут очищать ресурсы после него. Они передаются в тестовые функции как аргументы.
import pytest
@pytest.fixture
def sample_user():
"""Фикстура, создающая тестовые данные."""
user = {"name": "Alex", "role": "admin"}
return user
@pytest.fixture
def database_connection():
"""Фикстура с этапом очистки (Teardown) ресурсов."""
print("\nПодключение к БД создано") # Setup
db = {"status": "connected"}
yield db # Здесь тест получает управление и выполняется
print("\nПодключение к БД закрыто") # Teardown (выполнится ВСЕГДА после теста)
def test_user_role(sample_user):
# Фикстура sample_user автоматически подставится сюда
assert sample_user["role"] == "admin"
def test_db_status(database_connection):
assert database_connection["status"] == "connected"
Jest (JavaScript/TypeScript)
Jest — это самый популярный и мощный фреймворк для тестирования в экосистеме JavaScript и TypeScript. Созданный компанией Meta (Facebook), он работает по принципу «всё включено» (zero-configuration): из коробки доступны средство запуска, библиотека утверждений (assertions), моки (mocking) и отчеты о покрытии кода (code coverage).
Ключевые фичи Jest:
- Скорость и изоляция: Тесты выполняются параллельно в собственных песочницах (worker processes).
- Снимки (Snapshot Testing): Позволяет сохранять слепки структуры данных или UI-компонентов и сравнивать их при следующих запусках.
- Отличный Mocking: Мощные встроенные инструменты для подмены функций, модулей, таймеров и API-запросов.
Jest создан с учётом специфики фронтенд-разработки — динамической природы модулей, асинхронности и необходимости изоляции глобального состояния (например, DOM). Его ключевая особенность — sandboxing: каждый тестовый файл выполняется в изолированном окружении, что исключает утечки состояния между файлами.
Snapshot testing фиксирует сериализованное представление вывода (например, React-компонента) и при последующих запусках сравнивает его с сохранённой "снимковой" версией. Это эффективно для контроля неожиданных изменений в UI-логике, но требует ручного аудита при обновлении снимков.
Jest предоставляет глобальные моки через jest.mock(), которые заменяют импортируемые модули до загрузки тестируемого кода:
jest.mock('./apiClient');
const { fetchUser } = require('./userService');
Это позволяет изолировать даже глубоко вложенные зависимости без досрочного внедрения через конструктор.
Для асинхронных тестов Jest поддерживает async/await, возврат Promise, а также колбэки (done). Он автоматически ждёт завершения всех микрозадач и макрозадач перед завершением теста — что критично для корректной проверки таймеров и сетевых вызовов.
Тесты организуются с помощью функций describe (группировка) и test или it (конкретный тест). Проверки выполняются через конструкцию expect(...).to....
// math.js или math.ts
export const sum = (a, b) => a + b;
// math.test.js
import { sum } from './math';
describe('Модуль Math', () => {
test('правильно складывает два числа', () => {
expect(sum(2, 2)).toBe(4); // toBe использует Object.is (для примитивов)
});
test('работа со сложными объектами', () => {
const data = { one: 1 };
data['two'] = 2;
// expect(data).toBe({ one: 1, two: 2 }); // Ошибка! Ссылки разные.
expect(data).toEqual({ one: 1, two: 2 }); // Отлично! Проверяет глубокое равенство.
});
});
Mocha + Chai (JavaScript/Node.js)
Mocha + Chai — это классическая и максимально гибкая связка для тестирования в экосистеме JavaScript и Node.js. В отличие от Jest, который предоставляет всё «из коробки», здесь обязанности разделены: Mocha отвечает за запуск тестов и организацию их структуры, а Chai — за проверку утверждений (assertions).
Архитектура связки:
- Mocha — это тестовый запуск (Test Runner). Он дает структуру describe, it и хуки жизненного цикла.
- Chai — это библиотека утверждений (Assertion Library). Она подключается к Mocha и предоставляет три разных стиля написания проверок.
Mocha — это фреймворк для запуска, а не полный стек. Он предоставляет:
- гибкую систему хуков (
before,beforeEach,after,afterEach); - поддержку асинхронных тестов (возврат
Promise, колбэкdone,async/await); - генерацию отчётов в различных форматах (spec, dot, json, junit);
- расширяемость через reporters и interfaces (BDD, TDD, QUnit-стиль).
Но он не содержит встроенных утверждений. Для этого используется Chai — библиотека, предлагающая три стиля:
assert— процедурный, похож на JUnit;should— цепочка через прототипObject.prototype;expect— цепочка через expect(value).to..., наиболее популярный:
expect(result).to.equal(42);
expect(user.name).to.include('Alice');
Mocha особенно удобен при тестировании систем, где важна настройка окружения (например, инициализация базы данных перед всем набором тестов). Однако отсутствие встроенных моков и утверждений требует ручной сборки стека (часто с Sinon.js для мокинга).
Chai уникален тем, что позволяет писать тесты как в классическом стиле, так и на «человеческом» английском языке (BDD-стиль).
const { assert, expect, should } = require('chai');
should(); // Инициализация стиля should (модифицирует прототип объектов)
const value = 'hello';
// 1. Стиль Assert (классический, похож на Node.js Assert)
assert.typeOf(value, 'string');
assert.equal(value, 'hello');
// 2. Стиль Expect (BDD-стиль, самый популярный и рекомендуемый)
expect(value).to.be.a('string');
expect(value).to.equal('hello');
// 3. Стиль Should (BDD-стиль, использует геттеры)
value.should.be.a('string');
value.should.equal('hello');
RSpec (Ruby)
RSpec — это главный фреймворк для тестирования в экосистеме Ruby (особенно популярен в Ruby on Rails). Он построен на принципах BDD (Behavior-Driven Development — разработка через поведение) и предоставляет выразительный, человекочитаемый предметно-ориентированный язык (DSL) для описания ожидаемого поведения приложения.
Вместо терминов «тест» и «проверка» RSpec использует понятия «пример» (example) и «ожидание» (expectation). Тесты пишутся в файлах с расширением _spec.rb.
# calculator.rb
class Calculator
def add(a, b)
a + b
end
end
# spec/calculator_spec.rb
require_relative '../calculator'
RSpec.describe Calculator do
# describe группирует тесты вокруг конкретного класса или метода
describe '#add' do
# it описывает конкретное ожидаемое поведение (пример)
it 'returns the sum of two numbers' do
calculator = Calculator.new
# Синтаксис ожиданий: expect(...).to ...
expect(calculator.add(2, 3)).to eq(5)
end
end
end
RSpec — результат применения идей Behavior-Driven Разработка к юнит-тестированию. Хотя BDD формально относится к уровню приёмочных тестов, RSpec показал, что язык спецификаций может быть полезен и на уровне кода.
Структура теста в RSpec — иерархия describe (контекст) → context (подконтекст) → it (пример поведения). Это позволяет выразить "при пустом входном списке метод возвращает 0, а при наличии элементов — их сумму". Такой подход превращает тесты в исполняемую документацию.
Встроенный DSL для мокинга (allow(obj).to receive(:method).and_return(value)) интегрирован в синтаксис и не требует внешних библиотек. RSpec также поддерживает shared Примеры — переиспользуемые наборы тестов для проверки одинакового поведения у разных объектов (например, всех реализаций интерфейса).
PHPUnit (PHP)
PHPUnit — это стандарт де-факто для модульного (юнит) тестирования в экосистеме PHP. Он является основой для тестирования во всех современных фреймворках, таких как Laravel и Symfony. PHPUnit ориентирован на объектно-синтаксическую модель тестирования, где каждый тестовый класс является наследником базового класса фреймворка.
Ключевые особенности:
- Классическая структура: Тесты группируются в классы, методы которых должны начинаться со слова
test. - Строгая типизация: Отлично поддерживает современные возможности PHP 8.x (атрибуты, типизированные свойства).
- Встроенный Mocking: Имеет собственную систему создания заглушек (Mocks и Stubs) без необходимости ставить сторонние библиотеки.
PHPUnit — эталонный фреймворк, входящий в рекомендации PHP-FIG и поддерживающий PSR-стандарты. Он следует традиционной модели TestCase с методами setUp() и tearDown(). Для мокинга используется встроенный createMock() или сторонние библиотеки (Prophecy, Mockery).
Особенность PHPUnit — поддержка Данные providers — метод, возвращающий массив наборов данных, который связывается с тестом через аннотацию @dataProvider. Это позволяет отделить логику теста от данных.
PHPUnit интегрирован в основные фреймворки: в Laravel используется TestCase, наследующий от PHPUnit; в Symfony — дополнительные утверждения для HTTP-клиента и контейнера.
Все тестовые классы должны наследоваться от PHPUnit\Framework\TestCase. Начиная с версии PHPUnit 10+, для конфигурации и управления жизненным циклом активно используются современные атрибуты PHP, сменившие старые аннотации в комментариях PHPDoc.
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\Attributes\After;
class CalculatorTest extends TestCase
{
private Calculator $calc;
public static function setUpBeforeClass(): void
{
// Выполняется ОДИН раз перед всеми тестами класса (статический метод)
}
#[Before]
protected function init(): void
{
// Выполняется перед КАЖДЫМ тестом (аналог старого setUp)
$this->calc = new Calculator();
}
public function testAddition(): void
{
$result = $this->calc->add(2, 3);
// Основной синтаксис проверок через $this->assert...
$this->assertEquals(5, $result, "Сумма 2 и 3 должна быть равна 5");
}
#[After]
protected function clean(): void
{
// Выполняется после КАЖДОГО теста (аналог старого tearDown)
}
public static function tearDownAfterClass(): void
{
// Выполняется ОДИН раз после всех тестов класса (статический метод)
}
}
Юнит-тестирование валидатора электронной почты
Общая спецификация поведения
Допустим, в системе требуется базовая, но надёжная валидация email-адресов по следующим правилам:
- Адрес не может быть
nullили пустой строкой — ошибка: "Email не может быть пустым". - Адрес должен содержать ровно один символ
@. - Часть до
@(локальная) не должна быть пустой. - Часть после
@(доменная) должна содержать хотя бы одну точку и не начинаться/заканчиваться ею.
Это упрощённая, но практически значимая модель (без учёта IDN, кириллических доменов, RFC 5322 в полном объёме — что оправдано для вводного примера).
C# (xUnit.net + FluentAssertions)
Тестируемый код
Код ITЗагрузка примера кода…
Тесты
Код ITЗагрузка примера кода…
Почему так
- Использован
recordдляValidationResult— неизменяемый DTO, идеален для возврата из чистых функций. - Параметризация группируется по семантическим категориям ошибок, а не просто по набору строк — это улучшает читаемость и упрощает сопровождение.
FluentAssertionsдаёт цепочку.Should().Be..., что ближе к естественному языку и снижает вероятность опечаток в сравнениях.- Отдельный
[Fact]для случая@example.com, потому что он одновременно нарушает два правила (пустая локальная часть + недопустимый домен), но должна сработать первая проверка — это проверка порядка условий.
Python (PyTest + встроенные утверждения)
Тестируемый модуль (email_validator.py)
Код ITЗагрузка примера кода…
Тесты (test_email_validator.py)
Код ITЗагрузка примера кода…
Пояснения
- Использован
@dataclass(frozen=True)— неизменяемый результат, позволяющий сравнивать объекты по значению (через==). - Проверка
assert result == ValidationResult(...)— прямое сравнение ожидаемого и фактического объекта. Это возможно благодаряfrozen=Trueи автоматически сгенерированному__eq__. - Нет необходимости в сторонних assertion-библиотеках — стандартный
assertв PyTest достаточно выразителен для таких объектов. - Параметризация организована так же, как в C# — по семантическим группам.
Java (JUnit 5 + AssertJ)
Тестируемый класс
Код ITЗагрузка примера кода…
Тесты
Код ITЗагрузка примера кода…
Пояснения
ValidationResultреализован какstatic final classс фабричными методами и неизменяемыми полями — это стандартный паттерн для value-объектов в Java.- Переопределены
equalsиhashCode, чтобы поддерживать сравнение по значению (иначеassertThat(...).isEqualTo(...)не сработает). - Для
nullвыделен отдельный тест —@ValueSourceне поддерживаетnull, и это правильное решение: проверкаnull— отдельная семантическая категория. - Использован
split("@", -1), чтобы корректно обрабатывать@domain.com(иначеsplitбез limit вернул бы массив длины 1).
Навигация по разделу "Тестирование"
- Маршрут: О разделе · Резюме раздела · Карта уровней и практик (Unit / Integration / UI / E2E, TDD, BDD)
- Теория и процесс: Основы · Классификация · Жизненный цикл · Порядок этапов · Артефакты качества
- Уровни проверок: Unit · Integration · E2E, системное и UI · API · Тестовые дублёры · Покрытие кода · White-box · Мутационное тестирование
- Практика QA: Документация · Тест-дизайн · Ручное веб · SQL
- Автоматизация: Стратегия и пирамида · Каталог инструментов · Selenium · Playwright
- Практикум и углубление: Подготовка среды и создание первого теста · Проверка взаимодействия компонентов · Проверка пользовательского сценария · Проверка надежности под нагрузкой · Мобильное · Нагрузка · Безопасность · Самопроверка · Доп. материалы курса · Инструменты с низким кодом для тестирования · Тестирование нейроморфных систем