<!DOCTYPE html>
<html class="h-100" data-bs-theme="light" data-mantine-color-scheme="light" lang="ru" prefix="og: https://ogp.me/ns#">
<head>
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<link crossorigin="true" href="https://cdn.hexlet.io" rel="preconnect">
<link href="https://mc.yandex.ru" rel="preconnect">
<meta content="aa2vrdtq64dub8knuf83lwywit311w" name="facebook-domain-verification">
<link href="/favicon.ico" rel="icon" sizes="any">
<link href="/favicon.svg" rel="icon" type="image/svg+xml">
<link href="/apple-touch-icon.png" rel="apple-touch-icon">
<link href="/manifest.webmanifest" rel="manifest">
<script>
//<![CDATA[
window.gon={};gon.ym_counter="25559621";gon.is_bot=true;gon.applications={};gon.current_user={"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26 19:46:36 UTC","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false};gon.token="QmiXvSUO-8PqdJXua1MjKwIKRvRvHM2KJgWZYsUkWCCtuVyK13BWo1w3sXZnXNNcwgNrXmcrMyib5QM2lyO_Tg";gon.locale="ru";gon.language="ru";gon.theme="light";gon.rails_env="production";gon.mobile=false;gon.google={"analytics_key":"UA-1360700-51","optimize_key":"GTM-5QDVFPF"};gon.captcha={"google_v3_site_key":"6LenGbgZAAAAAM7HbrDbn5JlizCSzPcS767c9vaY","yandex_site_key":"ysc1_Vyob5ZPPUdPBsu0ykt8bVFdzsfpoVjQChLGl2b4g19647a89","verification_failed":null};gon.social_signin=false;gon.typoreporter_google_form_id="1FAIpQLSeibfGq-KvWQ2Fyru-zkFFRVTLBuzXAHAoEyN1p49FtDmNoNA";
//]]>
</script>
<meta charset="utf-8">
<title>Интеграционные тесты | Spring Boot</title>
<meta name="description" content="Интеграционные тесты / Spring Boot: Учимся писать надежные интеграционные тесты">
<link rel="canonical" href="https://ru.hexlet.io/courses/java-spring-boot/lessons/integration-tests/theory_unit">
<meta name="robots" content="noarchive">
<meta property="og:title" content="Интеграционные тесты">
<meta property="og:title" content="Spring Boot">
<meta property="og:description" content="Интеграционные тесты / Spring Boot: Учимся писать надежные интеграционные тесты">
<meta property="og:url" content="https://ru.hexlet.io/courses/java-spring-boot/lessons/integration-tests/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="w1BlhqaaMSz2Owk8nEnDSfhb9kJVqpRIII8B-ZJlzkwsga6xVOScTEB4LaSQRjM-OFLb6F2dauqdb5utwGIpIg" />
<script src="/vite/assets/inertia-DfXos102.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/preload-helper-BJ4cLWpC.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-BrRXra1y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ahoy-DrlRQ-1D.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/analytics-cb8xch9l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Surface-DL2bpZA-.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/extends-C-EagtpE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/inheritsLoose-BBd-DCVI.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/objectWithoutPropertiesLoose-DRHXDhjp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/index.esm-DAqKOkZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Button-CGPUux8l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/CloseButton-D1euiPao.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Group-BX48WcuU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Loader-BQEY8g6v.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Modal-Cy3HByv7.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/OptionalPortal-1Hza5P2w.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Stack-CtjJzfw4.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Textarea-Ck64llAy.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/DirectionProvider-Dc9zdUke.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/events-DJQOhap0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-reduced-motion-D2owz4wa.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-disclosure-zKtK5W1r.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-hotkeys-Cnc_Rwkb.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/random-id-DOQyszCZ.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/exports-C_MrNx_T.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<link rel="stylesheet" href="/vite/assets/application-BqhCP46M.js" />
<script src="/vite/assets/application-Df9RExpe.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/autocomplete-VMNbxKGl.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/createPopper-C3aM9r1M.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/js.cookie-D1-O8zkX.js" as="script" crossorigin="anonymous"><link rel="stylesheet" href="/vite/assets/application-C8HjmMaq.css" media="screen" />
<script>
window.ym = function(){(ym.a=ym.a||[]).push(arguments)};
window.addEventListener('load', function() {
setTimeout(function() {
ym.l = 1*new Date();
ym(window.gon.ym_counter, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
});
// Загружаем скрипт
var k = document.createElement('script');
k.async = 1;
k.src = 'https://mc.yandex.ru/metrika/tag.js';
document.head.appendChild(k);
ym(window.gon.ym_counter, 'getClientID', function(clientID) {
window.ymClientId = clientID;
});
}, 1500);
});
</script>
<!-- Google Tag Manager - deferred -->
<script>
// dataLayer stub сразу — пуши работают до загрузки скрипта
window.dataLayer = window.dataLayer || [];
// Сам скрипт — отложенно после load
window.addEventListener('load', function() {
setTimeout(function() {
dataLayer.push({'gtm.start': new Date().getTime(), event: 'gtm.js'});
var j = document.createElement('script');
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-WK88TH';
document.head.appendChild(j);
}, 1500);
});
</script>
<!-- End Google Tag Manager -->
</head>
<body>
<noscript>
<div>
<img alt="" src="https://mc.yandex.ru/watch/25559621" style="position:absolute; left:-9999px;">
</div>
</noscript>
<header class="sticky-top bg-body">
<nav class="navbar navbar-expand-lg">
<div class="container-xxl">
<a class="navbar-brand" href="/"><img alt="Логотип Хекслета" height="24" src="https://ru.hexlet.io/vite/assets/logo_ru_light-BpiEA1LT.svg" width="96">
</a><button aria-controls="collapsable" aria-expanded="false" aria-label="Меню" class="navbar-toggler border-0 mb-0 mt-1" data-bs-target="#collapsable" data-bs-toggle="collapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="collapsable">
<ul class="navbar-nav mb-lg-0 mt-lg-1">
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
Все курсы
<span class="bi bi-chevron-down align-middle ms-1"></span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item d-flex py-2" href="/courses"><div class="fw-bold me-auto">Все что есть</div>
<div class="text-muted">117</div>
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные категории</b>
</li>
<li>
<a class="dropdown-item py-2" href="/courses_devops">Курсы по DevOps
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_data_analytics">Курсы по аналитике данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_programming">Курсы по программированию
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_testing">Курсы по тестированию
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные курсы</b>
</li>
<li>
<a class="dropdown-item py-2" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/go">Go-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/java">Java-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/python">Python-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/qa-auto-engineer-java">Автоматизатор тестирования на Java
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/data-analytics">Аналитик данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/frontend">Фронтенд-разработчик
</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
О Хекслете
<span class="bi bi-chevron-down align-middle"></span>
</button>
<ul class="dropdown-menu bg-body">
<li>
<a class="dropdown-item py-2" href="/pages/about">О нас
</a></li>
<li>
<a class="dropdown-item py-2" href="/blog">Блог
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/hse-research" role="button">Результаты (Исследование)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://career.hexlet.io" role="button">Хекслет Карьера
</span></li>
<li>
<a class="dropdown-item py-2" href="/testimonials">Отзывы студентов
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://t.me/hexlet_help_bot" role="button">Поддержка (В ТГ)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/referal-program/?promo_creative=priglasite-druzei&promo_name=referal-program&promo_position=promo_position&promo_start=010724&promo_type=link" role="button">Реферальная программа
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/certificate" role="button">Подарочные сертификаты
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://hh.ru/employer/4307094" role="button">Вакансии
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://b2b.hexlet.io" data-target="_blank" role="button">Компаниям
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexly.ru/" data-target="_blank" role="button">Колледж
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexlyschool.ru/" data-target="_blank" role="button">Частная школа
</span></li>
</ul>
</li>
<li><a class="nav-link" href="/subscription/new">Подписка</a></li>
</ul>
<ul class="navbar-nav flex-lg-row align-items-lg-center gap-2 ms-auto">
<li>
<a class="nav-link" aria-label="Переключить тему" href="/theme/switch?new_theme=dark"><span aria-hidden="true" class="bi bi-moon"></span>
</a></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="/u/new" role="button"><span>Регистрация</span>
</span></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="https://ru.hexlet.io/session/new" role="button"><span>Вход</span>
</span></li>
</ul>
</div>
</div>
</nav>
</header>
<div class="x-container-xxxl">
</div>
<main class="mb-6 min-vh-100 h-100">
<link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzczNSwicHVyIjoiYmxvYl9pZCJ9fQ==--883f3fd4e1b571538035b5680c8d4a9eb504b1f6/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Source%20code-amico.png"/><link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzY3NSwicHVyIjoiYmxvYl9pZCJ9fQ==--b3b44cb29727c1bcb0b9aee0c285371dc12aa50f/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Code%20typing-pana.png"/><link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NTIxOSwicHVyIjoiYmxvYl9pZCJ9fQ==--e79a18f1e9f90eae650f1f4f04115cba51f4e650/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Low%20code%20development-amico.png"/><link rel="preload" as="image" href="/vite/assets/development-BVihs_d5.png"/><div id="app" data-page="{"component":"web/courses/lessons/theory_unit","props":{"errors":{},"locale":"ru","language":"ru","httpsHost":"https://ru.hexlet.io","host":"ru.hexlet.io","colorScheme":"light","auth":{"user":{"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26T19:46:36.770Z","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false}},"cloudflareTurnstileSiteKey":"0x4AAAAAAA15KmeFXzd2H0Xo","vkIdClientId":"51586979","yandexIdClientId":"88d071f1d3384eb4bd1deb37910235c7","formAuthToken":"LpR7ZppSH6QURjp1RcwG4y3k9t4Q2eWIep2fWngCBcrBRbBRaCyyxKIFHu1Jw_aU7e3bdBjuGyrHfQUOKgXipA","topics":[{"id":96870,"title":"Меня лично сбило с толку почему когда мы используем генератор Instancio, то передаем в Select.field() ссылку на геттер, а не имя атрибута класса. Не хватает объяснения как именно в итоге вычисляется имя атрибута так как это не интуитивно.","plain_title":"Меня лично сбило с толку почему когда мы используем генератор Instancio, то передаем в Select.field() ссылку на геттер, а не имя атрибута класса. Не хватает объяснения как именно в итоге вычисляется имя атрибута так как это не интуитивно. ","creator":{"public_name":"Павел Краснощеков","id":766252,"is_tutor":false},"comments":[{"creator":{"public_name":"Maksim Litvinov","id":198906,"is_tutor":true},"id":187815,"body":"Хороший вопрос, Павел! Можно посмотреть в документации на Instancio, как это все работает внутри. Добавил ссылку в доп материал к уроку. Посмотрите пожалуйста, там есть ответ на ваш вопрос","topic_id":96870}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Интеграционные тесты","entity_url":null,"active":true}},{"id":94077,"title":"Здравствуйте , подскажите пожалуйста, раньше тема тестирования в Spring boot находилась по следующему адресу - https://ru.hexlet.io/courses/java-spring/lessons/spring-tests/theory_unit\nСейчас такой станицы не существует , где можно найти эти материалы лекций / видео и там ещё была \nзадача по тестированию небольшого проекта?","plain_title":"Здравствуйте , подскажите пожалуйста, раньше тема тестирования в Spring boot находилась по следующему адресу - https://ru.hexlet.io/courses/java-spring/lessons/spring-tests/theory_unit Сейчас такой станицы не существует , где можно найти эти материалы лекций / видео и там ещё была задача по тестированию небольшого проекта? ","creator":{"public_name":"Kirill Svirid","id":190113,"is_tutor":false},"comments":[{"creator":{"public_name":"Kirill Svirid","id":190113,"is_tutor":false},"id":184463,"body":"**Maksim Litvinov**, Здравствуйте! Спасибо за ответ. Но всё же уроков именно по автоматизированному тестированию на Java стало меньше.\nПодскажите пожалуйста, планируется ли курс по автоматизированному тестированию на Java , аналогичный вот этому - https://ru.hexlet.io/programs/python-pytest-testing ?","topic_id":94077},{"creator":{"public_name":"Maksim Litvinov","id":198906,"is_tutor":true},"id":184469,"body":"В Java тоже есть курс по автоматизированному тестированию https://ru.hexlet.io/courses/java-testing. Более продвинутые курсы по тестированию в целом планируются, но по срокам сориентировть не могу","topic_id":94077},{"creator":{"public_name":"Maksim Litvinov","id":198906,"is_tutor":true},"id":184335,"body":"Кирилл, курс по спрингу обновился, мы убрали старые видеоуроки и заменили их на актуальные текстовые. Теперь это актуальный урок по тестированию, и тут практически аналогичная задача","topic_id":94077},{"creator":{"public_name":"Kirill Svirid","id":190113,"is_tutor":false},"id":184478,"body":"**Maksim Litvinov**, Понятно, спасибо, интересовало больше тестирование со @Spring boot testing, @Mock-ами.\n","topic_id":94077}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Интеграционные тесты","entity_url":null,"active":true}},{"id":95395,"title":"В теории, в разделе \"Взаимодействие с базой\" говорится о том, что для тестов необходимо наполнять БД данными и о том, что важно эти данные потом за собой удалить. По материалу разъяснено как наполнять данными БД, но нет разъяснений о том, как БД потом очищается. Как эту задачу выполнить или данные сами удалятся после тестов?","plain_title":"В теории, в разделе \"Взаимодействие с базой\" говорится о том, что для тестов необходимо наполнять БД данными и о том, что важно эти данные потом за собой удалить. По материалу разъяснено как наполнять данными БД, но нет разъяснений о том, как БД потом очищается. Как эту задачу выполнить или данные сами удалятся после тестов? ","creator":{"public_name":"Игорь Черкасов","id":265748,"is_tutor":false},"comments":[{"creator":{"public_name":"Maksim Litvinov","id":198906,"is_tutor":true},"id":186077,"body":"Приветствую, Игорь! Вот тут есть ответ на ваш вопрос:\n\n> По этой причине в большинстве фреймворков каждый тест выполняется в транзакции, которая откатывается в конце теста. Таким образом достигается полная изоляция тестов друг от друга","topic_id":95395},{"creator":{"public_name":"Игорь Черкасов","id":265748,"is_tutor":false},"id":186084,"body":"**Maksim Litvinov**, \nспасибо за ответ. Но предложение написано так, будто это не относится к теме Spring. Как можно быть полностью уверенным в том, что транзакции откатываются?\n\nЯ нашел информацию в оф доке https://docs.spring.io/spring-framework/reference/testing/integration.html#testing-tx, но в теории в примере не использовали TestContext Framework.","topic_id":95395},{"creator":{"public_name":"Игорь Черкасов","id":265748,"is_tutor":false},"id":186081,"body":"Спасибо за ответ.\nНо предложение написано так, будто это не относится к теме Spring. Как можно быть полностью уверенным в том, что транзакции откатываются?\n\nЯ нашел информацию в оф доке https://docs.spring.io/spring-framework/reference/testing/integration.html#testing-tx, но мы не использовали явно в примере в задаче TestContext Framework. ","topic_id":95395},{"creator":{"public_name":"Maksim Litvinov","id":198906,"is_tutor":true},"id":186120,"body":"Игорь, это делает аннотация `@SpringBootTest`, которую мы используем https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/context/SpringBootTest.html\n\n> Annotation that can be specified on a test class that runs Spring Boot based tests. Provides the following features over and above the regular Spring TestContext Framework","topic_id":95395}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Интеграционные тесты","entity_url":null,"active":true}},{"id":94659,"title":"Добрый день! Вижу опечатку в пути к файлу, где нужно выполнить упражнение. Написано:\n\n> src/main/java/exercise/controller/Application.java\n\nДолжно быть:\n\n> src/test/java/exercise/controller/TaskControllerTest.java\n\nИ ещё такой вопрос: корректно ли работает механизм по отправке ошибок/опечаток? Отправляла сообщение об этой ошибке пару-тройку дней назад, и, как вижу, ошибку так и не исправили. Ранее тоже отправляла подобные сообщения, но не замечала изменений после этого.","plain_title":"Добрый день! Вижу опечатку в пути к файлу, где нужно выполнить упражнение. Написано: src/main/java/exercise/controller/Application.java Должно быть: src/test/java/exercise/controller/TaskControllerTest.java И ещё такой вопрос: корректно ли работает механизм по отправке ошибок/опечаток? Отправляла сообщение об этой ошибке пару-тройку дней назад, и, как вижу, ошибку так и не исправили. Ранее тоже отправляла подобные сообщения, но не замечала изменений после этого. ","creator":{"public_name":"Elisa Moritz","id":632086,"is_tutor":false},"comments":[{"creator":{"public_name":"Elisa Moritz","id":632086,"is_tutor":false},"id":185150,"body":"**Maksim Litvinov**, поняла, буду иметь в виду :)","topic_id":94659},{"creator":{"public_name":"Maksim Litvinov","id":198906,"is_tutor":true},"id":185130,"body":"Добрый день! Спасибо, поправил. Чтобы получить обновленную версию домашки, нужно будет ресетом перекачать. Отправка сообщений об опечатках работает, сообщения приходят. Обычно мы исправляем их в течении недели, так что до вашего скорее всего пока еще не добрались)","topic_id":94659}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Интеграционные тесты","entity_url":null,"active":true}},{"id":99407,"title":"Здравствуйте! \n```\nvar user = Instancio.of(User.class)\n .ignore(Select.field(User::getId))\n .supply(Select.field(User::getEmail), () -> faker.internet().emailAddress())\n .create();\nuserRepository.save(user);\n```\nПочему мы не можем создать сами класс в тестах?\n\n```\nUser user = new User();\nuser.setEmail = faker.internet().emailAddress();\nuserRepository.save(user);\n```\nНаверняка у библиотеки Instancio много преимуществ. Но в данном случае они(преимущества) не очевидны.\nПопытался найти на сайте. Тоже не многословно и обтекаемо.\nhttps://www.instancio.org/#:~:text=Why%20use%20it,static%20test%20data.\n>** Why use it?**\nThe goal is to reduce the time and lines of code spent on manual data setup in unit tests, and potentially to uncover bugs that may have gone unnoticed with static test data.\n\nСпасибо за пояснения!","plain_title":"Здравствуйте! var user = Instancio.of(User.class) .ignore(Select.field(User::getId)) .supply(Select.field(User::getEmail), () -> faker.internet().emailAddress()) .create(); userRepository.save(user); Почему мы не можем создать сами класс в тестах? User user = new User(); user.setEmail = faker.internet().emailAddress(); userRepository.save(user); Наверняка у библиотеки Instancio много преимуществ. Но в данном случае они(преимущества) не очевидны. Попытался найти на сайте. Тоже не многословно и обтекаемо. https://www.instancio.org/#:~:text=Why%20use%20it,static%20test%20data. ** Why use it?** The goal is to reduce the time and lines of code spent on manual data setup in unit tests, and potentially to uncover bugs that may have gone unnoticed with static test data. Спасибо за пояснения! ","creator":{"public_name":"Evgeniy Shevelev","id":481140,"is_tutor":false},"comments":[{"creator":{"public_name":"Maksim Litvinov","id":198906,"is_tutor":true},"id":190883,"body":"Добрый день! Можно и так, но зачем руками это делать, когда можно всю рутину поручить уже написанным инструментам. Instancio довольно гибко позволяет настраивать прцесс создания объектов. А в более общем случае вообще можно вот так написать `Instancio.of(User.class).create()` и он сам все заполнит, опираясь на поля переданной модели","topic_id":99407}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Интеграционные тесты","entity_url":null,"active":true}},{"id":99684,"title":"Метод generateTask уже реализован, думаю, следует удалить :)\n> \"Создание объекта сущности можно вынести в отдельный метод\"","plain_title":"Метод generateTask уже реализован, думаю, следует удалить :) \"Создание объекта сущности можно вынести в отдельный метод\" ","creator":{"public_name":"Алекс Гамбург","id":418830,"is_tutor":false},"comments":[{"creator":{"public_name":"Maksim Litvinov","id":198906,"is_tutor":true},"id":191235,"body":"Пожалуй, вы правы. Убрал этот метод из уже реализованной части","topic_id":99684}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Интеграционные тесты","entity_url":null,"active":true}},{"id":102949,"title":"> Кроме наполнения базы, нам нужна еще и ее очистка.\nПредставьте, что мы написали тест, который создает пользователя. Если после теста мы не удалим этого пользователя, то следующий тест может завершиться с ошибкой — он не рассчитывает, что в базе уже есть такие данные. По этой причине в большинстве фреймворков каждый тест выполняется в отдельной транзакции, которая откатывается в конце теста. Таким образом достигается полная изоляция тестов друг от друга.\n\nА каким образом можно очистить БД после тестов? Кто это должен делать? Instancio же только позволяет в удобном виде создать тестовую сущность, если я правильно понимаю. В статье не раскрывается никак этот вопрос, хотя и написано отдельным абзацем, что нужно очищать. Или вручную писать @AfterEach? Как тогда можно откатить эту транзакцию? \n\nМожно поподробнее раскрыть этот абзац, что тут имеется ввиду? обычное удаление ранее созданного пользователя или именно транзакционный механизм БД каким-то образом задействовать можно:\n- @BeforeEach -> открыть транзакцию\n- @Test -> выполнить какую-то проверку, какие-то манипуляции с БД\n- @AfterEach -> делаем rollback и все изменения автоматически откатываются\n?","plain_title":"Кроме наполнения базы, нам нужна еще и ее очистка. Представьте, что мы написали тест, который создает пользователя. Если после теста мы не удалим этого пользователя, то следующий тест может завершиться с ошибкой — он не рассчитывает, что в базе уже есть такие данные. По этой причине в большинстве фреймворков каждый тест выполняется в отдельной транзакции, которая откатывается в конце теста. Таким образом достигается полная изоляция тестов друг от друга. А каким образом можно очистить БД после тестов? Кто это должен делать? Instancio же только позволяет в удобном виде создать тестовую сущность, если я правильно понимаю. В статье не раскрывается никак этот вопрос, хотя и написано отдельным абзацем, что нужно очищать. Или вручную писать @AfterEach? Как тогда можно откатить эту транзакцию? Можно поподробнее раскрыть этот абзац, что тут имеется ввиду? обычное удаление ранее созданного пользователя или именно транзакционный механизм БД каким-то образом задействовать можно: - @BeforeEach -> открыть транзакцию - @Test -> выполнить какую-то проверку, какие-то манипуляции с БД - @AfterEach -> делаем rollback и все изменения автоматически откатываются ? ","creator":{"public_name":"Илья Гутевич","id":160459,"is_tutor":false},"comments":[{"creator":{"public_name":"Maksim Litvinov","id":198906,"is_tutor":true},"id":195327,"body":"Приветствую, Илья! Да, вы правы, в уроке нужно рассказать про это. Спасибо, доработаю урок. Тут можно пойти двумя путями - использовать аннотацию @Transactional, тогда каждый тест будет выполняться в транзакции и она будет откатываться по завершении теста. Либо чистить базу в хуке beforeEach перед каждым тестом при помощи методов репозитория. Пример можно в нашем демо проекте посмотреть https://github.com/hexlet-components/java-spring-blog/blob/main/src/test/java/io/hexlet/blog/controller/api/PostsCommentsControllerTest.java","topic_id":102949}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Интеграционные тесты","entity_url":null,"active":true}},{"id":104748,"title":"В теории на шаге 2, мы же игнорим id, как мы можем быть уверены, что task сохранится в базе с таким же id? При сохранении же id создаётся автоматом, и не факт, что такой же.\n\nВозможный ответ: в базе ничего нет, поэтому мы точно знаем, что id == 1?\n\nВозможный ответ: лучше искать в базе по title, чем по id?","plain_title":"В теории на шаге 2, мы же игнорим id, как мы можем быть уверены, что task сохранится в базе с таким же id? При сохранении же id создаётся автоматом, и не факт, что такой же. Возможный ответ: в базе ничего нет, поэтому мы точно знаем, что id == 1? Возможный ответ: лучше искать в базе по title, чем по id? ","creator":{"public_name":"Ramil yabbarov Яббаров","id":42999,"is_tutor":false},"comments":[{"creator":{"public_name":"Maksim Litvinov","id":198906,"is_tutor":true},"id":197345,"body":"В теории речь о юзерах ведь. Но сути это наверное не меняет. Мы ведь и не опираемся на конкретный айдишник, мы получаем его у сущности:\n\n```java\nvar request = put(\"/api/users/\" + user.getId())\n```\nМы не можем быть точно уверены, под каким id сущность сохранилась в базе. Игнор означает, что айдишник не присваивается библиотекой Instancio при создании объекта, но он присваивается базой. И после сохранения сущности у нее уже появляется айдишник. На него мы и ориентируемся\n\n> лучше искать в базе по title, чем по id?\n\nНазвания, имена и тд не уникальны, может быть много пользователей с одинаковым именем и статей с одинаковым названием. А вот айдишник - это первичный ключ и он уникален","topic_id":104748},{"creator":{"public_name":"Виталий Проценко","id":808689,"is_tutor":false},"id":197393,"body":"Возник такой же вопрос) Можно поподробнее. С помощью библиотеки Instancio мы создали объект, игнорируемые поля будут либо 0 либо null. С этим все ясно. Но ведь метод save который сохраняет сущность не изменяет наш созданный объект, а возвращает новый, в документации так и написано использовать возвращаемый экземпляр для дальнейших операций. Далее при формировании пути запроса мы получаем id и он будет null. Или я ошибаюсь? Разве мы не должны перезаписать переменную user = userRepository.save(user) \n```\n//Создали объект, поле id будет иметь значение null\nvar user = Instancio.of(User.class)\n .ignore(Select.field(User::getId))\n .supply(Select.field(User::getEmail), () -> faker.internet().emailAddress())\n .create();\n// Сохраняем в базу данных, но не присваиваем возвращаемый объект переменной\nuserRepository.save(user);\n\nvar data = new HashMap<>();\ndata.put(\"firstName\", \"Mike\");\n\n// Переменная user ссылается на объект с id null\nvar request = put(\"/api/users/\" + user.getId())\n .contentType(MediaType.APPLICATION_JSON)\n // ObjectMapper конвертирует Map в JSON\n .content(om.writeValueAsString(data));\n```","topic_id":104748},{"creator":{"public_name":"Ramil yabbarov Яббаров","id":42999,"is_tutor":false},"id":197455,"body":"В общем, поразбирался я, тут нет генерации кода, как в мапперах. Имплементация основана на прокси и перехватчике метода, который делегирует вызов уже куда нужно.\nА вот пример реального кода save()\n\n`@Override\n\t@Transactional\n\tpublic <S extends T> S save(S entity) {\n\t\tAssert.notNull(entity, ENTITY_MUST_NOT_BE_NULL);\n\t\tif (entityInformation.isNew(entity)) {\n\t\t\tentityManager.persist(entity);\n\t\t\treturn entity;\n\t\t} else {\n\t\t\treturn entityManager.merge(entity);\n\t\t}\n\t}\n`","topic_id":104748},{"creator":{"public_name":"Виталий Проценко","id":808689,"is_tutor":false},"id":197410,"body":"Вопрос снят, оказалось hibernate отслеживает состояние Entity объектов. Жаль что в теоретической части об этом ни слова. Удивительно как много работы проделывает спринг неявно, но именно эта фича пока что кажется избыточной имхо","topic_id":104748}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Интеграционные тесты","entity_url":null,"active":true}},{"id":104749,"title":"Хорошим ли тоном является выбрасывание исключений в тестах? Когда например запись не найдена?","plain_title":"Хорошим ли тоном является выбрасывание исключений в тестах? Когда например запись не найдена? ","creator":{"public_name":"Ramil yabbarov Яббаров","id":42999,"is_tutor":false},"comments":[{"creator":{"public_name":"Maksim Litvinov","id":198906,"is_tutor":true},"id":197346,"body":"Ну тут нужно порассуждать, а зачем это делать? И для чего вообще нужны исключения","topic_id":104749}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Интеграционные тесты","entity_url":null,"active":true}},{"id":107813,"title":"Почему мы прописываем в .and лямбды?\n\n```\nassertThatJson(\"{\\\"a\\\":1, \\\"b\\\":2, \\\"c\\\":3}\").and(\n a -> a.node(\"a\").isEqualTo(1),\n a -> a.node(\"b\").isEqualTo(2)\n);\n```\n\n Почему нельзя через .and() перечисления сделать? (так не работает)\n```\nassertThatJson(\"{\\\"a\\\":1, \\\"b\\\":2, \\\"c\\\":3}\")\n .node(\"a\").isEqualTo(1)\n .and()\n .node(\"b\").isEqualTo(2);\n```\n","plain_title":"Почему мы прописываем в .and лямбды? assertThatJson(\"{\\\"a\\\":1, \\\"b\\\":2, \\\"c\\\":3}\").and( a -> a.node(\"a\").isEqualTo(1), a -> a.node(\"b\").isEqualTo(2) ); Почему нельзя через .and() перечисления сделать? (так не работает) assertThatJson(\"{\\\"a\\\":1, \\\"b\\\":2, \\\"c\\\":3}\") .node(\"a\").isEqualTo(1) .and() .node(\"b\").isEqualTo(2); ","creator":{"public_name":"Андрей Кокорев","id":873990,"is_tutor":false},"comments":[{"creator":{"public_name":"Maksim Litvinov","id":198906,"is_tutor":true},"id":201887,"body":"Андрей, такой интерфейс сделали разработчики библиотеки. Метод and() принимает лямбды. Каждая лямбда - по сути своя цепочка ассертов для узлов","topic_id":107813}],"communitable":{"parent_entity_name":null,"parent_entity_url":null,"entity_name":"Интеграционные тесты","entity_url":null,"active":true}}],"lesson":{"exercise":{"id":4452,"slug":"java_spring_boot_integration_tests_exercise","name":null,"state":"active","kind":"exercise","language":"java","locale":"ru","has_web_view":true,"has_test_view":true,"reviewable":true,"readme":"Классический `@WebMvcTest` — это скорее «slice»-тест, который поднимает только слой Spring MVC вместе с контроллерами, мапперами, валидацией и т. п., но не затрагивает остальной контекст (JPA, сервисы, внешние системы). Поэтому строго интеграционными его не называют: он ближе к расширенным модульным тестам веб-слоя. Интеграционным обычно считается тест, где поднимается весь Spring Boot контекст или хотя бы несколько слоёв, проверяющих совместную работу настоящих компонентов.\n\nВ этом задании вам предстоит протестировать уже готовое приложение на Spring Boot – простой менеджер задач.\n\nВ файле уже настроен срез `@WebMvcTest`, поэтому бэкенд и база не поднимаются целиком. Ваша задача — дописать тесты:\n\n* `testIndex`\n* `testShow`\n* `testCreate`\n* `testUpdate`\n* `testDelete`\n\n### src/test/java/io/hexlet/controller/TaskControllerTest.java\n\n\nПроверьте работу следующих маршрутов:\n\n* Просмотр конкретной задачи\n* Создание новой задачи\n* Обновление существующей задачи\n* Удаление задачи\n\nДля генерации названия и описания для задачи используйте Faker. Выберите провайдер, который вам нравится. Можно использовать `lorem()`, который генерирует фразы:\n\n```java\nfaker.lorem().word(); // Генерирует одно слово\n\n// Генерирует предложение\n// Количество слов случайное, в пределах диапазона (wordCount+1)..(wordCount+6)\nfaker.lorem().sentence(int wordCount);\n```\n\nДля проверки полей JSON вам пригодится матчер `and()` библиотеки JsonUnit. Вот пример из официальной документации:\n\n```java\nassertThatJson(\"{\\\"test\\\":{\\\"a\\\":1, \\\"b\\\":2, \\\"c\\\":3}}\").and(\n a -> a.node(\"test.a\").isEqualTo(1),\n a -> a.node(\"test.b\").isEqualTo(2)\n);\n```\n\n## Запуск приложения\n\nЧтобы запустить приложение, соберите его командой `make compile` в терминале. После окончания сборки откройте веб доступ\n\nВ упражнении организована автоматическая перезагрузка сервера при внесении изменений в файлы классов. Чтобы подтянуть сделанные изменения, соберите приложение командой `make compile` и перезагрузите страницу веб доступа\n\n## Подсказки\n\n* Мок `TaskRepository` уже подключен (см. `@MockBean`). Задайте ожидаемое поведение через Mockito (`when`, `doNothing`, `verify`), чтобы контролировать ответы контроллера.\n* Для генерации тестовых данных удобно использовать `Faker`, а сериализацию/request body выполняйте через `ObjectMapper`, который уже внедрён в тест.\n* JsonUnit (`assertThatJson`) хорошо подходит для проверки отдельных полей JSON-ответа.\n* Если тесты работают слишком долго, перед запуском проверки выполните `make compile` — так контейнер собирает приложение заранее и быстрее стартует во время `make test`.\n","prepared_readme":"Классический `@WebMvcTest` — это скорее «slice»-тест, который поднимает только слой Spring MVC вместе с контроллерами, мапперами, валидацией и т. п., но не затрагивает остальной контекст (JPA, сервисы, внешние системы). Поэтому строго интеграционными его не называют: он ближе к расширенным модульным тестам веб-слоя. Интеграционным обычно считается тест, где поднимается весь Spring Boot контекст или хотя бы несколько слоёв, проверяющих совместную работу настоящих компонентов.\n\nВ этом задании вам предстоит протестировать уже готовое приложение на Spring Boot – простой менеджер задач.\n\nВ файле уже настроен срез `@WebMvcTest`, поэтому бэкенд и база не поднимаются целиком. Ваша задача — дописать тесты:\n\n* `testIndex`\n* `testShow`\n* `testCreate`\n* `testUpdate`\n* `testDelete`\n\n### src/test/java/io/hexlet/controller/TaskControllerTest.java\n\n\nПроверьте работу следующих маршрутов:\n\n* Просмотр конкретной задачи\n* Создание новой задачи\n* Обновление существующей задачи\n* Удаление задачи\n\nДля генерации названия и описания для задачи используйте Faker. Выберите провайдер, который вам нравится. Можно использовать `lorem()`, который генерирует фразы:\n\n```java\nfaker.lorem().word(); // Генерирует одно слово\n\n// Генерирует предложение\n// Количество слов случайное, в пределах диапазона (wordCount+1)..(wordCount+6)\nfaker.lorem().sentence(int wordCount);\n```\n\nДля проверки полей JSON вам пригодится матчер `and()` библиотеки JsonUnit. Вот пример из официальной документации:\n\n```java\nassertThatJson(\"{\\\"test\\\":{\\\"a\\\":1, \\\"b\\\":2, \\\"c\\\":3}}\").and(\n a -> a.node(\"test.a\").isEqualTo(1),\n a -> a.node(\"test.b\").isEqualTo(2)\n);\n```\n\n## Запуск приложения\n\nЧтобы запустить приложение, соберите его командой `make compile` в терминале. После окончания сборки откройте веб доступ\n\nВ упражнении организована автоматическая перезагрузка сервера при внесении изменений в файлы классов. Чтобы подтянуть сделанные изменения, соберите приложение командой `make compile` и перезагрузите страницу веб доступа\n\n## Подсказки\n\n* Мок `TaskRepository` уже подключен (см. `@MockBean`). Задайте ожидаемое поведение через Mockito (`when`, `doNothing`, `verify`), чтобы контролировать ответы контроллера.\n* Для генерации тестовых данных удобно использовать `Faker`, а сериализацию/request body выполняйте через `ObjectMapper`, который уже внедрён в тест.\n* JsonUnit (`assertThatJson`) хорошо подходит для проверки отдельных полей JSON-ответа.\n* Если тесты работают слишком долго, перед запуском проверки выполните `make compile` — так контейнер собирает приложение заранее и быстрее стартует во время `make test`.\n","has_solution":true,"entity_name":"Интеграционные тесты"},"units":[{"id":9562,"name":"theory","url":"/courses/java-spring-boot/lessons/integration-tests/theory_unit"},{"id":9994,"name":"quiz","url":"/courses/java-spring-boot/lessons/integration-tests/quiz_unit"},{"id":15095,"name":"exercise","url":"/courses/java-spring-boot/lessons/integration-tests/exercise_unit"}],"links":[{"id":428575,"name":"Документация Instancio","url":"https://www.instancio.org/user-guide/#selectors\n"},{"id":428576,"name":"Документация Spring Framework о тестировании\n","url":"https://docs.spring.io/spring/reference/testing.html"},{"id":428577,"name":"Как подключить SonarQube в свой проект\n","url":"https://ru.hexlet.io/blog/posts/kak-podklyuchit-sonarqube-v-svoy-proekt"},{"id":428578,"name":"Пример приложения с написаннными тестам и CI\n","url":"https://github.com/hexlet-components/java-spring-blog"}],"ordered_units":[{"id":9562,"name":"theory","url":"/courses/java-spring-boot/lessons/integration-tests/theory_unit"},{"id":9994,"name":"quiz","url":"/courses/java-spring-boot/lessons/integration-tests/quiz_unit"},{"id":15095,"name":"exercise","url":"/courses/java-spring-boot/lessons/integration-tests/exercise_unit"}],"id":4260,"slug":"integration-tests","state":"approved","name":"Интеграционные тесты","course_order":163,"goal":"Учимся писать надежные интеграционные тесты","self_study":"На этом шаге вы подготовите базовую тестовую инфраструктуру, напишете позитивные тесты для CRUD и настроите CI с отчётами о покрытии кода и анализом качества.\n\n1. **Добавляем зависимости для тестирования**\n * В `build.gradle.kts` подключите библиотеки и включите JUnit Platform.\n <details>\n <summary>Пример</summary>\n\n ```kotlin\n dependencies {\n testImplementation(\"org.springframework.boot:spring-boot-starter-test\")\n // при необходимости: testImplementation(\"com.h2database:h2\")\n }\n\n tasks.test {\n useJUnitPlatform()\n }\n ```\n\n </details>\n\n2. **Пишем позитивные тесты для CRUD**\n * Начните с самых важных сценариев: создание, получение списка, получение по id, обновление, удаление. Используйте `MockMvc` или слой сервиса — по вашему проекту.\n <details>\n <summary>Пример: UserControllerTest (фрагмент)</summary>\n\n ```java\n @SpringBootTest\n @AutoConfigureMockMvc\n class UserControllerTest {\n\n @Autowired\n private MockMvc mockMvc;\n\n @Test\n void createUser_returns201_andBody() throws Exception {\n var body = \"\"\"\n {\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\"\n }\n \"\"\";\n\n mockMvc.perform(post(\"/api/users\")\n .contentType(MediaType.APPLICATION_JSON)\n .content(body))\n .andExpect(status().isCreated())\n .andExpect(jsonPath(\"$.id\").exists())\n .andExpect(jsonPath(\"$.email\").value(\"john@example.com\"));\n }\n }\n ```\n\n </details>\n\n <details>\n <summary>Пример: PostControllerTest (фрагмент)</summary>\n\n ```java\n @SpringBootTest\n @AutoConfigureMockMvc\n class PostControllerTest {\n\n @Autowired\n private MockMvc mockMvc;\n\n @Test\n void listPublished_returns200_andPage() throws Exception {\n mockMvc.perform(get(\"/api/posts\")\n .param(\"page\", \"0\")\n .param(\"size\", \"5\"))\n .andExpect(status().isOk())\n .andExpect(jsonPath(\"$.content\").isArray());\n }\n }\n ```\n\n </details>\n\n3. **Настраиваем CI в GitHub Actions**\n * Создайте файл воркфлоу, чтобы на каждом push/PR собирался проект и запускались тесты.\n\n <details>\n <summary>Пример: <code>.github/workflows/ci.yml</code></summary>\n\n ```yaml\n name: CI\n\n on:\n push:\n branches: [ \"main\" ]\n pull_request:\n branches: [ \"main\" ]\n\n jobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - name: Set up JDK\n uses: actions/setup-java@v4\n with:\n distribution: temurin\n java-version: \"24\"\n - name: Cache Gradle\n uses: gradle/actions/setup-gradle@v3\n - name: Build & Test\n run: ./gradlew clean build test\n ```\n\n </details>\n\n4. **Подключаем покрытие кода (JaCoCo)**\n * Включите плагин и генерируйте отчёты (HTML/XML) локально и на CI.\n <details>\n <summary>Пример: _build.gradle.kts_</summary>\n\n ```kotlin\n plugins {\n jacoco\n }\n\n tasks.jacocoTestReport {\n dependsOn(tasks.test)\n reports {\n xml.required.set(true)\n html.required.set(true)\n }\n }\n\n tasks.check {\n dependsOn(tasks.jacocoTestReport)\n }\n ```\n\n </details>\n\n <details>\n <summary>Пример: шаг в CI</summary>\n\n ```yaml\n - name: Coverage report\n run: ./gradlew jacocoTestReport\n ```\n\n </details>\n\n5. **Интегрируем SonarQube / SonarCloud**\n * Подключите плагин и добавьте анализ в CI (потребуются токены).\n\n <details>\n <summary>Пример: <code>build.gradle.kts</code> + шаг анализа</summary>\n\n ```kotlin\n plugins {\n id(\"org.sonarqube\") version \"4.4.1.3373\"\n }\n\n sonarqube {\n properties {\n property(\"sonar.projectKey\", \"YOUR_PROJECT_KEY\")\n property(\"sonar.organization\", \"YOUR_ORG\") // для SonarCloud\n property(\"sonar.host.url\", \"https://sonarcloud.io\") // или ваш SonarQube\n property(\"sonar.java.coveragePlugin\", \"jacoco\")\n property(\"sonar.junit.reportPaths\", \"build/test-results/test\")\n property(\"sonar.jacoco.reportPaths\", \"build/jacoco/test.exec\")\n property(\"sonar.coverage.jacoco.xmlReportPaths\", \"build/reports/jacoco/test/jacocoTestReport.xml\")\n }\n }\n ```\n\n </details>\n\n <details>\n <summary>Пример: шаг Sonar в CI</summary>\n\n ```yaml\n - name: SonarQube Scan\n env:\n SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}\n run: ./gradlew sonarqube -Dsonar.login=${{ secrets.SONAR_TOKEN }}\n ```\n\n </details>\n6. **Обновляем README**\n * Добавьте бейджи: статус CI, покрытие, Quality Gate.\n <details>\n <summary>Пример (заготовки)</summary>\n\n ```md\n \n [](https://sonarcloud.io/summary/new_code?id=YOUR_PROJECT_KEY)\n ```\n\n </details>\n\n**Проверяем работу**\n\n* Локально: запустить `./gradlew test jacocoTestReport` — убедитесь, что отчёт покрытия появился в _build/reports/jacoco/test/html_.\n* В GitHub: проверьте, что по PR запускается CI, тесты проходят, отчёты генерируются, анализ Sonar выполняется.\n\n**Итог:** у вас есть автоматизированные тесты для CRUD, CI-пайплайн с прогоном тестов и сборкой, формируется отчёт о покрытии кода и выполняется статический анализ качества. Это ускорит обратную связь и повысит надёжность проекта.\n","theory_video_provider":null,"theory_video_uid":null,"theory":"Тестирование приложений на Spring Boot — неотъемлемая часть профессиональной жизни веб-разработчиков на Java. Сюда входит написание различных тестов:\n\n* Юнит-тестов для отдельных модулей\n* Интеграционных тестов, проверяющих работоспособность всего приложения\n\nВ этом уроке мы научимся создавать интеграционные тесты для наших веб-приложений на Spring Boot.\n\nИнтеграционное тестирование веб-приложений устроено сложнее, чем тестирование библиотечного кода, где мы вызываем какие-то методы и смотрим на результат.\n\nВеб-приложения работают по сети, обрабатывая HTTP-запросы. Такое поведение придется повторять прямо в тестах или как-то имитировать. Spring Boot позволяет использовать оба подхода. Мы остановимся на подходе с подменой веб-сервера, чтобы ускорить запуск и выполнение тестов. В остальном эти тесты проверяют работу приложения от запроса до ответа, что дает очень высокую степень уверенности в том, что приложение работает.\n\nДля работы с тестами нужно установить зависимости:\n\n```java\ntestImplementation(platform(\"org.junit:junit-bom:5.10.0\"))\ntestImplementation(\"org.junit.jupiter:junit-jupiter:5.10.0\")\ntestImplementation(\"org.springframework.boot:spring-boot-starter-test\")\n// Понадобится когда мы начнем работать с аутентификацией\ntestImplementation(\"org.springframework.security:spring-security-test\")\n```\n\nКроме классического Junit, здесь мы видим пакеты, специфичные для Spring Boot. Они дают все необходимые инструменты, чтобы мы могли писать тесты легко и эффективно.\n\n## Первый тест\n\nИнтеграционные тесты в Spring Boot связаны с маршрутами. Каждый тест — это запрос на конкретный адрес для тестирования конкретного маршрута. Количество тестов для одного маршрута может быть разным, но конкретный тест — это всегда запрос-ответ.\n\nНачнем с примера. Предположим, что у нас есть маршрут */api/users*, который возвращает список пользователей. Тест на такой маршрут должен выполнить запрос на этот адрес. Вот как будет выглядеть структура файлов в этом случае:\n\n```bash\ntree src\nsrc\n├── main\n│ ├── java\n│ │ └── io\n│ │ └── hexlet\n│ │ └── spring\n│ │ ├── Application.java\n│ │ ├── controller\n│ │ │ └── api\n│ │ │ └── UsersController.java\n│ │ ├── model\n│ │ │ └── User.java\n│ │ ├── repository\n│ │ │ └── UserRepository.java\n│ └── resources\n│ ├── application.yml\n└── test\n └── java\n └── io\n └── hexlet\n └── spring\n └── controller\n └── api\n └── UsersControllerTest.java\n\n```\n\nТесты Spring Boot расположены в директории *src/test/java/io/hexlet/spring*. Интеграционные тесты фактически повторяют структуру контроллеров, поэтому удобнее всего делать прямое соответствие между структурой контроллеров и тестами. В примере выше мы видим одни и те же директории. Название теста получается из названия контроллера с добавлением *Test* в название файла.\n\nСам тест выглядит так:\n\n```java\npackage io.hexlet.spring.controller.api;\n\nimport static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;\nimport static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;\nimport static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.web.servlet.MockMvc;\n\n@SpringBootTest\n@AutoConfigureMockMvc\npublic class UsersControllerTest {\n\n @Autowired\n private MockMvc mockMvc;\n\n // Технически имя тестового метода не важно\n // Лучше использовать шаблон testНазваниеМетодаКонтроллера для основного сценария\n @Test\n public void testIndex() throws Exception {\n mockMvc.perform(get(\"/api/users\"))\n .andExpect(status().isOk());\n }\n}\n```\n\nФайл тестов — это классический JUnit-класс, в котором тестовые методы помечены аннотациями `@Test`. Все остальное — это уже специфика Spring Boot. Сюда относятся аннотации `@SpringBootTest` и `@AutoConfigureMockMvc`. Во время старта тестов Spring Boot читает эти аннотации, стартует приложение и конфигурирует его в соответствие с аннотациями. Например, нам становится доступным объект `mockMvc`, через который можно выполнять HTTP-запросы к нашему приложению. Разберем по шагам:\n\n* Метод `get(\"/api/users\")` формирует объект запроса к указанной странице. Кроме запроса `get`, мы можем выполнить любой другой запрос\n* Метод `mockMvc.perform()` выполняет сформированный запрос. На самом деле здесь не происходит HTTP-вызова — запрос передается в приложение напрямую, поэтому тесты работают быстрее, чем с реальным веб-сервером\n* Метод `andExpect(status().isOk())` проверяем, что в ответ вернулся ответ *200*. По необходимости можно проверить любой другой статус\n\nПроверка на код ответа считается одной из базовых проверок. Она показывает, что код в целом отработал ожидаемо. При этом мы не можем с уверенностью сказать, что все правильно.\n\nНапример, мы ожидаем, что в теле ответа будет JSON определенной структуры, но вдруг там ничего нет? Для контроля ответа нужно добавить проверку тела ответа. Сделать это можно множеством разных способов и библиотек, мы используем следующие:\n\n```java\ntestImplementation(\"net.javacrumbs.json-unit:json-unit-assertj:3.2.2\")\n```\n\nИспользование выглядит так:\n\n```java\npackage io.hexlet.spring.controller.api;\n\nimport static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;\nimport static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;\nimport static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static org.assertj.core.api.Assertions.assertThat;\n\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.web.servlet.MockMvc;\n\n@SpringBootTest\n@AutoConfigureMockMvc\npublic class UsersControllerTest {\n\n @Autowired\n private MockMvc mockMvc;\n\n // Технически имя тестового метода не важно\n // Лучше использовать шаблон testНазваниеМетодаКонтроллера для основного сценария\n @Test\n public void testIndex() throws Exception {\n var result = mockMvc.perform(get(\"/api/users\"))\n .andExpect(status().isOk())\n .andReturn();\n\n // Тело это строка, в этом случае JSON\n var body = result.getResponse().getContentAsString();\n assertThatJson(body).isArray();\n // Еще проверки\n }\n}\n```\n\nБиблиотека JsonUnit обладает широкими возможностями по проверке того, как устроен JSON. Подробнее с этими возможностями можно ознакомиться в [официальной документации](https://github.com/lukas-krecan/JsonUnit). Изучим несколько примеров:\n\n```java\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;\nimport static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;\n\n// Compares two JSON documents (note lenient parsing of expected value)\nassertThatJson(\"{\\\"a\\\":1, \\\"b\\\":2}\").isEqualTo(\"{b:2, a:1}\");\n\n// Objects are automatically serialized before comparison\nassertThatJson(jsonObject).isEqualTo(\"{\\n\\\"test\\\": 1\\n}\");\n\n// AssertJ map assertions (numbers are converted to BigDecimals)\nassertThatJson(\"{\\\"a\\\":1}\").isObject().containsEntry(\"a\", BigDecimal.valueOf(1));\n```\n\nИ последний шаг — запуск тестов:\n\n```bash\n./gradlew test\n# В этом выводе может быть много дополнительных строчек с логами\nUsersControllerTest > testIndex() PASSED\n\nBUILD SUCCESSFUL in 4s\n```\n\n## Взаимодействие с базой\n\nПример теста списка пользователей не включает в себя одну важную деталь — наполнение базы данных. По умолчанию тесты используют ту базу данных, которая указана в конфигурации. За ее наполнение отвечает программист, а не Spring Boot. Кроме наполнения базы, нам нужна еще и ее очистка.\n\nПредставьте, что мы написали тест, который создает пользователя. Если после теста мы не удалим этого пользователя, то следующий тест может завершиться с ошибкой — он не рассчитывает, что в базе уже есть такие данные. По этой причине в большинстве фреймворков каждый тест выполняется в отдельной транзакции, которая откатывается в конце теста. Таким образом достигается полная изоляция тестов друг от друга.\n\nМожно наполнить базу данных, написав пачку SQL-запросов, но это неудобно и сложно в поддержке, особенно на больших объемах. Было бы удобнее, если бы могли автоматически создавать объекты на базе сущностей и сохранять их в базу. В Java есть специальная библиотека — [Instancio](https://www.instancio.org/).\n\nПосмотрим на работу такого теста на примере запроса, обновляющего пользователя. Для этой операции используем маршрут */api/users/{id}*. Для выполнения запроса нам понадобится идентификатор пользователя, которого мы создадим с помощью библиотеки *Instancio*.\n\nДля начала установим необходимые зависимости:\n\n```java\nimplementation(\"net.datafaker:datafaker:2.0.1\")\nimplementation(\"org.instancio:instancio-junit:3.3.0\")\n```\n\nТеперь посмотрим готовый тест, а затем разберем его:\n\n```java\npackage io.hexlet.spring.controller.api;\n\nimport static org.assertj.core.api.Assertions.assertThat;\nimport static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;\nimport static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;\n\nimport java.util.HashMap;\n\nimport org.instancio.Instancio;\nimport org.instancio.Select;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.http.MediaType;\nimport org.springframework.test.web.servlet.MockMvc;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\nimport io.hexlet.blog.model.User;\nimport io.hexlet.blog.repository.UserRepository;\nimport net.datafaker.Faker;\n\n@SpringBootTest\n@AutoConfigureMockMvc\npublic class UsersControllerTest {\n\n @Autowired\n private MockMvc mockMvc;\n\n @Autowired\n private Faker faker;\n\n @Autowired\n private UserRepository userRepository;\n\n @Autowired\n private ObjectMapper om;\n\n @Test\n public void testUpdate() throws Exception {\n var user = Instancio.of(User.class)\n .ignore(Select.field(User::getId))\n .supply(Select.field(User::getEmail), () -> faker.internet().emailAddress())\n .create();\n userRepository.save(user);\n\n var data = new HashMap<>();\n data.put(\"firstName\", \"Mike\");\n\n var request = put(\"/api/users/\" + user.getId())\n .contentType(MediaType.APPLICATION_JSON)\n // ObjectMapper конвертирует Map в JSON\n .content(om.writeValueAsString(data));\n\n mockMvc.perform(request)\n .andExpect(status().isOk());\n\n user = userRepository.findById(user.getId()).get();\n assertThat(user.getFirstName()).isEqualTo((\"Mike\"));\n }\n}\n```\n\n**Шаг 1**. Сначала мы создаем пользователя. Instancio делает это автоматически, базируясь на полях переданной модели. По умолчанию данные создаются для всех полей, но это не всегда удобно. Во-первых, не нужно заполнять значение для идентификатора, во-вторых, `email` должен быть настоящим, поэтому здесь мы используем кастомизацию и добавляем адрес с помощью Faker:\n\n```java\nvar user = Instancio.of(User.class)\n .ignore(Select.field(User::getId))\n .supply(Select.field(User::getEmail), () -> faker.internet().emailAddress())\n .create();\nuserRepository.save(user);\n```\n\n**Шаг 2**. Затем мы подготавливаем запрос. Сначала формируем объект с данными, затем преобразуем их в JSON и устанавливаем соответствующий заголовок. В самом запросе формируем правильный адрес, подставляя идентификатор созданного пользователя:\n\n```java\nvar data = new HashMap<>();\ndata.put(\"firstName\", \"Mike\");\n\nvar request = put(\"/api/users/\" + user.getId())\n .contentType(MediaType.APPLICATION_JSON)\n // ObjectMapper конвертирует Map в JSON\n .content(om.writeValueAsString(data));\n```\n\n**Шаг 3**. Выполняем запрос и проверяем, что он действительно изменил пользователя в базе данных:\n\n```java\nmockMvc.perform(request)\n .andExpect(status().isOk());\n\nuser = userRepository.findById(user.getId()).get();\nassertThat(user.getFirstName()).isEqualTo((\"Mike\"));\n```\n\nКроме изменения данных в базе, имеет смысл протестировать ответ, который возвращается после запроса.\n\nОбратите внимание на важную деталь, связанную с интеграционными тестами. На протяжении урока мы писали тесты и убеждались, что приложение работает, даже не посмотрев на реализацию самого приложения. В этом и заключается суть интеграционных тестов. Нам не важно, как написано приложение внутри — мы убеждаемся только в том, что оно работает правильно. Из-за этого интеграционные тесты очень устойчивы к изменениям в коде, они меняются в основном из-за изменений API.\n"},"lessonMember":null,"courseMember":null,"course":{"start_lesson":{"exercise":null,"units":[{"id":7158,"name":"theory","url":"/courses/java-spring-boot/lessons/intro/theory_unit"}],"links":[],"ordered_units":[{"id":7158,"name":"theory","url":"/courses/java-spring-boot/lessons/intro/theory_unit"}],"id":3187,"slug":"intro","state":"approved","name":"Введение","course_order":100,"goal":"Знакомимся с целями и задачами курса","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"Spring Boot — это самый популярный фреймворк для создания веб-приложений на Java. С его помощью можно делать полноценные веб-приложения, сервисы для внутреннего использования и многое другое. Для примера посмотрим, как написать программу _Hello World_ на Java со Spring Boot:\n\n```java\n@RestController\n@SpringBootApplication\npublic class MyApplication {\n\n @RequestMapping(\"/\")\n String home() {\n return \"Hello World!\";\n }\n\n public static void main(String[] args) {\n SpringApplication.run(MyApplication.class, args);\n }\n}\n```\n\nSpring Boot обладает высоким уровнем автоматизации: он выполняет многие типовые задачи автоматически или с небольшим уровнем конфигурации, что позволяет легко и быстро написать рабочий проект. С другой стороны, Spring Boot — это целая экосистема со множеством подпроектов, интеграций и подходов, на изучение которых нужно время.\n\nВ этом курсе мы разберем ключевые концепции, которые помогут познакомиться со Spring Boot:\n\n* Аннотации\n* Инверсия зависимостей\n* REST API\n* JPA Data\n* Entity/Repository\n* Data Transfer Objects (DTO)\n* Интеграционное тестирование с помощью MockMVC\n* Аутентификация с использованием JWT\n* Service Layer\n* Профайлы\n"},"id":246,"slug":"java-spring-boot","challenges_count":6,"name":"Spring Boot","allow_indexing":true,"state":"approved","course_state":"finished","pricing_type":"paid","description":"На этом курсе вы изучите Spring Boot – популярное дополнение к фреймворку Spring, которое значительно облегчает создание приложений на Spring. Вы узнаете больше об ORM и миграциях, познакомитесь с аутентификацией и авторизацией — действиями, которые защищают наши данные от доступа посторонних лиц. Вы разберетесь с архитектурой приложения и узнаете об очередях сообщений. В итоге вы научитесь собирать новое приложение с помощью Spring Boot, создавать сущности, сохранять их в базу и организовывать бизнес-логику приложения. Так вы сможете создавать собственные аннотации и тестировать свое приложение. Знания из этого курса позволят вам разрабатывать полноценные крупные проекты производственного класса на Spring.","kind":"advanced","updated_at":"2026-02-24T16:30:07.013Z","language":"java","duration_cache":128580,"skills":["Создавать веб-приложения с помощью Spring Boot","Работать с сущностями","Управлять изменениями базы данных с помощью миграций","Писать интеграционные тесты"],"keywords":[],"lessons_count":28,"cover":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTU1MjYsInB1ciI6ImJsb2JfaWQifX0=--3571fa932b10f212771fc24ce2054798387e8223/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fZmlsbCI6WzYwMCw0MDBdfSwicHVyIjoidmFyaWF0aW9uIn19--6067466c2912ca31a17eddee04b8cf2a38c6ad17/image.png"},"recommendedLandings":[{"stack":{"id":3,"slug":"java","title":"Java-разработчик","audience":"for_beginners","start_type":"weekly","pricing_model":"purchase","priority":"high","kind":"profession","state":"published","stack_state":"finished","order":30,"duration_in_months":10},"id":3,"slug":"java","title":"Java-разработчик","subtitle":"Изучите Java и фреймворк Spring Boot и REST API","subtitle_for_lists":"Изучите Java и фреймворк Spring Boot и REST API","locale":"ru","current":true,"duration_in_months_text":"10 месяцев","stack_slug":"java","price_text":"от 6 792 ₽","duration_text":"10 месяцев","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzczNSwicHVyIjoiYmxvYl9pZCJ9fQ==--883f3fd4e1b571538035b5680c8d4a9eb504b1f6/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Source%20code-amico.png"},{"stack":{"id":132,"slug":"spring-boot","title":"Spring Boot","audience":"for_programmers","start_type":"anytime","pricing_model":"subscription","priority":"medium","kind":"track","state":"published","stack_state":"finished","order":50,"duration_in_months":1},"id":224,"slug":"spring-boot","title":"Spring Boot","subtitle":"Навык создавать масштабируемые веб-приложения на Java с Spring Boot","subtitle_for_lists":"Навык работы с Spring Boot для масштабируемых веб-приложений","locale":"ru","current":true,"duration_in_months_text":"1 месяц","stack_slug":"spring-boot","price_text":"от 3 900 ₽","duration_text":"1 месяц","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzY3NSwicHVyIjoiYmxvYl9pZCJ9fQ==--b3b44cb29727c1bcb0b9aee0c285371dc12aa50f/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Code%20typing-pana.png"},{"stack":{"id":467,"slug":"middle-java","title":"Middle-java разработчик","audience":"for_programmers","start_type":"weekly","pricing_model":"purchase","priority":"high","kind":"profession","state":"published","stack_state":"not_finished","order":2002,"duration_in_months":6},"id":595,"slug":"middle-java","title":"Middle-java разработчик","subtitle":"Углубите знания Java, Spring и архитектуры серверных приложений","subtitle_for_lists":"Углубите знания Java, Spring и архитектуры серверных приложений","locale":"ru","current":true,"duration_in_months_text":"6 месяцев","stack_slug":"middle-java","price_text":"от 4 050 ₽","duration_text":"6 месяцев","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NTIxOSwicHVyIjoiYmxvYl9pZCJ9fQ==--e79a18f1e9f90eae650f1f4f04115cba51f4e650/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Low%20code%20development-amico.png"}],"lessonMemberUnit":null,"accessToLearnUnitExists":false,"accessToCourseExists":false},"url":"/courses/java-spring-boot/lessons/integration-tests/theory_unit","version":"8f286f6358a90a7bef2263b3a6edf5a90a94fa42","encryptHistory":false,"clearHistory":false}"><style data-mantine-styles="true">:root, :host{--mantine-font-family: Arial, sans-serif;--mantine-font-family-headings: Arial, sans-serif;--mantine-heading-font-weight: normal;--mantine-radius-default: 0rem;--mantine-primary-color-filled: var(--mantine-color-indigo-filled);--mantine-primary-color-filled-hover: var(--mantine-color-indigo-filled-hover);--mantine-primary-color-light: var(--mantine-color-indigo-light);--mantine-primary-color-light-hover: var(--mantine-color-indigo-light-hover);--mantine-primary-color-light-color: var(--mantine-color-indigo-light-color);--mantine-spacing-xxl: calc(4rem * var(--mantine-scale));--mantine-font-size-xs: 12px;--mantine-font-size-sm: 14px;--mantine-font-size-md: 16px;--mantine-font-size-lg: clamp(16.0000px, calc(15.2727px + 0.2273vw), 18.0000px);--mantine-font-size-xl: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-display-3: clamp(32.0000px, calc(26.1818px + 1.8182vw), 48.0000px);--mantine-font-size-display-2: clamp(36.0000px, calc(25.8182px + 3.1818vw), 64.0000px);--mantine-font-size-display-1: clamp(40.0000px, calc(25.4545px + 4.5455vw), 80.0000px);--mantine-font-size-h1: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-font-size-h2: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-font-size-h3: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-font-size-h4: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-font-size-h5: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-h6: 1rem;--mantine-primary-color-0: var(--mantine-color-indigo-0);--mantine-primary-color-1: var(--mantine-color-indigo-1);--mantine-primary-color-2: var(--mantine-color-indigo-2);--mantine-primary-color-3: var(--mantine-color-indigo-3);--mantine-primary-color-4: var(--mantine-color-indigo-4);--mantine-primary-color-5: var(--mantine-color-indigo-5);--mantine-primary-color-6: var(--mantine-color-indigo-6);--mantine-primary-color-7: var(--mantine-color-indigo-7);--mantine-primary-color-8: var(--mantine-color-indigo-8);--mantine-primary-color-9: var(--mantine-color-indigo-9);--mantine-color-red-0: #ffeaea;--mantine-color-red-1: #fed4d4;--mantine-color-red-2: #f4a7a8;--mantine-color-red-3: #ec7878;--mantine-color-red-4: #e55050;--mantine-color-red-5: #e03131;--mantine-color-red-6: #e02829;--mantine-color-red-7: #c71a1c;--mantine-color-red-8: #b21218;--mantine-color-red-9: #9c0411;--mantine-color-violet-0: #fce9ff;--mantine-color-violet-1: #f1cfff;--mantine-color-violet-2: #e09bff;--mantine-color-violet-3: #d16fff;--mantine-color-violet-4: #be37fe;--mantine-color-violet-5: #b51afe;--mantine-color-violet-6: #b009ff;--mantine-color-violet-7: #9b00e4;--mantine-color-violet-8: #8a00cc;--mantine-color-violet-9: #7800b3;--mantine-color-indigo-0: #edecff;--mantine-color-indigo-1: #d6d5fe;--mantine-color-indigo-2: #aaa9f4;--mantine-color-indigo-3: #7b79eb;--mantine-color-indigo-4: #5451e4;--mantine-color-indigo-5: #3b37e0;--mantine-color-indigo-6: #2d2adf;--mantine-color-indigo-7: #1f1ec7;--mantine-color-indigo-8: #1819b2;--mantine-color-indigo-9: #0c149e;--mantine-color-cyan-0: #dffdff;--mantine-color-cyan-1: #caf5ff;--mantine-color-cyan-2: #99e8ff;--mantine-color-cyan-3: #64daff;--mantine-color-cyan-4: #3ccffe;--mantine-color-cyan-5: #24c8fe;--mantine-color-cyan-6: #00c2ff;--mantine-color-cyan-7: #00ade4;--mantine-color-cyan-8: #009acd;--mantine-color-cyan-9: #0085b5;--mantine-color-green-0: #e9fdec;--mantine-color-green-1: #d7f6dc;--mantine-color-green-2: #b0eab9;--mantine-color-green-3: #86df94;--mantine-color-green-4: #62d574;--mantine-color-green-5: #4ccf5f;--mantine-color-green-6: #3fcc54;--mantine-color-green-7: #2fb344;--mantine-color-green-8: #25a03b;--mantine-color-green-9: #138a2e;--mantine-color-yellow-0: #fff7e2;--mantine-color-yellow-1: #ffeecd;--mantine-color-yellow-2: #ffdc9c;--mantine-color-yellow-3: #ffc966;--mantine-color-yellow-4: #feb93a;--mantine-color-yellow-5: #feae1e;--mantine-color-yellow-6: #ffa90f;--mantine-color-yellow-8: #ca8200;--mantine-color-yellow-9: #af7000;--mantine-h1-font-size: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-h1-font-weight: normal;--mantine-h2-font-size: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-h2-font-weight: normal;--mantine-h3-font-size: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-h3-font-weight: normal;--mantine-h4-font-size: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-h4-font-weight: normal;--mantine-h5-font-size: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-h5-font-weight: normal;--mantine-h6-font-size: 1rem;--mantine-h6-font-weight: normal;}
:root[data-mantine-color-scheme="dark"], :host([data-mantine-color-scheme="dark"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-dark-filled: var(--mantine-color-dark-5);--mantine-color-dark-filled-hover: var(--mantine-color-dark-6);--mantine-color-dark-light: rgba(105, 105, 105, 0.15);--mantine-color-dark-light-hover: rgba(105, 105, 105, 0.2);--mantine-color-dark-light-color: var(--mantine-color-dark-0);--mantine-color-dark-outline: var(--mantine-color-dark-1);--mantine-color-dark-outline-hover: rgba(184, 184, 184, 0.05);--mantine-color-gray-filled: var(--mantine-color-gray-5);--mantine-color-gray-filled-hover: var(--mantine-color-gray-6);--mantine-color-gray-light: rgba(222, 226, 230, 0.15);--mantine-color-gray-light-hover: rgba(222, 226, 230, 0.2);--mantine-color-gray-light-color: var(--mantine-color-gray-0);--mantine-color-gray-outline: var(--mantine-color-gray-1);--mantine-color-gray-outline-hover: rgba(241, 243, 245, 0.05);--mantine-color-red-filled: var(--mantine-color-red-5);--mantine-color-red-filled-hover: var(--mantine-color-red-6);--mantine-color-red-light: rgba(236, 120, 120, 0.15);--mantine-color-red-light-hover: rgba(236, 120, 120, 0.2);--mantine-color-red-light-color: var(--mantine-color-red-0);--mantine-color-red-outline: var(--mantine-color-red-1);--mantine-color-red-outline-hover: rgba(254, 212, 212, 0.05);--mantine-color-pink-filled: var(--mantine-color-pink-5);--mantine-color-pink-filled-hover: var(--mantine-color-pink-6);--mantine-color-pink-light: rgba(250, 162, 193, 0.15);--mantine-color-pink-light-hover: rgba(250, 162, 193, 0.2);--mantine-color-pink-light-color: var(--mantine-color-pink-0);--mantine-color-pink-outline: var(--mantine-color-pink-1);--mantine-color-pink-outline-hover: rgba(255, 222, 235, 0.05);--mantine-color-grape-filled: var(--mantine-color-grape-5);--mantine-color-grape-filled-hover: var(--mantine-color-grape-6);--mantine-color-grape-light: rgba(229, 153, 247, 0.15);--mantine-color-grape-light-hover: rgba(229, 153, 247, 0.2);--mantine-color-grape-light-color: var(--mantine-color-grape-0);--mantine-color-grape-outline: var(--mantine-color-grape-1);--mantine-color-grape-outline-hover: rgba(243, 217, 250, 0.05);--mantine-color-violet-filled: var(--mantine-color-violet-5);--mantine-color-violet-filled-hover: var(--mantine-color-violet-6);--mantine-color-violet-light: rgba(209, 111, 255, 0.15);--mantine-color-violet-light-hover: rgba(209, 111, 255, 0.2);--mantine-color-violet-light-color: var(--mantine-color-violet-0);--mantine-color-violet-outline: var(--mantine-color-violet-1);--mantine-color-violet-outline-hover: rgba(241, 207, 255, 0.05);--mantine-color-indigo-filled: var(--mantine-color-indigo-5);--mantine-color-indigo-filled-hover: var(--mantine-color-indigo-6);--mantine-color-indigo-light: rgba(123, 121, 235, 0.15);--mantine-color-indigo-light-hover: rgba(123, 121, 235, 0.2);--mantine-color-indigo-light-color: var(--mantine-color-indigo-0);--mantine-color-indigo-outline: var(--mantine-color-indigo-1);--mantine-color-indigo-outline-hover: rgba(214, 213, 254, 0.05);--mantine-color-blue-filled: var(--mantine-color-blue-5);--mantine-color-blue-filled-hover: var(--mantine-color-blue-6);--mantine-color-blue-light: rgba(116, 192, 252, 0.15);--mantine-color-blue-light-hover: rgba(116, 192, 252, 0.2);--mantine-color-blue-light-color: var(--mantine-color-blue-0);--mantine-color-blue-outline: var(--mantine-color-blue-1);--mantine-color-blue-outline-hover: rgba(208, 235, 255, 0.05);--mantine-color-cyan-filled: var(--mantine-color-cyan-5);--mantine-color-cyan-filled-hover: var(--mantine-color-cyan-6);--mantine-color-cyan-light: rgba(100, 218, 255, 0.15);--mantine-color-cyan-light-hover: rgba(100, 218, 255, 0.2);--mantine-color-cyan-light-color: var(--mantine-color-cyan-0);--mantine-color-cyan-outline: var(--mantine-color-cyan-1);--mantine-color-cyan-outline-hover: rgba(202, 245, 255, 0.05);--mantine-color-teal-filled: var(--mantine-color-teal-5);--mantine-color-teal-filled-hover: var(--mantine-color-teal-6);--mantine-color-teal-light: rgba(99, 230, 190, 0.15);--mantine-color-teal-light-hover: rgba(99, 230, 190, 0.2);--mantine-color-teal-light-color: var(--mantine-color-teal-0);--mantine-color-teal-outline: var(--mantine-color-teal-1);--mantine-color-teal-outline-hover: rgba(195, 250, 232, 0.05);--mantine-color-green-filled: var(--mantine-color-green-5);--mantine-color-green-filled-hover: var(--mantine-color-green-6);--mantine-color-green-light: rgba(134, 223, 148, 0.15);--mantine-color-green-light-hover: rgba(134, 223, 148, 0.2);--mantine-color-green-light-color: var(--mantine-color-green-0);--mantine-color-green-outline: var(--mantine-color-green-1);--mantine-color-green-outline-hover: rgba(215, 246, 220, 0.05);--mantine-color-lime-filled: var(--mantine-color-lime-5);--mantine-color-lime-filled-hover: var(--mantine-color-lime-6);--mantine-color-lime-light: rgba(192, 235, 117, 0.15);--mantine-color-lime-light-hover: rgba(192, 235, 117, 0.2);--mantine-color-lime-light-color: var(--mantine-color-lime-0);--mantine-color-lime-outline: var(--mantine-color-lime-1);--mantine-color-lime-outline-hover: rgba(233, 250, 200, 0.05);--mantine-color-yellow-filled: var(--mantine-color-yellow-5);--mantine-color-yellow-filled-hover: var(--mantine-color-yellow-6);--mantine-color-yellow-light: rgba(255, 201, 102, 0.15);--mantine-color-yellow-light-hover: rgba(255, 201, 102, 0.2);--mantine-color-yellow-light-color: var(--mantine-color-yellow-0);--mantine-color-yellow-outline: var(--mantine-color-yellow-1);--mantine-color-yellow-outline-hover: rgba(255, 238, 205, 0.05);--mantine-color-orange-filled: var(--mantine-color-orange-5);--mantine-color-orange-filled-hover: var(--mantine-color-orange-6);--mantine-color-orange-light: rgba(255, 192, 120, 0.15);--mantine-color-orange-light-hover: rgba(255, 192, 120, 0.2);--mantine-color-orange-light-color: var(--mantine-color-orange-0);--mantine-color-orange-outline: var(--mantine-color-orange-1);--mantine-color-orange-outline-hover: rgba(255, 232, 204, 0.05);--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-9) 0%, var(--mantine-color-cyan-7) 100%);--app-color-surface: #2e2e2e;}
:root[data-mantine-color-scheme="light"], :host([data-mantine-color-scheme="light"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-red-light: rgba(224, 40, 41, 0.1);--mantine-color-red-light-hover: rgba(224, 40, 41, 0.12);--mantine-color-red-outline-hover: rgba(224, 40, 41, 0.05);--mantine-color-violet-light: rgba(176, 9, 255, 0.1);--mantine-color-violet-light-hover: rgba(176, 9, 255, 0.12);--mantine-color-violet-outline-hover: rgba(176, 9, 255, 0.05);--mantine-color-indigo-light: rgba(45, 42, 223, 0.1);--mantine-color-indigo-light-hover: rgba(45, 42, 223, 0.12);--mantine-color-indigo-outline-hover: rgba(45, 42, 223, 0.05);--mantine-color-cyan-light: rgba(0, 194, 255, 0.1);--mantine-color-cyan-light-hover: rgba(0, 194, 255, 0.12);--mantine-color-cyan-outline-hover: rgba(0, 194, 255, 0.05);--mantine-color-green-light: rgba(63, 204, 84, 0.1);--mantine-color-green-light-hover: rgba(63, 204, 84, 0.12);--mantine-color-green-outline-hover: rgba(63, 204, 84, 0.05);--mantine-color-yellow-light: rgba(255, 169, 15, 0.1);--mantine-color-yellow-light-hover: rgba(255, 169, 15, 0.12);--mantine-color-yellow-outline-hover: rgba(255, 169, 15, 0.05);--app-color-surface: #f1f3f5;--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-5) 100%);}</style><style data-mantine-styles="classes">@media (max-width: 35.99375em) {.mantine-visible-from-xs {display: none !important;}}@media (min-width: 36em) {.mantine-hidden-from-xs {display: none !important;}}@media (max-width: 47.99375em) {.mantine-visible-from-sm {display: none !important;}}@media (min-width: 48em) {.mantine-hidden-from-sm {display: none !important;}}@media (max-width: 61.99375em) {.mantine-visible-from-md {display: none !important;}}@media (min-width: 62em) {.mantine-hidden-from-md {display: none !important;}}@media (max-width: 74.99375em) {.mantine-visible-from-lg {display: none !important;}}@media (min-width: 75em) {.mantine-hidden-from-lg {display: none !important;}}@media (max-width: 87.99375em) {.mantine-visible-from-xl {display: none !important;}}@media (min-width: 88em) {.mantine-hidden-from-xl {display: none !important;}}</style><div style="position:absolute;top:0rem" class=""></div><div style="max-width:var(--container-size-xl);height:100%;min-height:0rem" class=""><style data-mantine-styles="inline">.__m__-_R_5ub_{--grid-gutter:0rem;}</style><div style="height:100%;min-height:0rem" class="m_410352e9 mantine-Grid-root __m__-_R_5ub_"><div class="m_dee7bd2f mantine-Grid-inner" style="height:100%"><style data-mantine-styles="inline">.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:91.66666666666667%;--col-max-width:91.66666666666667%;}@media(min-width: 48em){.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:83.33333333333334%;--col-max-width:83.33333333333334%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem;display:flex" class="m_96bdd299 mantine-Grid-col __m__-_R_rdub_"><style data-mantine-styles="inline">.__m__-_R_6qrdub_{margin-top:0rem;padding-inline:var(--mantine-spacing-xs);width:100%;}@media(min-width: 48em){.__m__-_R_6qrdub_{margin-top:var(--mantine-spacing-xl);width:80%;}}@media(min-width: 62em){.__m__-_R_6qrdub_{padding-inline:var(--mantine-spacing-xl);}}</style><div style="margin-inline:auto;max-width:var(--mantine-breakpoint-xl)" class="__m__-_R_6qrdub_"><div style="color:var(--mantine-color-dimmed)" class="m_4451eb3a mantine-Center-root" data-inline="true"><div style="--ti-size:var(--ti-size-xs);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;margin-inline-end:calc(0.125rem * var(--mantine-scale));color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="xs"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-lock "><path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6"></path><path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path><path d="M8 11v-4a4 4 0 1 1 8 0v4"></path></svg></div><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Spring Boot</p></div><h1 style="--title-fw:var(--mantine-h1-font-weight);--title-lh:var(--mantine-h1-line-height);--title-fz:var(--mantine-h1-font-size);margin-bottom:var(--mantine-spacing-xl)" class="m_8a5d1357 mantine-Title-root" data-order="1">Теория: Интеграционные тесты</h1><script type="application/ld+json">{"@context":"https://schema.org","@type":"LearningResource","name":"Интеграционные тесты","inLanguage":"ru","isPartOf":{"@type":"LearningResource","name":"Spring Boot"},"isAccessibleForFree":"False","hasPart":{"@type":"WebPageElement","isAccessibleForFree":"False","cssSelector":".paywalled"}}</script><div class=""><div style="--alert-color:var(--mantine-color-indigo-light-color);margin-bottom:var(--mantine-spacing-lg);font-size:var(--mantine-font-size-lg)" class="m_66836ed3 mantine-Alert-root" id="mantine-_R_remqrdub_" role="alert" aria-describedby="mantine-_R_remqrdub_-body" aria-labelledby="mantine-_R_remqrdub_-title"><div class="m_a5d60502 mantine-Alert-wrapper"><div class="m_667f2a6a mantine-Alert-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-rocket "><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3"></path><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3"></path><path d="M14 9a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path></svg></div><div class="m_667c2793 mantine-Alert-body"><div class="m_6a03f287 mantine-Alert-title"><span id="mantine-_R_remqrdub_-title" class="m_698f4f23 mantine-Alert-label">Полный доступ к материалам</span></div><div id="mantine-_R_remqrdub_-body" class="m_7fa78076 mantine-Alert-message"><div style="--group-gap:var(--mantine-spacing-md);--group-align:center;--group-justify:space-between;--group-wrap:wrap" class="m_4081bf90 mantine-Group-root"><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Зарегистрируйтесь и получите доступ к этому и десяткам других курсов</p><a style="--button-height:var(--button-height-xs);--button-padding-x:var(--button-padding-x-xs);--button-fz:var(--mantine-font-size-xs);--button-bg:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-hover:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-color:var(--mantine-color-white);--button-bd:none" class="mantine-focus-auto mantine-active m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root" data-variant="gradient" data-size="xs" href="/u/new"><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">Зарегистрироваться</span></span></a></div></div></div></div></div><div class="paywalled m_d08caa0 mantine-Typography-root"><p>Тестирование приложений на Spring Boot — неотъемлемая часть профессиональной жизни веб-разработчиков на Java. Сюда входит написание различных тестов:</p>
<ul>
<li>Юнит-тестов для отдельных модулей</li>
<li>Интеграционных тестов, проверяющих работоспособность всего приложения</li>
</ul>
<p>В этом уроке мы научимся создавать интеграционные тесты для наших веб-приложений на Spring Boot.</p>
<p>Интеграционное тестирование веб-приложений устроено сложнее, чем тестирование библиотечного кода, где мы вызываем какие-то методы и смотрим на результат.</p>
<p>Веб-приложения работают по сети, обрабатывая HTTP-запросы. Такое поведение придется повторять прямо в тестах или как-то имитировать. Spring Boot позволяет использовать оба подхода. Мы остановимся на подходе с подменой веб-сервера, чтобы ускорить запуск и выполнение тестов. В остальном эти тесты проверяют работу приложения от запроса до ответа, что дает очень высокую степень уверенности в том, что приложение работает.</p>
<p>Для работы с тестами нужно установить зависимости:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testImplementation("org.springframework.boot:spring-boot-starter-test")
// Понадобится когда мы начнем работать с аутентификацией
testImplementation("org.springframework.security:spring-security-test")</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Кроме классического Junit, здесь мы видим пакеты, специфичные для Spring Boot. Они дают все необходимые инструменты, чтобы мы могли писать тесты легко и эффективно.</p>
<h2 id="heading-2-1">Первый тест</h2>
<p>Интеграционные тесты в Spring Boot связаны с маршрутами. Каждый тест — это запрос на конкретный адрес для тестирования конкретного маршрута. Количество тестов для одного маршрута может быть разным, но конкретный тест — это всегда запрос-ответ.</p>
<p>Начнем с примера. Предположим, что у нас есть маршрут <em>/api/users</em>, который возвращает список пользователей. Тест на такой маршрут должен выполнить запрос на этот адрес. Вот как будет выглядеть структура файлов в этом случае:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">tree src
src
├── main
│ ├── java
│ │ └── io
│ │ └── hexlet
│ │ └── spring
│ │ ├── Application.java
│ │ ├── controller
│ │ │ └── api
│ │ │ └── UsersController.java
│ │ ├── model
│ │ │ └── User.java
│ │ ├── repository
│ │ │ └── UserRepository.java
│ └── resources
│ ├── application.yml
└── test
└── java
└── io
└── hexlet
└── spring
└── controller
└── api
└── UsersControllerTest.java</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Тесты Spring Boot расположены в директории <em>src/test/java/io/hexlet/spring</em>. Интеграционные тесты фактически повторяют структуру контроллеров, поэтому удобнее всего делать прямое соответствие между структурой контроллеров и тестами. В примере выше мы видим одни и те же директории. Название теста получается из названия контроллера с добавлением <em>Test</em> в название файла.</p>
<p>Сам тест выглядит так:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">package io.hexlet.spring.controller.api;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest
@AutoConfigureMockMvc
public class UsersControllerTest {
@Autowired
private MockMvc mockMvc;
// Технически имя тестового метода не важно
// Лучше использовать шаблон testНазваниеМетодаКонтроллера для основного сценария
@Test
public void testIndex() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk());
}
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Файл тестов — это классический JUnit-класс, в котором тестовые методы помечены аннотациями <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">@Test</code>. Все остальное — это уже специфика Spring Boot. Сюда относятся аннотации <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">@SpringBootTest</code> и <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">@AutoConfigureMockMvc</code>. Во время старта тестов Spring Boot читает эти аннотации, стартует приложение и конфигурирует его в соответствие с аннотациями. Например, нам становится доступным объект <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">mockMvc</code>, через который можно выполнять HTTP-запросы к нашему приложению. Разберем по шагам:</p>
<ul>
<li>Метод <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">get("/api/users")</code> формирует объект запроса к указанной странице. Кроме запроса <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">get</code>, мы можем выполнить любой другой запрос</li>
<li>Метод <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">mockMvc.perform()</code> выполняет сформированный запрос. На самом деле здесь не происходит HTTP-вызова — запрос передается в приложение напрямую, поэтому тесты работают быстрее, чем с реальным веб-сервером</li>
<li>Метод <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">andExpect(status().isOk())</code> проверяем, что в ответ вернулся ответ <em>200</em>. По необходимости можно проверить любой другой статус</li>
</ul>
<p>Проверка на код ответа считается одной из базовых проверок. Она показывает, что код в целом отработал ожидаемо. При этом мы не можем с уверенностью сказать, что все правильно.</p>
<p>Например, мы ожидаем, что в теле ответа будет JSON определенной структуры, но вдруг там ничего нет? Для контроля ответа нужно добавить проверку тела ответа. Сделать это можно множеством разных способов и библиотек, мы используем следующие:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">testImplementation("net.javacrumbs.json-unit:json-unit-assertj:3.2.2")</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Использование выглядит так:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">package io.hexlet.spring.controller.api;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest
@AutoConfigureMockMvc
public class UsersControllerTest {
@Autowired
private MockMvc mockMvc;
// Технически имя тестового метода не важно
// Лучше использовать шаблон testНазваниеМетодаКонтроллера для основного сценария
@Test
public void testIndex() throws Exception {
var result = mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andReturn();
// Тело это строка, в этом случае JSON
var body = result.getResponse().getContentAsString();
assertThatJson(body).isArray();
// Еще проверки
}
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Библиотека JsonUnit обладает широкими возможностями по проверке того, как устроен JSON. Подробнее с этими возможностями можно ознакомиться в <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://github.com/lukas-krecan/JsonUnit" rel="noopener noreferrer" target="_blank">официальной документации</a>. Изучим несколько примеров:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
// Compares two JSON documents (note lenient parsing of expected value)
assertThatJson("{\"a\":1, \"b\":2}").isEqualTo("{b:2, a:1}");
// Objects are automatically serialized before comparison
assertThatJson(jsonObject).isEqualTo("{\n\"test\": 1\n}");
// AssertJ map assertions (numbers are converted to BigDecimals)
assertThatJson("{\"a\":1}").isObject().containsEntry("a", BigDecimal.valueOf(1));</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>И последний шаг — запуск тестов:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">./gradlew test
# В этом выводе может быть много дополнительных строчек с логами
UsersControllerTest > testIndex() PASSED
BUILD SUCCESSFUL in 4s</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<h2 id="heading-2-2">Взаимодействие с базой</h2>
<p>Пример теста списка пользователей не включает в себя одну важную деталь — наполнение базы данных. По умолчанию тесты используют ту базу данных, которая указана в конфигурации. За ее наполнение отвечает программист, а не Spring Boot. Кроме наполнения базы, нам нужна еще и ее очистка.</p>
<p>Представьте, что мы написали тест, который создает пользователя. Если после теста мы не удалим этого пользователя, то следующий тест может завершиться с ошибкой — он не рассчитывает, что в базе уже есть такие данные. По этой причине в большинстве фреймворков каждый тест выполняется в отдельной транзакции, которая откатывается в конце теста. Таким образом достигается полная изоляция тестов друг от друга.</p>
<p>Можно наполнить базу данных, написав пачку SQL-запросов, но это неудобно и сложно в поддержке, особенно на больших объемах. Было бы удобнее, если бы могли автоматически создавать объекты на базе сущностей и сохранять их в базу. В Java есть специальная библиотека — <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://www.instancio.org/" rel="noopener noreferrer" target="_blank">Instancio</a>.</p>
<p>Посмотрим на работу такого теста на примере запроса, обновляющего пользователя. Для этой операции используем маршрут <em>/api/users/{id}</em>. Для выполнения запроса нам понадобится идентификатор пользователя, которого мы создадим с помощью библиотеки <em>Instancio</em>.</p>
<p>Для начала установим необходимые зависимости:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">implementation("net.datafaker:datafaker:2.0.1")
implementation("org.instancio:instancio-junit:3.3.0")</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Теперь посмотрим готовый тест, а затем разберем его:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">package io.hexlet.spring.controller.api;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.HashMap;
import org.instancio.Instancio;
import org.instancio.Select;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.hexlet.blog.model.User;
import io.hexlet.blog.repository.UserRepository;
import net.datafaker.Faker;
@SpringBootTest
@AutoConfigureMockMvc
public class UsersControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private Faker faker;
@Autowired
private UserRepository userRepository;
@Autowired
private ObjectMapper om;
@Test
public void testUpdate() throws Exception {
var user = Instancio.of(User.class)
.ignore(Select.field(User::getId))
.supply(Select.field(User::getEmail), () -> faker.internet().emailAddress())
.create();
userRepository.save(user);
var data = new HashMap<>();
data.put("firstName", "Mike");
var request = put("/api/users/" + user.getId())
.contentType(MediaType.APPLICATION_JSON)
// ObjectMapper конвертирует Map в JSON
.content(om.writeValueAsString(data));
mockMvc.perform(request)
.andExpect(status().isOk());
user = userRepository.findById(user.getId()).get();
assertThat(user.getFirstName()).isEqualTo(("Mike"));
}
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p><strong>Шаг 1</strong>. Сначала мы создаем пользователя. Instancio делает это автоматически, базируясь на полях переданной модели. По умолчанию данные создаются для всех полей, но это не всегда удобно. Во-первых, не нужно заполнять значение для идентификатора, во-вторых, <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">email</code> должен быть настоящим, поэтому здесь мы используем кастомизацию и добавляем адрес с помощью Faker:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">var user = Instancio.of(User.class)
.ignore(Select.field(User::getId))
.supply(Select.field(User::getEmail), () -> faker.internet().emailAddress())
.create();
userRepository.save(user);</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p><strong>Шаг 2</strong>. Затем мы подготавливаем запрос. Сначала формируем объект с данными, затем преобразуем их в JSON и устанавливаем соответствующий заголовок. В самом запросе формируем правильный адрес, подставляя идентификатор созданного пользователя:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">var data = new HashMap<>();
data.put("firstName", "Mike");
var request = put("/api/users/" + user.getId())
.contentType(MediaType.APPLICATION_JSON)
// ObjectMapper конвертирует Map в JSON
.content(om.writeValueAsString(data));</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p><strong>Шаг 3</strong>. Выполняем запрос и проверяем, что он действительно изменил пользователя в базе данных:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">mockMvc.perform(request)
.andExpect(status().isOk());
user = userRepository.findById(user.getId()).get();
assertThat(user.getFirstName()).isEqualTo(("Mike"));</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Кроме изменения данных в базе, имеет смысл протестировать ответ, который возвращается после запроса.</p>
<p>Обратите внимание на важную деталь, связанную с интеграционными тестами. На протяжении урока мы писали тесты и убеждались, что приложение работает, даже не посмотрев на реализацию самого приложения. В этом и заключается суть интеграционных тестов. Нам не важно, как написано приложение внутри — мы убеждаемся только в том, что оно работает правильно. Из-за этого интеграционные тесты очень устойчивы к изменениям в коде, они меняются в основном из-за изменений API.</p></div><div style="margin-block:var(--mantine-spacing-xl)" class=""><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md)" class="m_8a5d1357 mantine-Title-root" data-order="2">Рекомендуемые программы</h2><style data-mantine-styles="inline">.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xs);--carousel-slide-size:70%;}@media(min-width: 36em){.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xl);--carousel-slide-size:50%;}}</style><div style="--carousel-control-size:calc(2.5rem * var(--mantine-scale));--carousel-controls-offset:var(--mantine-spacing-sm);margin-bottom:var(--mantine-spacing-lg);padding-block:var(--mantine-spacing-sm);background:var(--app-color-surface)" class="m_17884d0f mantine-Carousel-root responsiveClassName" data-orientation="horizontal" data-include-gap-in-size="true"><div class="m_39bc3463 mantine-Carousel-controls" data-orientation="horizontal"><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="previous" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="next" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(-90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button></div><div class="m_a2dae653 mantine-Carousel-viewport" data-type="media"><div class="m_fcd81474 mantine-Carousel-container __m__-_R_2mremqrdub_" data-orientation="horizontal"><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/java?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">10 месяцев</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">С нуля</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Java-разработчик</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Изучите Java и фреймворк Spring Boot и REST API</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzczNSwicHVyIjoiYmxvYl9pZCJ9fQ==--883f3fd4e1b571538035b5680c8d4a9eb504b1f6/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Source%20code-amico.png" alt="Java-разработчик" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 6 792 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/spring-boot?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">1 месяц</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Для продвинутых</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Spring Boot</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Навык работы с Spring Boot для масштабируемых веб-приложений</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MzY3NSwicHVyIjoiYmxvYl9pZCJ9fQ==--b3b44cb29727c1bcb0b9aee0c285371dc12aa50f/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Code%20typing-pana.png" alt="Spring Boot" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 3 900 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/middle-java?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">6 месяцев</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Для продвинутых</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Middle-java разработчик</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Углубите знания Java, Spring и архитектуры серверных приложений</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NTIxOSwicHVyIjoiYmxvYl9pZCJ9fQ==--e79a18f1e9f90eae650f1f4f04115cba51f4e650/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Low%20code%20development-amico.png" alt="Middle-java разработчик" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 4 050 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md);font-size:var(--mantine-font-size-h3)" class="m_8a5d1357 mantine-Title-root" data-order="2" data-responsive="true">Каталог</h2><p style="margin-bottom:auto" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Полный список доступных курсов по разным направлениям</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="/vite/assets/development-BVihs_d5.png" alt="Orientation"/></div></div></div></a></div></div></div></div></div></div></div></div></div><style data-mantine-styles="inline">.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:8.333333333333334%;--col-max-width:8.333333333333334%;}@media(min-width: 48em){.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:16.666666666666668%;--col-max-width:16.666666666666668%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem" class="m_96bdd299 mantine-Grid-col __m__-_R_1bdub_"><div style="margin-inline:var(--mantine-spacing-xs)" class="mantine-visible-from-sm"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-lg);text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/java-spring-boot/lessons/integration-tests/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label"><span style="margin-inline-end:var(--mantine-spacing-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Дальше</span>→</span></span></a><a style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Навигация по теме</span><span class="m_57492dcc mantine-NavLink-description">Теория</span></div><span class="m_690090b5 mantine-NavLink-section" data-position="right"></span></a><div style="margin-block:var(--mantine-spacing-lg)" class="m_3eebeb36 mantine-Divider-root" data-orientation="horizontal" role="separator"></div><div style="margin-block:var(--mantine-spacing-lg)" class=""><div style="justify-content:space-between;margin-bottom:calc(0.1875rem * var(--mantine-scale));color:var(--mantine-color-dimmed);font-size:var(--mantine-font-size-xs)" class="m_8bffd616 mantine-Flex-root __m__-_R_qimrbdub_"><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Завершено</p><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">0 / 28</p></div><div style="--progress-size:var(--progress-size-sm)" class="m_db6d6462 mantine-Progress-root" data-size="sm"><div style="--progress-section-size:0%;--progress-section-color:var(--mantine-color-gray-filled)" class="m_2242eb65 mantine-Progress-section" role="progressbar" aria-valuemax="100" aria-valuemin="0" aria-valuenow="0" aria-valuetext="0%"></div></div></div><button style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root" type="button"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Обсуждения (архив)</span><span class="m_57492dcc mantine-NavLink-description"></span></div></button><div style="--toc-bg:var(--mantine-color-blue-light);--toc-color:var(--mantine-color-blue-light-color);--toc-size:var(--mantine-font-size-sm);--toc-radius:var(--mantine-radius-sm);margin-top:var(--mantine-spacing-xl)" class="m_bcaa9990 mantine-TableOfContents-root" data-variant="light" data-size="sm"></div></div><div class="mantine-hidden-from-sm"><div style="--stack-gap:0rem;--stack-align:stretch;--stack-justify:flex-start" class="m_6d731127 mantine-Stack-root"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-xs);padding:0rem;text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/java-spring-boot/lessons/integration-tests/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">→</span></span></a><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" data-disabled="true" type="button" disabled=""><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></span></button><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto mantine-active m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" type="button"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-message "><path d="M8 9h8"></path><path d="M8 13h6"></path><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12"></path></svg></span></button></div></div></div></div></div></div></div>
</main>
<footer class="bg-dark fw-light text-light px-3 py-5">
<div class="row small">
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 mb-3">Хекслет</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/about">О нас</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/testimonials">Отзывы</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://b2b.hexlet.io" role="button">Корпоративное обучение</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/blog">Блог</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/qna">Вопросы и ответы</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/glossary">Глоссарий</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://help.hexlet.io" data-target="_blank" role="button">Справка</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" target="_blank" rel="noopener noreferrer" href="/map">Карта сайта</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 fw-normal mb-3">Направления</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_devops">DevOps
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_data_analytics">Аналитика
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_backend_development">Бэкенд
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_programming">Программирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_testing">Тестирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_front_end_dev">Фронтенд
</a></li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Профессии</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/go">Go-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/java">Java-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python">Python-разработчик </a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/data-analytics">Аналитик данных</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/qa-engineer">Инженер по ручному тестированию</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php">РНР-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/frontend">Фронтенд-разработчик</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Навыки</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python-django-developer">Django</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/docker">Docker</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php-laravel-developer">Laravel</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/postman">Postman</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-react-developer">React</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-rest-api">REST API в Node.js</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/spring-boot">Spring Boot</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/typescript">Typescript</a>
</li>
</ul>
</div>
</div>
<hr>
<div class="row">
<div class="col-12 col-sm-4 col-md-2">
<div class="fs-4">
<ul class="list-unstyled d-flex">
<li class="me-3">
<a aria-label="Telegram" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://t.me/hexlet_ru"><span class="bi bi-telegram"></span>
</a></li>
<li>
<a aria-label="Youtube" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://www.youtube.com/user/HexletUniversity"><span class="bi bi-youtube"></span>
</a></li>
</ul>
</div>
<div class="mb-2 d-flex flex-column">
<a class="link-light text-decoration-none" rel="nofollow" href="mailto:support@hexlet.io">support@hexlet.io</a>
<a class="link-light text-decoration-none py-2" target="_blank" href="https://t.me/hexlet_help_bot">t.me/hexlet_help_bot</a>
</div>
<ul class="list-unstyled d-flex">
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://hexlet.io/locale/switch?new_locale=en" data-target="_self" role="button"><span class="my-auto">EN</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 opacity-100 external-link" rel="nofollow" data-href="https://ru.hexlet.io/locale/switch?new_locale=ru" data-target="_self" role="button"><span class="my-auto">RU</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://kz.hexlet.io/locale/switch?new_locale=kz" data-target="_self" role="button"><span class="my-auto">KZ</span>
</span></li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<ul class="list-unstyled fs-4">
<li class="mb-3">
<a class="link-light text-decoration-none" href="tel:8%20800%20100%2022%2047">8 800 100 22 47</a>
<span class="d-block opacity-50 small">бесплатно по РФ</span>
</li>
<li>
<a class="link-light text-decoration-none" href="tel:%2B7%20495%20085%2021%2062">+7 495 085 21 62</a>
<span class="d-block opacity-50 small">бесплатно по Москве</span>
</li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<div class="small mb-3">Образовательные услуги оказываются на основании Л035-01298-77/01989008 от 14.03.2025</div>
<ul class="list-unstyled small">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/legal">Правовая информация</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/offer">Оферта</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/license">Лицензия</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/contacts">Контакты</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-12 col-md-4 small">
<div class="mb-2">
<div>ООО «<a href="/" class="text-decoration-none link-light">Хекслет Рус</a>»</div>
<div>108813 г. Москва, вн.тер.г. поселение Московский,</div>
<div>г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3</div>
<div>ОГРН 1217300010476</div>
<div>ИНН 7325174845</div>
</div>
<hr>
<div>АНО ДПО «<a href="/" class="text-decoration-none link-light">Учебный центр «Хекслет</a>»</div>
<div>119331 г. Москва, вн. тер. г. муниципальный округ</div>
<div>Ломоносовский, пр-кт Вернадского, д. 29</div>
<div>ОГРН 1247700712390</div>
<div>ИНН 7736364948</div>
</div>
</div>
</footer>
<div id="root-assistant-offcanvas"></div>
<script src="/vite/assets/assistant-Bukl1lYy.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-BrRXra1y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/MarkdownBlock-DbyKWoR_.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/shiki-V011pkdv.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-XR8Qr8kR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dist-GCHh59xr.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useIsomorphicEffect-HJ6VK0D3.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-KSp6QbZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/classnames-l6ipYlLR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/debounce-jMQ_Cf4f.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v67327c56f0bb4ef8b305cae61679db8f1769101564043" integrity="sha512-rdcWY47ByXd76cbCFzznIcEaCN71jqkWBBqlwhF1SY7KubdLKZiEGeP7AyieKZlGP9hbY/MhGrwXzJC/HulNyg==" data-cf-beacon='{"version":"2024.11.0","token":"d11015b65d11429ea6b4a2ef37dd7e0b","server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>
</html>