Created by AIImproved by people

Riqli · living documents · updated continuously

Where AI knowledge meets human practice.

Share with friends
image

Software Testing (JUnit, Mockito, Selenium, TestContainers). Hard skill. (Unit testing. JUnit. Mockito. Integration testing. Selenium. TestContainers. TDD. BDD. Cucumber. Self-study. Q&A. Tutorials. Documentation.)

sections

Введение

contents

Аннотация. В мире разработки программного обеспечения на Java качество кода и стабильность приложений имеют первостепенное значение. Курс «Тестирование ПО на Java: от JUnit до контейнеризации» — это исчерпывающее руководство, созданное для того, чтобы превратить вас из разработчика, который пишет код, в инженера, который создаёт надёжные и поддерживаемые системы. Он решает ключевую проблему современной разработки — снижение рисков внесения ошибок при изменении кода и обеспечение предсказуемости работы приложения в различных средах. В отличие от абстрактных теоретических лекций, этот курс фокусируется на конкретных инструментах и методологиях, таких как JUnit, Mockito, Selenium и TestContainers, обучая вас не только тому, «что» тестировать, но и «как» это делать эффективно. Вы освоите подходы, которые позволяют автоматизировать проверку каждой единицы кода, интегрировать тесты в процесс разработки и добиться максимального покрытия, что особенно актуально для команд, работающих по методологиям Agile и DevOps.

contents

Цель курса. После прохождения курса вы сможете самостоятельно проектировать и внедрять комплексную стратегию автоматизированного тестирования для Java-приложений любого уровня сложности, используя современные фреймворки и библиотеки для обеспечения качества, надёжности и безопасности кода.

contents

Результаты обучения.

  • Знать: архитектуру и возможности JUnit 5 (Jupiter, Platform, Vintage); принципы работы библиотек Mockito, AssertJ и Hamcrest; подходы к интеграционному тестированию с TestContainers и Selenium; основные паттерны тестирования, такие как Page Object Model; метрики качества кода и coverage; методологии TDD и BDD.
  • Уметь: писать эффективные модульные тесты для изолированного тестирования методов и классов; создавать моки и стабы для эмуляции зависимостей; разрабатывать интеграционные тесты с реальными базами данных и внешними API; автоматизировать UI-тесты для веб-приложений; интегрировать тесты в CI/CD пайплайны с использованием Maven, Gradle и Jenkins; применять практики TDD для проектирования надёжного кода.
  • Владеть: навыками отладки и рефакторинга тестов; методами параметризации тестов; техниками анализа покрытия кода; навыками работы с системой контроля версий Git в контексте совместной разработки тестов.
contents

Для кого этот курс.

Этот курс предназначен для Java-разработчиков всех уровней — от джуниоров, желающих заложить правильные основы профессиональной деятельности, до опытных мидл- и сеньор-специалистов, которые хотят систематизировать свои знания и ознакомиться с последними трендами в области тестирования. Он будет чрезвычайно полезен инженерам по автоматизации тестирования (QA Automation), стремящимся углубить свои технические навыки, а также архитекторам и техническим лидам, ответственным за качество и надёжность конечного продукта. Программа также принесёт пользу разработчикам Big Data и SRE-инженерам, которым важно уметь быстро диагностировать проблемы на любом уровне приложения.

Однако этот курс не подойдёт тем, кто делает первые шаги в программировании и не знаком с синтаксисом Java, принципами объектно-ориентированного программирования (ООП) и базовыми инструментами сборки, такими как Maven или Gradle. Предполагается, что слушатели уже обладают фундаментальными навыками разработки.

sections

Модуль 1. Основы тестирования и экосистема JUnit

contents

Пирамида тестирования и её значение. В основе любой стратегии тестирования лежит «пирамида тестирования» — концепция, определяющая оптимальное соотношение различных типов тестов в проекте. Основание пирамиды — это модульные (unit) тесты, которых должно быть большинство (около 70%). Они быстрые, стабильные и дешёвые в написании, так как проверяют отдельные изолированные компоненты — методы или классы . Средний уровень занимают интеграционные тесты (~20%), которые проверяют взаимодействие между компонентами системы, например, с базой данных или внешними API. На вершине пирамиды находятся сквозные (end-to-end) или функциональные тесты (~10%), имитирующие действия пользователя в реальном браузере, — они самые медленные и дорогие в поддержке. Следование этой пирамиде гарантирует, что у вас будет быстрый и надёжный набор тестов, который быстро выявляет проблемы на ранних стадиях. Чем выше по пирамиде, тем больше усилий требуется на поддержку, поэтому количество таких тестов должно быть минимальным.

contents

JUnit 5: Новая эра тестирования. JUnit 5 — это не просто обновление, а фундаментальная перестройка самого популярного фреймворка для тестирования в Java. Он состоит из трёх ключевых подпроектов, что делает его чрезвычайно гибким. JUnit Platform служит основой для запуска тестов на JVM, поддерживая не только JUnit, но и другие фреймворки, что позволяет использовать его как единую точку входа для всех видов тестов. JUnit Jupiter — это современный движок, представляющий новую модель программирования и расширений, где вы пишете тесты с использованием аннотаций, таких как @Test, @ParameterizedTest, и лямбда-выражений для ассертов. Наконец, JUnit Vintage обеспечивает обратную совместимость, позволяя запускать старые тесты, написанные для JUnit 3 и 4, без необходимости их полной переписки . Этот модульный подход облегчает миграцию и позволяет использовать все преимущества нового API.

contents

Жизненный цикл тестов: @BeforeEach, @AfterEach, @BeforeAll, @AfterAll. Для написания чистых и непротиворечивых тестов критически важно управлять состоянием окружения. JUnit 5 предоставляет мощный набор аннотаций для контроля жизненного цикла тестов. Метод, помеченный @BeforeEach, будет выполнен перед каждым тестовым методом, что идеально подходит для создания новых экземпляров тестируемых объектов или сброса состояния. Симметричная аннотация @AfterEach запускается после каждого теста и используется для очистки ресурсов, например, закрытия соединений с базой данных или очистки временных файлов . Для операций, которые должны быть выполнены один раз за весь класс, например, инициализация дорогостоящего соединения с внешним сервисом, используются @BeforeAll и @AfterAll. Важно помнить, что методы, помеченные @BeforeAll и @AfterAll, должны быть static .

contents

Утверждения (Assertions): От assertTrue до assertAll. Утверждения, или ассерты, — это сердце любого теста, именно они определяют, пройден тест или нет. JUnit 5 предлагает богатый набор статических методов в классе org.junit.jupiter.api.Assertions. Базовые ассерты, такие как assertEquals(), assertTrue(), assertFalse(), assertNotNull() и assertThrows() для проверки исключений, позволяют проверить ожидаемые значения и состояния . Однако настоящая мощь раскрывается с появлением группированных утверждений с помощью assertAll(), которое позволяет выполнить несколько проверок и получить отчёт о всех упавших ассертах одновременно, а не останавливаться на первом же сбое. Кроме того, поддержка лямбда-выражений в ассертах, например, для проверки сообщений исключений, делает код тестов более лаконичным и выразительным.

contents

Параметризованные тесты с @ParameterizedTest. Один из самых мощных и полезных инструментов JUnit 5 — это параметризованные тесты. Они позволяют запускать один и тот же тест с разными наборами входных данных, что значительно сокращает дублирование кода и расширяет покрытие тестами краевых случаев. Для создания такого теста достаточно использовать аннотацию @ParameterizedTest вместо @Test и указать источник данных с помощью одной из аннотаций, таких как @ValueSource (для простых литералов), @CsvSource (для CSV-строк), @EnumSource (для констант перечислений), @MethodSource (для фабричных методов) или @CsvFileSource (для чтения из файла) . Например, вместо того чтобы писать десять тестов для проверки метода, вычисляющего скидку, вы пишете один параметризованный тест, который принимает на вход сумму покупки и ожидаемую скидку. Это делает тесты более компактными и упрощает их поддержку при добавлении новых сценариев.

contents

TestNG: Мощная альтернатива. Хотя JUnit является стандартом де-факто, стоит упомянуть TestNG — фреймворк, который в некоторых сценариях предлагает более широкие возможности. Вдохновлённый JUnit и NUnit, TestNG предоставляет более гибкий контроль над выполнением тестов. Его ключевые особенности — это мощные аннотации (@BeforeSuite, @AfterClass и др.), поддержка зависимостей между тестами, группировка тестов (например, «smoke», «regression») и возможность параллельного выполнения тестов, что критически важно для сокращения времени прогона огромных наборов тестов . В отличие от JUnit 5, где многие из этих функций реализованы через расширения, TestNG предлагает их «из коробки» с помощью XML-конфигурации. Это делает его предпочтительным выбором для крупных проектов со сложной логикой тестирования, хотя у него и более крутая кривая обучения по сравнению с JUnit .

sections

Модуль 2. Разработка через тестирование (TDD) и BDD

contents

Test-Driven Development (TDD): Красный-Зелёный-Рефакторинг. Разработка через тестирование (TDD) — это не просто методика, а дисциплина, которая меняет весь процесс написания кода. Её суть описывается коротким циклом «Красный-Зелёный-Рефакторинг» . Шаг первый — Красный: вы пишете тест для новой функции, и он гарантированно падает, так как функция ещё не реализована. Шаг второй — Зелёный: вы пишете минимальный код, необходимый для того, чтобы тест стал «зелёным» (прошёл). Шаг третий — Рефакторинг: вы улучшаете структуру и чистоту написанного кода, при этом все тесты остаются зелёными . Этот цикл позволяет не только получить высокое покрытие кода тестами, но и проектировать архитектуру приложения «снизу вверх», что ведёт к созданию более модульного, слабо связанного и, как следствие, более поддерживаемого кода. TDD тесно связан с гибкими методологиями разработки и является ключевым навыком для профессионального разработчика.

contents

BDD и Cucumber: Тесты на понятном языке. Разработка, управляемая поведением (BDD), расширяет концепцию TDD, фокусируясь не на технических аспектах тестирования, а на поведении системы с точки зрения пользователя и бизнеса. Ключевой инструмент BDD для Java — это Cucumber. Cucumber позволяет описывать тестовые сценарии на языке Gherkin, который использует структурированный, но понятный всем участникам команды естественный язык (например, на русском). Сценарий описывает, что происходит, когда пользователь совершает определённое действие (Given-When-Then). Эти сценарии служат как живой документацией, которая всегда актуальна. Благодаря интеграции с JUnit через @RunWith(Cucumber.class), эти текстовые сценарии преобразуются в выполняемые тесты, связывая бизнес-требования с проверкой кода и обеспечивая прозрачность и ясность процесса разработки для всех стейкхолдеров .

sections

Модуль 3. Изоляция зависимостей: Mockito и не только

contents

Мокирование с Mockito: Что и зачем. В идеальном мире модульный тест должен проверять только логику одного конкретного класса, изолируя его от всех зависимостей — баз данных, файловых систем, внешних API. Для этого используются тестовые двойники (test doubles), и самый популярный инструмент для их создания в Java — это Mockito . Mockito позволяет создавать моки (mocks) — объекты-заглушки, которые эмулируют поведение реальных зависимостей. Вместо того чтобы создавать сложный объект с реальным соединением к БД, вы создаёте его мок, настраиваете метод findUserById() так, чтобы он возвращал заранее известного пользователя, и проверяете, что ваш сервис правильно обработал этот результат. Это делает тесты быстрыми, стабильными и предсказуемыми, так как они перестают зависеть от внешних факторов, таких как доступность сервера.

contents

Создание и настройка моков в Mockito. Работа с Mockito начинается с создания мока через mock(MyClass.class) или с помощью аннотации @Mock в сочетании с MockitoJUnitRunner или MockitoExtension для JUnit 5. После создания мока его поведение настраивается с помощью синтаксиса when(...).thenReturn(...) или when(...).thenThrow(...) . Например, when(userDao.findById(1L)).thenReturn(Optional.of(new User("Alex"))). Важным преимуществом Mockito является его интуитивно понятный API и понятные сообщения об ошибках, что значительно упрощает отладку. Помимо моков, Mockito поддерживает шпионы (spies) через @Spy, которые позволяют частично мокировать реальный объект, сохраняя поведение его настоящих методов. Mockito активно поддерживается и отлично интегрируется с большинством сред разработки .

contents

Верификация и ArgumentMatchers. Ключевая задача мокирования — не только «подставить» нужное поведение, но и проверить, что тестируемый код взаимодействует с зависимостями корректно. Mockito предоставляет метод verify() для проверки, что определённый метод мока был вызван с определёнными аргументами. Это особенно важно при тестировании сервисного слоя, где нужно убедиться, что сохранение в репозиторий происходит с правильными данными. Для гибкой проверки аргументов используются ArgumentMatchers, такие как any(), anyLong(), eq(), argThat() . Например, verify(userService).saveUser(argThat(user -> user.getAge() > 18)) проверяет, что метод saveUser был вызван с пользователем, возраст которого больше 18. Это мощный инструмент для точной и контекстно-зависимой проверки поведения системы.

contents

Расширение возможностей: PowerMock для «сложных» случаев. Несмотря на всю мощь Mockito, у него есть ограничения: по умолчанию он не может мокировать статические методы, финальные классы или конструкторы. Для этих редких, но сложных сценариев используется библиотека PowerMock (или её более современный аналог, Mockito с поддержкой inline мокинга). PowerMock работает путём манипуляции байт-кодом, что позволяет ему обходить эти ограничения . Для его использования требуется аннотация @PrepareForTest(StaticClass.class) и вызов специальных методов, например, PowerMockito.mockStatic(StaticClass.class). Однако стоит отметить, что использование PowerMock часто считается «запахом кода», сигнализирующим о проблемах в архитектуре, где слишком много статики или финальных классов, и к его применению следует прибегать с осторожностью, как к временному решению, а не постоянной практике.

sections

Модуль 4. Интеграционное тестирование и контейнеризация

contents

Интеграционные тесты: Проверка взаимодействий. В то время как модульные тесты проверяют «кирпичики» по отдельности, интеграционные тесты проверяют, как эти кирпичики работают вместе. Они убеждаются, что взаимодействие между компонентами системы, такими как базы данных, очереди сообщений и внешние сервисы, происходит корректно. В отличие от модульных тестов, интеграционные тесты работают с реальными или максимально приближенными к реальным зависимостями. Для их написания часто используется @SpringBootTest (для Spring-приложений), который поднимает весь контекст приложения. Однако запуск интеграционных тестов требует дополнительных ресурсов и времени. Здесь ключевую роль играет баланс: интеграционных тестов должно быть значительно меньше, чем модульных, чтобы не замедлять цикл разработки .

contents

TestContainers: Тестирование с реальными зависимостями в Docker. TestContainers — это библиотека, которая совершила революцию в интеграционном тестировании. Она позволяет запускать изолированные контейнеры Docker прямо во время выполнения тестов. Это значит, что вы можете поднять реальный экземпляр PostgreSQL, Redis, Kafka или любого другого сервиса, накатить на него схему, выполнить тест и уничтожить контейнер — всё это в рамках одного теста . Такой подход гарантирует, что ваши тесты работают с той же версией БД, что и в продакшне, а не с эмуляцией H2, которая может вести себя иначе. TestContainers интегрируется с JUnit 5 через API, позволяя легко объявлять контейнеры в коде теста и управлять их жизненным циклом, обеспечивая высочайший уровень достоверности интеграционных проверок.

contents

Spring Boot Test и MockMvc для веб-слоя. Тестирование веб-контроллеров в Spring Boot имеет свою специфику. Вместо поднятия всего приложения для теста одного эндпоинта, Spring предоставляет мощный инструмент MockMvc. Он позволяет выполнять HTTP-запросы к вашему контроллеру в тестовом окружении, не запуская полноценный веб-сервер, что значительно ускоряет выполнение тестов. С помощью @WebMvcTest(MyController.class) можно поднять только необходимый слой MVC и мокировать сервисные зависимости. MockMvc даёт возможность проверить и статус ответа, и структуру JSON-ответа, и даже заголовки . Например, mockMvc.perform(get("/api/users/1")).andExpect(status().isOk()).andExpect(jsonPath('$.name').value('John')). Такой подход обеспечивает быструю обратную связь при разработке REST API.

contents

Selenium WebDriver и Page Object Model. Selenium WebDriver является стандартом де-факто для автоматизации тестирования веб-пользовательских интерфейсов. Он позволяет программно управлять браузером, имитируя действия пользователя: клики, ввод текста, переход по ссылкам. Однако написание множества селекторов прямо в тесте делает код хрупким и сложным в поддержке. Для решения этой проблемы используется паттерн Page Object Model (POM) . Его суть заключается в том, что для каждой страницы или крупного компонента создаётся отдельный класс, который содержит все методы и локаторы для взаимодействия с этой страницей. Например, класс LoginPage будет иметь методы enterUsername(), enterPassword(), clickLoginButton(). Тесты же используют эти высокоуровневые методы, делая их читаемыми и устойчивыми к изменениям в вёрстке: если изменится id кнопки, исправление потребуется только в классе LoginPage, а не во всех тестах .

sections

Модуль 5. Инструменты качества, отладка и лучшие практики

contents

Структура и читаемость тестов: Принципы AAA и FIRST. Хороший тест не только проверяет код, но и служит документацией к нему. Для этого существуют проверенные практики. Принцип AAA (Arrange-Act-Assert) предписывает чёткое разделение теста на три части: Arrange — подготовка данных и окружения, Act — вызов тестируемого метода, Assert — проверка результатов. Это делает тест логичным и понятным с первого взгляда. Кроме того, тесты должны соответствовать принципу FIRST: Fast (быстрые), Isolated (изолированные, независимые друг от друга), Repeatable (повторяемые, дающие одинаковый результат при каждом запуске), Self-Validating (самопроверяемые, имеющие чёткий результат Pass/Fail) и Thorough (исчерпывающие, покрывающие краевые случаи и исключения) . Соблюдение этих принципов гарантирует, что ваш тестовый пакет будет надёжным фундаментом проекта.

contents

Покрытие кода (Code Coverage) и анализ с JaCoCo. Code Coverage — это метрика, показывающая, какая часть вашего кода (в строках или ветвлениях) была выполнена во время прогона тестов. Высокое покрытие само по себе не гарантирует качество тестов, но его низкие значения — верный признак проблемных мест. Для измерения покрытия в Java используется библиотека JaCoCo (Java Code Coverage), которая интегрируется с Maven и Gradle и может генерировать подробные отчёты. Эти отчёты показывают, какие строки, условия или методы не были покрыты тестами . Применять JaCoCo рекомендуется в паре с плагинами для CI, которые могут проверять порог покрытия и отклонять сборку, если он не достигнут. Однако нужно помнить, что цель — не 100% покрытие любой ценой, а покрытие критически важной логики, особенно сложных ветвлений и бизнес-правил .

contents

Статический анализ кода: PMD, SpotBugs и CheckStyle. Качество тестов и продуктивного кода можно проверять ещё до их выполнения, используя инструменты статического анализа. Эти инструменты анализируют исходный код на предмет потенциальных ошибок, уязвимостей и нарушений правил оформления. PMD ищет распространённые проблемы, такие как неиспользуемые переменные, пустые блоки catch, а также может вычислять метрики сложности, например, цикломатическую сложность . SpotBugs (ранее FindBugs) фокусируется на поиске ошибок безопасности (например, SQL-инъекции) и проблем с многопоточностью . CheckStyle же занимается в основном проверкой стиля кодирования, соответствием соглашениям об именовании и форматированию, что особенно полезно в больших командах для поддержания единообразия кодовой базы . Все эти инструменты легко интегрируются в Maven и Gradle.

contents

Интеграция в CI/CD и отчётность. Автоматическое тестирование теряет смысл без его интеграции в процесс непрерывной интеграции и доставки (CI/CD). В современной разработке каждый коммит в систему контроля версий (например, Git) должен автоматически запускать сборку (Maven или Gradle) и выполнять все тесты (JUnit, TestNG). Инструменты, такие как Jenkins, GitHub Actions или GitLab CI, позволяют настроить этот процесс. Они также агрегируют отчёты о тестах (обычно в формате XML или HTML), что позволяет команде быстро видеть статус сборки и просматривать результаты упавших тестов . Хорошо настроенный CI/CD пайплайн не только ускоряет процесс разработки, но и служит защитным барьером, предотвращая попадание «сломанного» кода в основную ветку или, тем более, в продакшн.

contents

Отладка (Debugging) и анализ ошибок в тестах. Упавший тест — это всегда сигнал к действию. Процесс отладки теста начинается с анализа логов и сообщений об ошибках. JUnit предоставляет детализированный вывод, показывающий, какой ассерт не прошёл и какое значение было получено вместо ожидаемого. Интеграция со средами разработки (IntelliJ IDEA, Eclipse) позволяет запускать тесты в режиме отладки, устанавливать точки останова, пошагово проходить выполнение тестируемого кода и проверять значения переменных в реальном времени. Если тест падает из-за проблем с зависимостями, на помощь приходят инструменты мокирования. Важно не просто «заставить» тест проходить, а понять корневую причину проблемы: ошибка в тесте (некорректный тест), ошибка в коде (баг) или ошибка в настройках окружения.

contents

Общие принципы написания хороших тестов. Следуя лучшим мировым практикам, можно выделить несколько золотых правил . Во-первых, каждый тест должен проверять только одну бизнес-логику (один метод или ветку условия), чтобы падение теста чётко указывало на конкретную проблему. Во-вторых, тест должен быть максимально коротким и читаемым, позволяя быстро понять, что именно проверяется. В-третьих, ожидаемый результат должен быть константой, а не вычисляться повторением тестируемой логики (например, int expected = 10 - 5 + 6 считается плохим тоном). В-четвёртых, тестовые данные должны быть понятными и располагаться рядом с проверкой, чтобы не приходилось искать определение переменных по всему классу. И наконец, если метод должен выбрасывать исключение, используйте assertThrows, а не try-catch с fail(), это делает код чище и информативнее.