Перейти к основному содержимому

Юнит-тестирование

Тестировщику Разработчику Аналитику
Теория данных (раздел 3)

Зачем эта статья. Unit — самый дешёвый уровень обратной связи: секунды, без браузера. Здесь — структура AAA, примеры на JS и Python, принцип изоляции и связь с тестовыми дублёрами. Практика с pytest — в Подготовка среды и создание первого теста. Что такое функция, метод, класс как объект теста — Код — о разделе; изоляция зависимостей — Зависимости — о разделе.

Play ITЗагрузка интерактивного демо…


Юнит-тестирование

Что такое юнит-тест и как он работает

Юнит-тест - это программа, которая проверяет другую программу (маленький кусочек).

Берём конкретную функцию, даём ей входные данные и смотрим, что вернулось. Если вернулось то, что ожидали - тест зелёный, если нет - красный.

Любой юнит-тест логически делится на три блока — паттерн AAA (Arrange — Act — Assert, «подготовка — действие — проверка»). Это не требование фреймворка, а соглашение о читаемости: при падении теста сразу видно, на каком этапе искать ошибку.

БлокПо-русскиЧто происходит
ArrangeПодготовкаНастройка окружения, создание объектов, подготовка входных данных
ActДействиеВызов тестируемого метода, выполнение целевой операции, сохранение результата
AssertПроверкаСравнение результата с ожидаемым, проверка исключений, подтверждение корректности

Arrange (подготовка)

На этом этапе тест не вызывает проверяемую логику — он готовит сцену:

  • создаёт экземпляр класса или импортирует функцию (система под тестом, SUT);
  • задаёт входные данные: аргументы, тестовые записи, JSON-файл;
  • подменяет внешние зависимости заглушками и моками (тестовые дублёры);
  • при необходимости сбрасывает состояние — очистка коллекции, фиксация «текущего времени» через clock-fake.

Чем меньше кода в Arrange, тем проще читать тест. Общую подготовку для группы кейсов выносят в beforeEach / setUp, но каждый тест должен оставаться понятным без прыжков по файлу.

Act (действие)

Одна целевая операция — то, что именно вы проверяете:

  • вызов метода calculateDiscount(cart, user);
  • отправка HTTP-запроса в тестируемый handler (без реальной сети, если это unit);
  • повторный вызов после изменения состояния.

Результат сохраняют в переменную (result, response, exception). В Act не смешивают проверки — иначе при падении непонятно: сломалась логика или неверное ожидание в assert.

Assert (проверка)

Здесь тест отвечает на вопрос «получили ли мы то, что ожидали?»:

  • сравнение возвращаемого значения с эталоном (expect(result).toBe(5), assert result == 5);
  • проверка изменённого состояния объекта (user.balance уменьшился на 100);
  • ожидание исключения (pytest.raises, assertThrows);
  • для моков — что зависимость вызвана нужное число раз с нужными аргументами.

Один тест — один смысловой вопрос. Несколько assert допустимы, если они проверяют одно поведение (например, и код ответа, и тело JSON при создании заказа).

Как оформлять в коде

Блоки визуально разделяют пустой строкой и коротким комментарием // 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)

Техника анализа граничных значений в коде удобно выражается через параметризацию: один тест, несколько пар "вход → ожидание".

Код ITЗагрузка примера кода…

Так проще читать отчёт — видно, какая граница сломалась, а не "упал тест с именем test_everything".


Если тест ходит в базу данных, вызывает реальный API, создаёт файлы на диске, проверяет две несвязанные функции в одном кейсе или зависит от sleep(5) — это уже не юнит-тест, а интеграционный или E2E. Граница не всегда чёткая, но ориентир простой: изолирован ли кусок логики и быстрый ли прогон?


Типичные ошибки в первых unit-тестах

У начинающих разработчиков и QA повторяется один и тот же набор проблем:

  • В одном тесте проверяется сразу несколько правил. При падении сложно понять причину.
  • Проверяются внутренние детали реализации вместо внешнего поведения функции.
  • Используются "магические" значения без объяснения, из-за чего падает читаемость.
  • Нет граничных значений, хотя именно они чаще всего ломаются в продакшене.
  • Случайные данные (random) применяются без фиксированного seed, и тест становится нестабильным.

Рабочий ориентир: каждый unit-тест отвечает на один вопрос формата "при условии X функция возвращает Y". Для системных стыков лучше перейти к интеграционному тестированию, а для выбора входных наборов открыть тест-дизайн.


Разбор кейса — "зелёные" тесты и реальный дефект

Ситуация:

  • В сервисе подписок была функция расчёта даты окончания пробного периода.
  • Набор 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"); тогда уместны моки — см. объекты в тестировании.

Эффективное юнит-тестирование снижает стоимость сопровождения кодовой базы. Когда модуль покрыт тестами, разработчик получает уверенность в том, что изменения, вносимые в код, не нарушают уже существующей функциональности. Это особенно важно при рефакторинге: без тестов каждое изменение сопряжено с необходимостью ручной проверки всех сценариев, включая граничные и исключительные. С тестами — достаточно запустить набор и убедиться в отсутствии регрессий.


Основные принципы юнит-тестирования

Изоляция

Каждый юнит-тест проверяет одну логическую единицу в контролируемом окружении. Зависимости (БД, HTTP, почта, время) подменяют тестовыми двойниками — не всегда нужен полный мок всего подряд, но внешний мир не должен решать, зелёный тест или красный.

ТипЗачемПример
Stub (заглушка)Возвращает заранее заданные данные"репозиторий" всегда отдаёт список из двух заказов
Mock (мок)Проверяет, как вызывали зависимость"уведомление отправлено ровно один раз"
Fake (подделка)Упрощённая рабочая реализацияin-memory БД вместо PostgreSQL
SpyЗаписывает вызовы реального объектаобёртка над сервисом с логом вызовов

В Python для stub/mock часто используют unittest.mock или pytest-mock; в Java — Mockito. Пример: метод считает скидку и читает тариф из PricingService. В unit-тесте подставляют stub, который возвращает фиксированный тариф — и проверяют только формулу скидки.

Изоляция гарантирует: упал тест → смотрим логику юнита, а не "упала тестовая БД в Docker".


Детерминированность

Тест должен выдавать один и тот же результат при каждом запуске при неизменном коде и начальных условиях. Недопустимо, чтобы тест проходил "иногда" из-за случайных факторов — времени суток, порядка итерации по хеш-таблице, состояния глобальной переменной, случайного числа и так далее. Недетерминированные тесты быстро теряют доверие — их приходится перезапускать вручную, а в CI они становятся источником ложных срабатываний.


Быстрота выполнения

Юнит-тесты должны выполняться за минимальное время — в идеале, суммарное время всех юнит-тестов в проекте не должно превышать нескольких секунд. Если тест требует подключения к сети, создания файла, запуска контейнера или ожидания таймера — это уже не юнит-тест. Такие проверки относятся к интеграционному уровню. Быстрота позволяет запускать тесты постоянно — после каждого сохранения файла, перед коммитом, в рамках pre-push хука. Это создаёт "немедленную обратную связь", критически важную для поддержания качества.


Независимость тестов

Каждый тест должен быть независим от других: порядок их выполнения не должен влиять на результат. Запрещено, чтобы один тест изменял глобальное состояние (например, статическую переменную), от которого зависит другой. После завершения каждого теста окружение должно быть приведено в исходное состояние — явно (через методы tearDown / AfterEach) или неявно (благодаря изоляции и отсутствию побочных эффектов). Это требование особенно актуально при параллельном запуске тестов.


Читаемость и сопровождаемость

Код теста — такой же продукт, как и основной код. Он должен быть написан с учётом будущего сопровождения: имена тестовых методов должны точно отражать проверяемое поведение, а структура — следовать стандартному шаблону Given-When-Then ("дано — когда — тогда") или Arrange-Act-Assert ("подготовка — действие — проверка"). Плохо написанный тест — хуже, чем его отсутствие: он создаёт ложное чувство защищённости и затрудняет диагностику при падении.


Границы применимости юнит-тестирования

Юнит-тестирование не является универсальным решением. Оно эффективно для проверки:

  • чистой логики — вычислений, преобразований, условных ветвлений, циклов;
  • корректности обработки входных данных — валидных, граничных, ошибочных;
  • соблюдения контрактов — предусловий, постусловий, инвариантов;
  • поведения в исключительных ситуациях: выброса ожидаемых исключений.

Оно неэффективно или нецелесообразно для проверки:

  • взаимодействия с внешними системами (БД, API, файловая система) — здесь уместны интеграционные тесты;
  • пользовательского интерфейса — для этого применяются end-to-end-тесты;
  • производительности, масштабируемости, отказоустойчивости — это предмет нагрузочного и стресс-тестирования;
  • глобальной согласованности состояния системы — требует сквозных сценариев.

Важно понимать: юнит-тесты не заменяют другие виды проверок. Они формируют первый и самый надёжный слой защиты, но без верхних слоёв (интеграционных, системных) нельзя говорить о полноценной проверке программного обеспечения.


Процесс написания юнит-теста

Типичный юнит-тест проходит несколько фаз, поддерживаемых фреймворком:

  1. Подготовка (Arrange / Setup)
    На этом этапе создаются необходимые для теста объекты — тестируемый экземпляр (Система under test), заглушки зависимостей, входные данные. Подготовка может выполняться один раз на весь набор тестов (@BeforeAll / [OneTimeSetUp]) или перед каждым тестом (@BeforeEach / [SetUp]). Рекомендуется минимизировать объём подготовки, чтобы избежать скрытых зависимостей между тестами.

  2. Действие (Act)
    Выполняется вызов тестируемого метода или функции с заданными аргументами. Этот этап должен быть максимально лаконичным — обычно одна строка. Любые побочные эффекты, возникающие в результате вызова, должны быть зафиксированы для последующей проверки.

  3. Проверка (Assert)
    Сравнивается фактический результат (возвращённое значение, изменённое состояние объекта, выброшенное исключение) с ожидаемым. Утверждения должны быть точными: "равно 42"; "строка равна "OK"". Использование неточных проверок снижает ценность теста.

  4. Завершение (Teardown / Cleanup)
    Освобождаются ресурсы, восстанавливается окружение. В современных фреймворках эта фаза часто не требуется благодаря автоматическому управлению памятью и изоляции, но может быть полезна при работе с внешними ресурсами (например, временными файлами в интеграционных тестах).

Даже в простейшем случае все эти фазы присутствуют — явно или неявно. Пропуск одной из них (например, многократное использование одного и того же мока без сброса его состояния) приводит к хрупким, ненадёжным тестам.


Связь с практиками разработки

Юнит-тестирование органично вписывается в современные методологии:

  • Test-Driven Разработка (TDD) предполагает написание теста до реализации логики. Это заставляет разработчика сначала чётко сформулировать ожидаемое поведение, а затем "довести" код до прохождения теста. Цикл "красный → зелёный → рефакторинг" создаёт естественный темп работы и минимизирует избыточность кода.

  • Refactoring (рефакторинг) становится безопасным только при наличии хорошего покрытия юнит-тестами. Без них изменение структуры кода без изменения поведения — рискованная операция.

  • Continuous Integration (CI) полагается на быстрые и надёжные юнит-тесты как на "стражей ворот" — если они не проходят, сборка отклоняется, и проблема обнаруживается на ранней стадии.


Фреймворки

JUnit 5 (Java)

JUnit остаётся стандартом де-факто в экосистеме Java благодаря глубокой интеграции в инструментальную цепочку и устойчивому сообществу. JUnit 5 (Jupiter) представляет собой перепроектирование с нуля. Архитектура разделена на три независимых модуля:

  • JUnit Platform — основа выполнения тестов, независимая от конкретного стиля тестирования. Позволяет запускать не только Jupiter-тесты, но и, например, Spock-спецификации через адаптеры.
  • JUnit Jupiter — собственно API и движок для написания и выполнения тестов. Поддерживает параметризованные тесты (@ParameterizedTest), условное выполнение (@EnabledOnOs, @DisabledIf), вложенные классы (@Nested) для логической группировки, а также кастомные расширения через Extension API.
  • JUnit Vintage — совместимость с JUnit 3 и 4, необходимая для постепенной миграции.

Ключевое отличие от JUnit 4 — отказ от статических методов жизненного цикла в пользу инстансных (но с сохранением @BeforeAll как static). Это позволяет инъектировать зависимости в методы, а не только в поля. JUnit 5 также вводит понятие dynamic tests — тестов, генерируемых во время выполнения, что полезно при тестировании наборов данных, читаемых из внешнего источника.

Важно: JUnit 5 не предоставляет встроенного механизма мокинга. Для этого используются сторонние библиотеки, наиболее распространённая — Mockito, которая тесно интегрируется с Jupiter через @ExtendWith(MockitoExtension.class).


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 предлагает более лаконичный синтаксис, но менее строгий контроль.


PyTest (Python)

PyTest — независимая реализация, ставшая фактическим стандартом благодаря своей выразительности и расширяемости. Главное отличие — отказ от наследования от TestCase. Тесты пишутся как обычные функции, что упрощает композицию и повторное использование.

Центральный механизм — фикстуры (@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 для строк и деревьев.


Jest (JavaScript/TypeScript)

Jest создан с учётом специфики фронтенд-разработки — динамической природы модулей, асинхронности и необходимости изоляции глобального состояния (например, DOM). Его ключевая особенность — sandboxing: каждый тестовый файл выполняется в изолированном окружении, что исключает утечки состояния между файлами.

Snapshot testing фиксирует сериализованное представление вывода (например, React-компонента) и при последующих запусках сравнивает его с сохранённой "снимковой" версией. Это эффективно для контроля неожиданных изменений в UI-логике, но требует ручного аудита при обновлении снимков.

Jest предоставляет глобальные моки через jest.mock(), которые заменяют импортируемые модули до загрузки тестируемого кода:

jest.mock('./apiClient');
const { fetchUser } = require('./userService');

Это позволяет изолировать даже глубоко вложенные зависимости без досрочного внедрения через конструктор.

Для асинхронных тестов Jest поддерживает async/await, возврат Promise, а также колбэки (done). Он автоматически ждёт завершения всех микрозадач и макрозадач перед завершением теста — что критично для корректной проверки таймеров и сетевых вызовов.


Mocha + Chai (JavaScript/Node.js)

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 для мокинга).


RSpec (Ruby)

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-FIG и поддерживающий PSR-стандарты. Он следует традиционной модели TestCase с методами setUp() и tearDown(). Для мокинга используется встроенный createMock() или сторонние библиотеки (Prophecy, Mockery).

Особенность PHPUnit — поддержка Данные providers — метод, возвращающий массив наборов данных, который связывается с тестом через аннотацию @dataProvider. Это позволяет отделить логику теста от данных.

PHPUnit интегрирован в основные фреймворки: в Laravel используется TestCase, наследующий от PHPUnit; в Symfony — дополнительные утверждения для HTTP-клиента и контейнера.


Юнит-тестирование валидатора электронной почты

Общая спецификация поведения

Допустим, в системе требуется базовая, но надёжная валидация email-адресов по следующим правилам:

  1. Адрес не может быть null или пустой строкой — ошибка: "Email не может быть пустым".
  2. Адрес должен содержать ровно один символ @.
  3. Часть до @ (локальная) не должна быть пустой.
  4. Часть после @ (доменная) должна содержать хотя бы одну точку и не начинаться/заканчиваться ею.

Это упрощённая, но практически значимая модель (без учёта 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).

Навигация по разделу "Тестирование"