[{"data":1,"prerenderedAt":18488},["ShallowReactive",2],{"navigation_docs":3,"-java-pr2-integration-testing-h2":2949,"-java-pr2-integration-testing-h2-surround":18483},[4,1640,1765,2219,2352,2559,2641,2691,2748,2782,2908,2945],{"title":5,"icon":6,"path":7,"stem":8,"children":9},"C#","i-devicon-csharp","/csharp","01.csharp",[10,13,60,90,120,202,219,253,379,404,457,650,1346,1636],{"title":11,"path":7,"stem":12},"C# Roadmap","01.csharp/index",{"title":14,"icon":15,"path":16,"stem":17,"children":18,"page":59},"Fundamentals","i-lucide-book-open","/csharp/fundamentals","01.csharp/01.fundamentals",[19,23,27,31,35,39,43,47,51,55],{"title":20,"path":21,"stem":22},"Вступ до екосистеми .NET","/csharp/fundamentals/introduction-to-ecosystem","01.csharp/01.fundamentals/01.introduction-to-ecosystem",{"title":24,"path":25,"stem":26},"Структура програми на C#","/csharp/fundamentals/program-structure","01.csharp/01.fundamentals/02.program-structure",{"title":28,"path":29,"stem":30},"Змінні та Типи Даних","/csharp/fundamentals/variables-data-types","01.csharp/01.fundamentals/03.variables-data-types",{"title":32,"path":33,"stem":34},"Масиви","/csharp/fundamentals/arrays","01.csharp/01.fundamentals/04.arrays",{"title":36,"path":37,"stem":38},"Strings & Text Handling","/csharp/fundamentals/strings-text-handling","01.csharp/01.fundamentals/05.strings-text-handling",{"title":40,"path":41,"stem":42},"Дати і Час","/csharp/fundamentals/dates-time-handling","01.csharp/01.fundamentals/06.dates-time-handling",{"title":44,"path":45,"stem":46},"Потік Керування","/csharp/fundamentals/control-flow","01.csharp/01.fundamentals/07.control-flow",{"title":48,"path":49,"stem":50},"Методи","/csharp/fundamentals/methods","01.csharp/01.fundamentals/08.methods",{"title":52,"path":53,"stem":54},"Основи Відлагодження","/csharp/fundamentals/debugging-basics","01.csharp/01.fundamentals/09.debugging-basics",{"title":56,"path":57,"stem":58},"Інтерактивна Консоль (Classic)","/csharp/fundamentals/interactive-console","01.csharp/01.fundamentals/10.interactive-console",false,{"title":61,"icon":62,"path":63,"stem":64,"children":65,"page":59},"OOP","i-lucide-box","/csharp/oop","01.csharp/02.oop",[66,70,74,78,82,86],{"title":67,"path":68,"stem":69},"Package Management (Управління Пакетами)","/csharp/oop/package-management","01.csharp/02.oop/01.package-management",{"title":71,"path":72,"stem":73},"Класи та Об'єкти","/csharp/oop/classes-objects","01.csharp/02.oop/02.classes-objects",{"title":75,"path":76,"stem":77},"Властивості та Поля","/csharp/oop/properties-fields","01.csharp/02.oop/03.properties-fields",{"title":79,"path":80,"stem":81},"Стовпи ООП","/csharp/oop/oop-pillars","01.csharp/02.oop/04.oop-pillars",{"title":83,"path":84,"stem":85},"Advanced Types","/csharp/oop/advanced-types","01.csharp/02.oop/05.advanced-types",{"title":87,"path":88,"stem":89},"Namespaces (Простори Імен)","/csharp/oop/namespaces","01.csharp/02.oop/06.namespaces",{"title":91,"icon":92,"path":93,"stem":94,"children":95,"page":59},"Advanced Core","i-lucide-zap","/csharp/advanced-core","01.csharp/03.advanced-core",[96,100,104,108,112,116],{"title":97,"path":98,"stem":99},"Generics (Узагальнення)","/csharp/advanced-core/generics","01.csharp/03.advanced-core/01.generics",{"title":101,"path":102,"stem":103},"Делегати, Події та Лямбда-вирази","/csharp/advanced-core/delegates-events-lambdas","01.csharp/03.advanced-core/02.delegates-events-lambdas",{"title":105,"path":106,"stem":107},"Interfaces Deep Dive (Інтерфейси: Поглиблений Розгляд)","/csharp/advanced-core/interfaces-deep-dive","01.csharp/03.advanced-core/03.interfaces-deep-dive",{"title":109,"path":110,"stem":111},"Обробка Винятків","/csharp/advanced-core/exception-handling","01.csharp/03.advanced-core/04.exception-handling",{"title":113,"path":114,"stem":115},"Pattern Matching","/csharp/advanced-core/pattern-matching","01.csharp/03.advanced-core/05.pattern-matching",{"title":117,"path":118,"stem":119},"Додаткові Можливості C#","/csharp/advanced-core/additional-features","01.csharp/03.advanced-core/06.additional-features",{"title":121,"icon":122,"path":123,"stem":124,"children":125,"page":59},"Architecture Best Practices","i-lucide-building-2","/csharp/architecture-best-practices","01.csharp/04.architecture-best-practices",[126,130,149,153,157,161,165,169],{"title":127,"path":128,"stem":129},"Software Design Principles (Частина 1)","/csharp/architecture-best-practices/software-design-principles","01.csharp/04.architecture-best-practices/01.software-design-principles",{"title":131,"icon":132,"path":133,"stem":134,"children":135,"page":59},"Design Patterns","i-lucide-folder","/csharp/architecture-best-practices/design-patterns","01.csharp/04.architecture-best-practices/02.design-patterns",[136],{"title":137,"icon":132,"path":138,"stem":139,"children":140,"page":59},"Creational","/csharp/architecture-best-practices/design-patterns/creational","01.csharp/04.architecture-best-practices/02.design-patterns/creational",[141,145],{"title":142,"path":143,"stem":144},"Singleton (Одинак)","/csharp/architecture-best-practices/design-patterns/creational/singleton","01.csharp/04.architecture-best-practices/02.design-patterns/creational/01.singleton",{"title":146,"path":147,"stem":148},"Builder (Будівельник)","/csharp/architecture-best-practices/design-patterns/creational/builder","01.csharp/04.architecture-best-practices/02.design-patterns/creational/02.builder",{"title":150,"path":151,"stem":152},"Building Professional CLIs","/csharp/architecture-best-practices/building-professional-clis","01.csharp/04.architecture-best-practices/03.building-professional-clis",{"title":154,"path":155,"stem":156},"Validation & Flow Control","/csharp/architecture-best-practices/validation-flow-control","01.csharp/04.architecture-best-practices/04.validation-flow-control",{"title":158,"path":159,"stem":160},"The Modern .NET Host (Microsoft.Extensions)","/csharp/architecture-best-practices/modern-dotnet-host","01.csharp/04.architecture-best-practices/05.modern-dotnet-host",{"title":162,"path":163,"stem":164},"Data Mapper: Repository та DAO патерни (Частина 1)","/csharp/architecture-best-practices/data-mapper-part1","01.csharp/04.architecture-best-practices/06.data-mapper-part1",{"title":166,"path":167,"stem":168},"Data Mapper: Repository та DAO патерни (Частина 2)","/csharp/architecture-best-practices/data-mapper-part2","01.csharp/04.architecture-best-practices/07.data-mapper-part2",{"title":170,"icon":132,"path":171,"stem":172,"children":173,"page":59},"Di Ioc","/csharp/architecture-best-practices/di-ioc","01.csharp/04.architecture-best-practices/08.di-ioc",[174,178,182,186,190,194,198],{"title":175,"path":176,"stem":177},"Проблема залежностей та Інверсія Контролю","/csharp/architecture-best-practices/di-ioc/the-dependency-problem","01.csharp/04.architecture-best-practices/08.di-ioc/01.the-dependency-problem",{"title":179,"path":180,"stem":181},"Будуємо власний Service Container","/csharp/architecture-best-practices/di-ioc/build-your-own-container","01.csharp/04.architecture-best-practices/08.di-ioc/02.build-your-own-container",{"title":183,"path":184,"stem":185},"Service Locator: Паттерн та Анти-паттерн","/csharp/architecture-best-practices/di-ioc/service-locator-pattern","01.csharp/04.architecture-best-practices/08.di-ioc/03.service-locator-pattern",{"title":187,"path":188,"stem":189},"Паттерни Dependency Injection","/csharp/architecture-best-practices/di-ioc/dependency-injection-patterns","01.csharp/04.architecture-best-practices/08.di-ioc/04.dependency-injection-patterns",{"title":191,"path":192,"stem":193},"Microsoft DI: IServiceCollection та IServiceProvider","/csharp/architecture-best-practices/di-ioc/microsoft-di-deep-dive","01.csharp/04.architecture-best-practices/08.di-ioc/05.microsoft-di-deep-dive",{"title":195,"path":196,"stem":197},"Service Lifetimes та Scopes","/csharp/architecture-best-practices/di-ioc/service-lifetimes-and-scopes","01.csharp/04.architecture-best-practices/08.di-ioc/06.service-lifetimes-and-scopes",{"title":199,"path":200,"stem":201},"DI Анти-паттерни та Найкращі Практики","/csharp/architecture-best-practices/di-ioc/di-anti-patterns-and-best-practices","01.csharp/04.architecture-best-practices/08.di-ioc/07.di-anti-patterns-and-best-practices",{"title":203,"icon":132,"path":204,"stem":205,"children":206,"page":59},"Standard Library","/csharp/standard-library","01.csharp/05.standard-library",[207,211,215],{"title":208,"path":209,"stem":210},"Collections (Колекції)","/csharp/standard-library/collections","01.csharp/05.standard-library/01.collections",{"title":212,"path":213,"stem":214},"High Performance Types (Високопродуктивні Типи)","/csharp/standard-library/high-performance-types","01.csharp/05.standard-library/02.high-performance-types",{"title":216,"path":217,"stem":218},"LINQ (Language Integrated Query)","/csharp/standard-library/linq","01.csharp/05.standard-library/03.linq",{"title":220,"icon":221,"path":222,"stem":223,"children":224,"page":59},"System Internals Concurrency","i-lucide-server","/csharp/system-internals-concurrency","01.csharp/06.system-internals-concurrency",[225,229,233,237,241,245,249],{"title":226,"path":227,"stem":228},"Memory Management","/csharp/system-internals-concurrency/memory-management","01.csharp/06.system-internals-concurrency/01.memory-management",{"title":230,"path":231,"stem":232},"Reflection API: System.Type та Метадані","/csharp/system-internals-concurrency/reflection-fundamentals","01.csharp/06.system-internals-concurrency/02.reflection-fundamentals",{"title":234,"path":235,"stem":236},"Attributes та Dynamic Language Runtime","/csharp/system-internals-concurrency/attributes-dynamic","01.csharp/06.system-internals-concurrency/03.attributes-dynamic",{"title":238,"path":239,"stem":240},"Expression Trees: Швидка Альтернатива Рефлексії","/csharp/system-internals-concurrency/expression-trees-compiled","01.csharp/06.system-internals-concurrency/04.expression-trees-compiled",{"title":242,"path":243,"stem":244},"Source Generators: Compile-Time Code Generation","/csharp/system-internals-concurrency/source-generators","01.csharp/06.system-internals-concurrency/05.source-generators",{"title":246,"path":247,"stem":248},"Multithreading Fundamentals","/csharp/system-internals-concurrency/multithreading-fundamentals","01.csharp/06.system-internals-concurrency/06.multithreading-fundamentals",{"title":250,"path":251,"stem":252},"Synchronization Primitives","/csharp/system-internals-concurrency/synchronization-primitives","01.csharp/06.system-internals-concurrency/07.synchronization-primitives",{"title":254,"icon":255,"path":256,"stem":257,"children":258,"page":59},"System Programming Windows","i-lucide-cpu","/csharp/system-programming-windows","01.csharp/07.system-programming-windows",[259,263,267,271,275,279,283,287,291,295,299,303,307,311,315,319,323,327,331,335,339,343,347,351,355,359,363,367,371,375],{"title":260,"path":261,"stem":262},"Як Працює Операційна Система","/csharp/system-programming-windows/how-os-works","01.csharp/07.system-programming-windows/01.how-os-works",{"title":264,"path":265,"stem":266},"Процеси в .NET — API та Запуск","/csharp/system-programming-windows/processes-in-dotnet","01.csharp/07.system-programming-windows/02.processes-in-dotnet",{"title":268,"path":269,"stem":270},"Процеси в .NET — IPC та Міжпроцесна Комунікація","/csharp/system-programming-windows/02a.processes-ipc","01.csharp/07.system-programming-windows/02a.processes-ipc",{"title":272,"path":273,"stem":274},"Application Domains та Збірки — AppDomain і AssemblyLoadContext","/csharp/system-programming-windows/appdomains-assemblies","01.csharp/07.system-programming-windows/03.appdomains-assemblies",{"title":276,"path":277,"stem":278},"Application Domains та Збірки — Plug-in Система з Hot-Reload","/csharp/system-programming-windows/03a.appdomains-plugin-system","01.csharp/07.system-programming-windows/03a.appdomains-plugin-system",{"title":280,"path":281,"stem":282},"Потоки — Основи та API Thread","/csharp/system-programming-windows/thread-fundamentals","01.csharp/07.system-programming-windows/04.thread-fundamentals",{"title":284,"path":285,"stem":286},"Потоки — Lifecycle, Пріоритети та Безпечне Завершення","/csharp/system-programming-windows/04a.thread-lifecycle-priorities","01.csharp/07.system-programming-windows/04a.thread-lifecycle-priorities",{"title":288,"path":289,"stem":290},"Проблеми Спільного Стану — Race Condition та Data Race","/csharp/system-programming-windows/shared-state-problems","01.csharp/07.system-programming-windows/05.shared-state-problems",{"title":292,"path":293,"stem":294},"Проблеми Спільного Стану — Memory Model та volatile","/csharp/system-programming-windows/05a.shared-state-memory-model","01.csharp/07.system-programming-windows/05a.shared-state-memory-model",{"title":296,"path":297,"stem":298},"Синхронізація — Monitor, lock та еволюція примітивів","/csharp/system-programming-windows/synchronization-fundamentals","01.csharp/07.system-programming-windows/06.synchronization-fundamentals",{"title":300,"path":301,"stem":302},"Синхронізація — Наскрізний Приклад та Deadlock Detection","/csharp/system-programming-windows/06a.synchronization-walkthrough","01.csharp/07.system-programming-windows/06a.synchronization-walkthrough",{"title":304,"path":305,"stem":306},"Синхронізація — Mutex, Semaphore та Event-Based Primitives","/csharp/system-programming-windows/synchronization-advanced","01.csharp/07.system-programming-windows/07.synchronization-advanced",{"title":308,"path":309,"stem":310},"Синхронізація — Interlocked, Volatile та Lock-Free Структури","/csharp/system-programming-windows/07a.synchronization-advanced-walkthrough","01.csharp/07.system-programming-windows/07a.synchronization-advanced-walkthrough",{"title":312,"path":313,"stem":314},"Interlocked, CAS та Lock-Free Структури","/csharp/system-programming-windows/interlocked-cas-lockfree","01.csharp/07.system-programming-windows/08.interlocked-cas-lockfree",{"title":316,"path":317,"stem":318},"Volatile, Memory Model та Spinning","/csharp/system-programming-windows/08a.volatile-memory-model","01.csharp/07.system-programming-windows/08a.volatile-memory-model",{"title":320,"path":321,"stem":322},"ThreadPool — Пул Потоків для Ефективного Виконання","/csharp/system-programming-windows/thread-pool","01.csharp/07.system-programming-windows/09.thread-pool",{"title":324,"path":325,"stem":326},"ThreadPool — Просунуті Сценарії та Внутрішня Будова","/csharp/system-programming-windows/09a.thread-pool-advanced","01.csharp/07.system-programming-windows/09a.thread-pool-advanced",{"title":328,"path":329,"stem":330},"Concurrent та Immutable Collections","/csharp/system-programming-windows/concurrent-collections","01.csharp/07.system-programming-windows/10.concurrent-collections",{"title":332,"path":333,"stem":334},"TPL, Task та Композиція — Від Thread до Task","/csharp/system-programming-windows/tpl-parallel-plinq","01.csharp/07.system-programming-windows/11.tpl-parallel-plinq",{"title":336,"path":337,"stem":338},"Parallel Class та PLINQ — Data Parallelism","/csharp/system-programming-windows/11a.tpl-parallel-plinq-advanced","01.csharp/07.system-programming-windows/11a.tpl-parallel-plinq-advanced",{"title":340,"path":341,"stem":342},"Async/Await — Фундамент Асинхронного Програмування","/csharp/system-programming-windows/async-fundamentals","01.csharp/07.system-programming-windows/12.async-fundamentals",{"title":344,"path":345,"stem":346},"SynchronizationContext та ConfigureAwait — Контекст Виконання","/csharp/system-programming-windows/async-context-configureawait","01.csharp/07.system-programming-windows/13.async-context-configureawait",{"title":348,"path":349,"stem":350},"Async — Просунуті Паттерни","/csharp/system-programming-windows/async-advanced","01.csharp/07.system-programming-windows/14.async-advanced",{"title":352,"path":353,"stem":354},"System.Threading.Channels — Async Producer-Consumer","/csharp/system-programming-windows/channels","01.csharp/07.system-programming-windows/15.channels",{"title":356,"path":357,"stem":358},"Асинхронна Синхронізація","/csharp/system-programming-windows/async-synchronization","01.csharp/07.system-programming-windows/16.async-synchronization",{"title":360,"path":361,"stem":362},"Unsafe Code та Вказівники","/csharp/system-programming-windows/unsafe-code","01.csharp/07.system-programming-windows/17.unsafe-code",{"title":364,"path":365,"stem":366},"P/Invoke та Windows API — Міст між .NET та Native Code","/csharp/system-programming-windows/pinvoke-winapi","01.csharp/07.system-programming-windows/18.pinvoke-winapi",{"title":368,"path":369,"stem":370},"Реєстр Windows — Центральна База Конфігурації Системи","/csharp/system-programming-windows/windows-registry","01.csharp/07.system-programming-windows/19.windows-registry",{"title":372,"path":373,"stem":374},"Windows Hooks, Hotkeys та Services — Глибока Інтеграція з ОС","/csharp/system-programming-windows/windows-hooks-services","01.csharp/07.system-programming-windows/20.windows-hooks-services",{"title":376,"path":377,"stem":378},"Системне Програмування C# (Windows) — 07.system-programming-windows","/csharp/system-programming-windows/implementation_plan","01.csharp/07.system-programming-windows/implementation_plan",{"title":380,"icon":132,"path":381,"stem":382,"children":383,"page":59},"Io","/csharp/io","01.csharp/08.io",[384,388,392,396,400],{"title":385,"path":386,"stem":387},"8.1.1. Основи роботи з файловою системою","/csharp/io/file-system-basics","01.csharp/08.io/01.file-system-basics",{"title":389,"path":390,"stem":391},"8.1.2. Потоки (Streams) та Серіалізація Даних","/csharp/io/streams-serialization","01.csharp/08.io/02.streams-serialization",{"title":393,"path":394,"stem":395},"8.2.1. JSON Serialization з System.Text.Json","/csharp/io/json-serialization","01.csharp/08.io/03.json-serialization",{"title":397,"path":398,"stem":399},"8.2.2. XML Serialization та LINQ to XML","/csharp/io/xml-serialization","01.csharp/08.io/04.xml-serialization",{"title":401,"path":402,"stem":403},"8.2.3. Binary Serialization: MessagePack та Protocol Buffers","/csharp/io/binary-serialization","01.csharp/08.io/05.binary-serialization",{"title":405,"icon":132,"path":406,"stem":407,"children":408,"page":59},"Ado Net","/csharp/ado-net","01.csharp/09.ado-net",[409,413,417,421,425,429,433,437,441,445,449,453],{"title":410,"path":411,"stem":412},"9.1. Введення в ADO.NET","/csharp/ado-net/introduction-to-adonet","01.csharp/09.ado-net/01.introduction-to-adonet",{"title":414,"path":415,"stem":416},"9.2. Клас DbConnection — з'єднання з базою даних","/csharp/ado-net/connection","01.csharp/09.ado-net/02.connection",{"title":418,"path":419,"stem":420},"9.3. Клас DbCommand — виконання SQL-запитів","/csharp/ado-net/command-and-queries","01.csharp/09.ado-net/03.command-and-queries",{"title":422,"path":423,"stem":424},"9.4. Клас DbDataReader — ефективне читання даних","/csharp/ado-net/datareader","01.csharp/09.ado-net/04.datareader",{"title":426,"path":427,"stem":428},"9.5. Параметризовані запити та захист від SQL Injection","/csharp/ado-net/parameters-and-sql-injection","01.csharp/09.ado-net/05.parameters-and-sql-injection",{"title":430,"path":431,"stem":432},"9.6. Транзакції в ADO.NET","/csharp/ado-net/transactions","01.csharp/09.ado-net/06.transactions",{"title":434,"path":435,"stem":436},"9.7. DbProviderFactory — провайдер-незалежний код","/csharp/ado-net/provider-factory","01.csharp/09.ado-net/07.provider-factory",{"title":438,"path":439,"stem":440},"9.8. Асинхронний доступ до даних","/csharp/ado-net/async-data-access","01.csharp/09.ado-net/08.async-data-access",{"title":442,"path":443,"stem":444},"9.9. Від'єднаний режим: DataSet, DataTable, DataRow","/csharp/ado-net/disconnected-mode-dataset","01.csharp/09.ado-net/09.disconnected-mode-dataset",{"title":446,"path":447,"stem":448},"9.10. DataAdapter — міст між DataSet та базою даних","/csharp/ado-net/data-adapter","01.csharp/09.ado-net/10.data-adapter",{"title":450,"path":451,"stem":452},"9.11. Data Mapper та Repository: Архітектура доступу до даних","/csharp/ado-net/data-mapper-repository","01.csharp/09.ado-net/11.data-mapper-repository",{"title":454,"path":455,"stem":456},"9.12. Identity Map, Unit of Work та Specification Pattern","/csharp/ado-net/advanced-patterns","01.csharp/09.ado-net/12.advanced-patterns",{"title":458,"icon":255,"path":459,"stem":460,"children":461,"page":59},"Ef Core","/csharp/ef-core","01.csharp/10.ef-core",[462,466,470,474,478,482,486,490,494,498,502,506,510,514,518,522,526,532,538,542,546,550,554,558,562,566,570,574,578,582,586,590,594,598,602,606,610,614,618,622,626,630,634,638,642,646],{"title":463,"path":464,"stem":465},"Що таке ORM? Від SQL до об'єктів","/csharp/ef-core/what-is-orm","01.csharp/10.ef-core/01.what-is-orm",{"title":467,"path":468,"stem":469},"Перший проєкт — від нуля до CRUD","/csharp/ef-core/first-project","01.csharp/10.ef-core/02.first-project",{"title":471,"path":472,"stem":473},"DbContext — Серце EF Core","/csharp/ef-core/dbcontext-deep-dive","01.csharp/10.ef-core/03.dbcontext-deep-dive",{"title":475,"path":476,"stem":477},"Провайдери баз даних — Архітектура та Вибір СУБД","/csharp/ef-core/database-providers","01.csharp/10.ef-core/04.database-providers",{"title":479,"path":480,"stem":481},"Конвенції EF Core — Магія без конфігурації","/csharp/ef-core/conventions","01.csharp/10.ef-core/05.conventions",{"title":483,"path":484,"stem":485},"Fluent API та Data Annotations — Явна конфігурація моделі","/csharp/ef-core/fluent-api-vs-annotations","01.csharp/10.ef-core/06.fluent-api-vs-annotations",{"title":487,"path":488,"stem":489},"Зв'язки — One-to-One та One-to-Many","/csharp/ef-core/relationships-basics","01.csharp/10.ef-core/07.relationships-basics",{"title":491,"path":492,"stem":493},"Зв'язки Advanced — Many-to-Many та Складні Сценарії","/csharp/ef-core/relationships-advanced","01.csharp/10.ef-core/08.relationships-advanced",{"title":495,"path":496,"stem":497},"Властивості — Типи, Конвертери, Компаратори (Частина 1)","/csharp/ef-core/property-configuration-part1","01.csharp/10.ef-core/09.property-configuration-part1",{"title":499,"path":500,"stem":501},"Властивості — Value Comparers, Generators, Shadow Properties (Частина 2)","/csharp/ef-core/property-configuration-part2","01.csharp/10.ef-core/09.property-configuration-part2",{"title":503,"path":504,"stem":505},"Складні типи — Owned Types та Complex Types (Частина 1)","/csharp/ef-core/complex-types-owned-part1","01.csharp/10.ef-core/10.complex-types-owned-part1",{"title":507,"path":508,"stem":509},"Складні типи — Complex Types, Keyless Entities, Порівняння (Частина 2)","/csharp/ef-core/complex-types-owned-part2","01.csharp/10.ef-core/10.complex-types-owned-part2",{"title":511,"path":512,"stem":513},"JSON Columns — Складні дані у JSON (Частина 1)","/csharp/ef-core/json-columns-part1","01.csharp/10.ef-core/11.json-columns-part1",{"title":515,"path":516,"stem":517},"JSON Columns — Value Comparers, Індекси, Провайдери (Частина 2)","/csharp/ef-core/json-columns-part2","01.csharp/10.ef-core/11.json-columns-part2",{"title":519,"path":520,"stem":521},"Успадкування — Абстрактні класи та TPH (Частина 1)","/csharp/ef-core/inheritance-part1","01.csharp/10.ef-core/12.inheritance-part1",{"title":523,"path":524,"stem":525},"Успадкування — TPT, TPC та Порівняння Стратегій (Частина 2)","/csharp/ef-core/inheritance-part2","01.csharp/10.ef-core/12.inheritance-part2",{"title":527,"path":528,"stem":529,"children":530},"Індекси, Обмеження та Схема (Частина 1)","/csharp/ef-core/indexes-constraints-part1","01.csharp/10.ef-core/13.indexes-constraints-part1",[531],{"title":527,"path":528,"stem":529},{"title":533,"path":534,"stem":535,"children":536},"Індекси, Обмеження та Схема (Частина 2)","/csharp/ef-core/indexes-constraints-part2","01.csharp/10.ef-core/13.indexes-constraints-part2",[537],{"title":533,"path":534,"stem":535},{"title":539,"path":540,"stem":541},"Seed Data — Початкові Дані (Частина 1)","/csharp/ef-core/seeding-part1","01.csharp/10.ef-core/14.seeding-part1",{"title":543,"path":544,"stem":545},"Seed Data — SQL-скрипти, Bogus та Стратегії (Частина 2)","/csharp/ef-core/seeding-part2","01.csharp/10.ef-core/14.seeding-part2",{"title":547,"path":548,"stem":549},"Global Query Filters — Глобальні Фільтри (Частина 1)","/csharp/ef-core/global-query-filters-part1","01.csharp/10.ef-core/15.global-query-filters-part1",{"title":551,"path":552,"stem":553},"Global Query Filters — Підводні камені та Інтеграція (Частина 2)","/csharp/ef-core/global-query-filters-part2","01.csharp/10.ef-core/15.global-query-filters-part2",{"title":555,"path":556,"stem":557},"LINQ-запити в EF Core (Частина 1)","/csharp/ef-core/linq-queries-part1","01.csharp/10.ef-core/16.linq-queries-part1",{"title":559,"path":560,"stem":561},"LINQ-запити в EF Core (Частина 2)","/csharp/ef-core/linq-queries-part2","01.csharp/10.ef-core/16.linq-queries-part2",{"title":563,"path":564,"stem":565},"Завантаження Пов'язаних Даних (Частина 1)","/csharp/ef-core/loading-related-data-part1","01.csharp/10.ef-core/17.loading-related-data-part1",{"title":567,"path":568,"stem":569},"Завантаження Пов'язаних Даних (Частина 2)","/csharp/ef-core/loading-related-data-part2","01.csharp/10.ef-core/17.loading-related-data-part2",{"title":571,"path":572,"stem":573},"Raw SQL, Views та Stored Procedures (Частина 1)","/csharp/ef-core/raw-sql-part1","01.csharp/10.ef-core/18.raw-sql-part1",{"title":575,"path":576,"stem":577},"Raw SQL — Stored Procedures, DbFunction та Bulk Operations (Частина 2)","/csharp/ef-core/raw-sql-part2","01.csharp/10.ef-core/18.raw-sql-part2",{"title":579,"path":580,"stem":581},"Продвинуті Запити — Compiled Queries, Bulk та Оптимізація (Частина 1)","/csharp/ef-core/advanced-queries-part1","01.csharp/10.ef-core/19.advanced-queries-part1",{"title":583,"path":584,"stem":585},"Продвинуті Запити — Query Tags, Bulk та Interceptors (Частина 2)","/csharp/ef-core/advanced-queries-part2","01.csharp/10.ef-core/19.advanced-queries-part2",{"title":587,"path":588,"stem":589},"Change Tracker — Відстеження Змін (Частина 1)","/csharp/ef-core/change-tracking-part1","01.csharp/10.ef-core/20.change-tracking-part1",{"title":591,"path":592,"stem":593},"Change Tracker — Графи Об'єктів та Disconnected (Частина 2)","/csharp/ef-core/change-tracking-part2","01.csharp/10.ef-core/20.change-tracking-part2",{"title":595,"path":596,"stem":597},"Збереження Даних та Транзакції (Частина 1)","/csharp/ef-core/saving-data-part1","01.csharp/10.ef-core/21.saving-data-part1",{"title":599,"path":600,"stem":601},"Збереження Даних — Concurrency та Outbox (Частина 2)","/csharp/ef-core/saving-data-part2","01.csharp/10.ef-core/21.saving-data-part2",{"title":603,"path":604,"stem":605},"Конкурентність та Блокування (Частина 1)","/csharp/ef-core/concurrency-part1","01.csharp/10.ef-core/22.concurrency-part1",{"title":607,"path":608,"stem":609},"Конкурентність — Дедлоки та Queue Processing (Частина 2)","/csharp/ef-core/concurrency-part2","01.csharp/10.ef-core/22.concurrency-part2",{"title":611,"path":612,"stem":613},"Міграції в EF Core — Основи (Частина 1)","/csharp/ef-core/migrations-basics-part1","01.csharp/10.ef-core/23.migrations-basics-part1",{"title":615,"path":616,"stem":617},"Міграції в EF Core — Основи (Частина 2)","/csharp/ef-core/migrations-basics-part2","01.csharp/10.ef-core/23.migrations-basics-part2",{"title":619,"path":620,"stem":621},"Міграції — Просунуті Сценарії (Частина 1)","/csharp/ef-core/migrations-advanced-part1","01.csharp/10.ef-core/24.migrations-advanced-part1",{"title":623,"path":624,"stem":625},"Міграції — Просунуті Сценарії (Частина 2)","/csharp/ef-core/migrations-advanced-part2","01.csharp/10.ef-core/24.migrations-advanced-part2",{"title":627,"path":628,"stem":629},"Управління Схемою та Database-First (Частина 1)","/csharp/ef-core/schema-management-part1","01.csharp/10.ef-core/25.schema-management-part1",{"title":631,"path":632,"stem":633},"Управління Схемою та Database-First (Частина 2)","/csharp/ef-core/schema-management-part2","01.csharp/10.ef-core/25.schema-management-part2",{"title":635,"path":636,"stem":637},"Продуктивність EF Core — Основи (Частина 1)","/csharp/ef-core/performance-fundamentals-part1","01.csharp/10.ef-core/26.performance-fundamentals-part1",{"title":639,"path":640,"stem":641},"Interceptors в EF Core (Частина 1)","/csharp/ef-core/interceptors-part1","01.csharp/10.ef-core/29.interceptors-part1",{"title":643,"path":644,"stem":645},"Interceptors в EF Core — Connection, Transaction та Materialization (Частина 2)","/csharp/ef-core/interceptors-part2","01.csharp/10.ef-core/29.interceptors-part2",{"title":647,"path":648,"stem":649},"План вивчення Entity Framework Core — Повний курс","/csharp/ef-core/implementation_plan","01.csharp/10.ef-core/implementation_plan",{"title":651,"icon":652,"path":653,"stem":654,"children":655,"page":59},"ASP.NET","i-devicon-dotnetcore","/csharp/aspnet","01.csharp/11.aspnet",[656,730,791,869,927,941,967,1057,1111,1182,1212,1289],{"title":657,"icon":658,"path":659,"stem":660,"children":661,"page":59},"Minimal API","i-lucide-network","/csharp/aspnet/minimal-api","01.csharp/11.aspnet/01.minimal-api",[662,666,670,674,678,682,686,690,694,698,702,706,710,714,718,722,726],{"title":663,"path":664,"stem":665},"Вступ до ASP.NET та еволюція фреймворку","/csharp/aspnet/minimal-api/introduction","01.csharp/11.aspnet/01.minimal-api/01.introduction",{"title":667,"path":668,"stem":669},"Перший додаток на ASP.NET Core","/csharp/aspnet/minimal-api/first-application","01.csharp/11.aspnet/01.minimal-api/02.first-application",{"title":671,"path":672,"stem":673},"WebApplication, Builder та Dependency Injection","/csharp/aspnet/minimal-api/webapplication-builder","01.csharp/11.aspnet/01.minimal-api/03.webapplication-builder",{"title":675,"path":676,"stem":677},"Конвеєр запитів та Middleware","/csharp/aspnet/minimal-api/request-pipeline-middleware","01.csharp/11.aspnet/01.minimal-api/04.request-pipeline-middleware",{"title":679,"path":680,"stem":681},"Маршрутизація в ASP.NET Core: Основи","/csharp/aspnet/minimal-api/routing-basics","01.csharp/11.aspnet/01.minimal-api/05.routing-basics",{"title":683,"path":684,"stem":685},"Маршрутизація в ASP.NET Core: Розширені можливості","/csharp/aspnet/minimal-api/routing-advanced","01.csharp/11.aspnet/01.minimal-api/06.routing-advanced",{"title":687,"path":688,"stem":689},"Статичні файли в ASP.NET Core","/csharp/aspnet/minimal-api/static-files","01.csharp/11.aspnet/01.minimal-api/07.static-files",{"title":691,"path":692,"stem":693},"Статичні Активи: MapStaticAssets (ASP.NET Core 9.0)","/csharp/aspnet/minimal-api/static-assets","01.csharp/11.aspnet/01.minimal-api/08.static-assets",{"title":695,"path":696,"stem":697},"Конфігурація в ASP.NET Core: Основи","/csharp/aspnet/minimal-api/configuration-fundamentals","01.csharp/11.aspnet/01.minimal-api/09.configuration-fundamentals",{"title":699,"path":700,"stem":701},"Конфігурація: Паттерн Options","/csharp/aspnet/minimal-api/configuration-options","01.csharp/11.aspnet/01.minimal-api/10.configuration-options",{"title":703,"path":704,"stem":705},"Логування в ASP.NET Core: Основи","/csharp/aspnet/minimal-api/logging-basics","01.csharp/11.aspnet/01.minimal-api/11.logging-basics",{"title":707,"path":708,"stem":709},"Логування: Serilog та Middleware","/csharp/aspnet/minimal-api/logging-advanced","01.csharp/11.aspnet/01.minimal-api/12.logging-advanced",{"title":711,"path":712,"stem":713},"Управління станом: HttpContext.Items та Cookies","/csharp/aspnet/minimal-api/state-management","01.csharp/11.aspnet/01.minimal-api/13.state-management",{"title":715,"path":716,"stem":717},"Стан сесії: Sessions","/csharp/aspnet/minimal-api/session-state","01.csharp/11.aspnet/01.minimal-api/14.session-state",{"title":719,"path":720,"stem":721},"Структура проєкту: від хаосу до архітектури","/csharp/aspnet/minimal-api/project-structure","01.csharp/11.aspnet/01.minimal-api/15.project-structure",{"title":723,"path":724,"stem":725},"Scalar у Minimal API: повний проєкт і Fluent OpenAPI","/csharp/aspnet/minimal-api/scalar-openapi-fluent","01.csharp/11.aspnet/01.minimal-api/16.scalar-openapi-fluent",{"title":727,"path":728,"stem":729},"Swagger / Swashbuckle у Minimal API: окремий класичний шлях","/csharp/aspnet/minimal-api/swagger-swashbuckle","01.csharp/11.aspnet/01.minimal-api/17.swagger-swashbuckle",{"title":731,"icon":658,"path":732,"stem":733,"children":734,"page":59},"API","/csharp/aspnet/api","01.csharp/11.aspnet/02.api",[735,739,743,747,751,755,759,763,767,771,775,779,783,787],{"title":736,"path":737,"stem":738},"Що таке API. Клієнт-серверна архітектура","/csharp/aspnet/api/what-is-api","01.csharp/11.aspnet/02.api/01.what-is-api",{"title":740,"path":741,"stem":742},"Формати даних: JSON, XML, TOML та бінарні формати","/csharp/aspnet/api/data-formats","01.csharp/11.aspnet/02.api/02.data-formats",{"title":744,"path":745,"stem":746},"Парадигми API та концепція REST","/csharp/aspnet/api/api-paradigms-rest","01.csharp/11.aspnet/02.api/03.api-paradigms-rest",{"title":748,"path":749,"stem":750},"HTTP-методи, статус-коди та заголовки","/csharp/aspnet/api/http-methods-status-codes","01.csharp/11.aspnet/02.api/04.http-methods-status-codes",{"title":752,"path":753,"stem":754},"Організація HTTP API за принципами REST","/csharp/aspnet/api/rest-organizing","01.csharp/11.aspnet/02.api/05.rest-organizing",{"title":756,"path":757,"stem":758},"Номенклатура URL та CRUD-операції","/csharp/aspnet/api/url-nomenclature-crud","01.csharp/11.aspnet/02.api/06.url-nomenclature-crud",{"title":760,"path":761,"stem":762},"Правила дизайну: іменування та стандарти","/csharp/aspnet/api/api-design-naming","01.csharp/11.aspnet/02.api/07.api-design-naming",{"title":764,"path":765,"stem":766},"Валідація, ліміти та обробка помилок","/csharp/aspnet/api/api-design-validation","01.csharp/11.aspnet/02.api/08.api-design-validation",{"title":768,"path":769,"stem":770},"Обробка помилок у Minimal API","/csharp/aspnet/api/error-handling-http","01.csharp/11.aspnet/02.api/09.error-handling-http",{"title":772,"path":773,"stem":774},"Ідемпотентність та синхронізація стану","/csharp/aspnet/api/idempotency-sync","01.csharp/11.aspnet/02.api/10.idempotency-sync",{"title":776,"path":777,"stem":778},"Пагінація та організація списків","/csharp/aspnet/api/pagination-lists","01.csharp/11.aspnet/02.api/11.pagination-lists",{"title":780,"path":781,"stem":782},"Безпека API, кешування та інтернаціоналізація","/csharp/aspnet/api/security-auth","01.csharp/11.aspnet/02.api/12.security-auth",{"title":784,"path":785,"stem":786},"Процес проєктування API та документування","/csharp/aspnet/api/api-design-process","01.csharp/11.aspnet/02.api/13.api-design-process",{"title":788,"path":789,"stem":790},"OpenAPI: контракт, специфікація та документація API","/csharp/aspnet/api/openapi","01.csharp/11.aspnet/02.api/14.openapi",{"title":792,"icon":793,"path":794,"stem":795,"children":796,"page":59},"Auth","i-lucide-shield-check","/csharp/aspnet/auth","01.csharp/11.aspnet/03.auth",[797,801,805,809,813,817,821,825,829,833,837,841,845,849,853,857,861,865],{"title":798,"path":799,"stem":800},"Основи аутентифікації та авторизації","/csharp/aspnet/auth/auth-fundamentals","01.csharp/11.aspnet/03.auth/01.auth-fundamentals",{"title":802,"path":803,"stem":804},"JWT-аутентифікація","/csharp/aspnet/auth/jwt-authentication","01.csharp/11.aspnet/03.auth/02.jwt-authentication",{"title":806,"path":807,"stem":808},"Авторизація: ролі, політики та resource-based доступ","/csharp/aspnet/auth/authorization-policies","01.csharp/11.aspnet/03.auth/03.authorization-policies",{"title":810,"path":811,"stem":812},"Cookie-аутентифікація та ASP.NET Core Identity","/csharp/aspnet/auth/cookie-auth-identity","01.csharp/11.aspnet/03.auth/04.cookie-auth-identity",{"title":814,"path":815,"stem":816},"JWT + Refresh Tokens (HttpOnly Cookie)","/csharp/aspnet/auth/04b.identity-auth-jwt","01.csharp/11.aspnet/03.auth/04b.identity-auth-jwt",{"title":818,"path":819,"stem":820},"Identity: Підтвердження Email та Скидання Пароля","/csharp/aspnet/auth/identity-email-confirmation","01.csharp/11.aspnet/03.auth/05.identity-email-confirmation",{"title":822,"path":823,"stem":824},"Identity: Двофакторна Аутентифікація (2FA)","/csharp/aspnet/auth/identity-two-factor","01.csharp/11.aspnet/03.auth/06.identity-two-factor",{"title":826,"path":827,"stem":828},"Identity: Внутрішня Архітектура та Кастомізація","/csharp/aspnet/auth/identity-internals","01.csharp/11.aspnet/03.auth/07.identity-internals",{"title":830,"path":831,"stem":832},"OAuth 2.0 та зовнішні провайдери","/csharp/aspnet/auth/oauth-external-providers","01.csharp/11.aspnet/03.auth/08.oauth-external-providers",{"title":834,"path":835,"stem":836},"Безпека на практиці: CORS, HTTPS та захист від атак","/csharp/aspnet/auth/security-hardening","01.csharp/11.aspnet/03.auth/09.security-hardening",{"title":838,"path":839,"stem":840},"Теорія OAuth 2.0: Поняття, Аналогії та Флоу","/csharp/aspnet/auth/oauth-theory","01.csharp/11.aspnet/03.auth/10.oauth-theory",{"title":842,"path":843,"stem":844},"OIDC, OAuth 2.0 та Keycloak в ASP.NET Core","/csharp/aspnet/auth/oidc-keycloak","01.csharp/11.aspnet/03.auth/10.oidc-keycloak",{"title":846,"path":847,"stem":848},"API Keys аутентифікація в ASP.NET Core","/csharp/aspnet/auth/api-keys","01.csharp/11.aspnet/03.auth/11.api-keys",{"title":850,"path":851,"stem":852},"Rate Limiting та Throttling в ASP.NET Core","/csharp/aspnet/auth/rate-limiting","01.csharp/11.aspnet/03.auth/12.rate-limiting",{"title":854,"path":855,"stem":856},"Refresh Token Rotation в ASP.NET Core","/csharp/aspnet/auth/refresh-token-rotation","01.csharp/11.aspnet/03.auth/13.refresh-token-rotation",{"title":858,"path":859,"stem":860},"Certificate Authentication та mTLS в ASP.NET Core","/csharp/aspnet/auth/certificate-auth","01.csharp/11.aspnet/03.auth/14.certificate-auth",{"title":862,"path":863,"stem":864},"RBAC, ABAC та ReBAC в ASP.NET Core","/csharp/aspnet/auth/rbac-abac-rebac","01.csharp/11.aspnet/03.auth/15.rbac-abac-rebac",{"title":866,"path":867,"stem":868},"Multi-tenancy та ізоляція даних в ASP.NET Core","/csharp/aspnet/auth/multi-tenancy","01.csharp/11.aspnet/03.auth/16.multi-tenancy",{"title":870,"icon":871,"path":872,"stem":873,"children":874,"page":59},"Нотифікації","i-lucide-bell","/csharp/aspnet/notifications","01.csharp/11.aspnet/04.notifications",[875,879,883,887,891,895,899,903,907,911,915,919,923],{"title":876,"path":877,"stem":878},"In-App нотифікації через базу даних","/csharp/aspnet/notifications/in-app-database-notifications","01.csharp/11.aspnet/04.notifications/01.in-app-database-notifications",{"title":880,"path":881,"stem":882},"Polling: Регулярний запит оновлень","/csharp/aspnet/notifications/polling","01.csharp/11.aspnet/04.notifications/02.polling",{"title":884,"path":885,"stem":886},"Server-Sent Events: Однострімовий push від сервера","/csharp/aspnet/notifications/server-sent-events","01.csharp/11.aspnet/04.notifications/03.server-sent-events",{"title":888,"path":889,"stem":890},"WebSockets: Двостороннє з'єднання в реальному часі","/csharp/aspnet/notifications/websockets","01.csharp/11.aspnet/04.notifications/04.websockets",{"title":892,"path":893,"stem":894},"SignalR: Абстракція над транспортами реального часу","/csharp/aspnet/notifications/signalr","01.csharp/11.aspnet/04.notifications/05.signalr",{"title":896,"path":897,"stem":898},"Background Services: Фонові задачі в ASP.NET Core","/csharp/aspnet/notifications/background-services","01.csharp/11.aspnet/04.notifications/06.background-services",{"title":900,"path":901,"stem":902},"Web Push нотифікації","/csharp/aspnet/notifications/web-push","01.csharp/11.aspnet/04.notifications/07.web-push",{"title":904,"path":905,"stem":906},"Email нотифікації","/csharp/aspnet/notifications/email-notifications","01.csharp/11.aspnet/04.notifications/08.email-notifications",{"title":908,"path":909,"stem":910},"Порівняння підходів: Як вибрати правильну технологію нотифікацій","/csharp/aspnet/notifications/choosing-the-right-approach","01.csharp/11.aspnet/04.notifications/09.choosing-the-right-approach",{"title":912,"path":913,"stem":914},"Hangfire: Надійне планування фонових задач","/csharp/aspnet/notifications/hangfire","01.csharp/11.aspnet/04.notifications/10.hangfire",{"title":916,"path":917,"stem":918},"Практика: Конвертація зображень у WebP через Hangfire","/csharp/aspnet/notifications/hangfire-image-webp","01.csharp/11.aspnet/04.notifications/11.hangfire-image-webp",{"title":920,"path":921,"stem":922},"Практика: Підготовка відео до HLS-стрімінгу через Hangfire","/csharp/aspnet/notifications/hangfire-video-hls","01.csharp/11.aspnet/04.notifications/12.hangfire-video-hls",{"title":924,"path":925,"stem":926},"Telegram-нотифікації: від одного повідомлення до масових розсилок і мульти-канального підходу","/csharp/aspnet/notifications/telegram-notifications","01.csharp/11.aspnet/04.notifications/13.telegram-notifications",{"title":928,"icon":929,"path":930,"stem":931,"children":932,"page":59},"Інтернаціоналізація","i-lucide-languages","/csharp/aspnet/i18n","01.csharp/11.aspnet/05.i18n",[933,937],{"title":934,"path":935,"stem":936},"Інтернаціоналізація (i18n) у Minimal API: від A до Я","/csharp/aspnet/i18n/internationalization","01.csharp/11.aspnet/05.i18n/01.internationalization",{"title":938,"path":939,"stem":940},"Humanizer: людиномовні рядки у .NET","/csharp/aspnet/i18n/humanizer","01.csharp/11.aspnet/05.i18n/02.humanizer",{"title":942,"icon":943,"path":944,"stem":945,"children":946,"page":59},"Кешування","i-lucide-layers","/csharp/aspnet/caching","01.csharp/11.aspnet/06.caching",[947,951,955,959,963],{"title":948,"path":949,"stem":950},"Огляд кешування: чотири рівні і коли що обирати","/csharp/aspnet/caching/caching","01.csharp/11.aspnet/06.caching/01.caching",{"title":952,"path":953,"stem":954},"IMemoryCache: кеш в оперативній пам'яті","/csharp/aspnet/caching/memory-cache","01.csharp/11.aspnet/06.caching/02.memory-cache",{"title":956,"path":957,"stem":958},"IDistributedCache і Redis: розподілений кеш","/csharp/aspnet/caching/distributed-cache","01.csharp/11.aspnet/06.caching/03.distributed-cache",{"title":960,"path":961,"stem":962},"Response Cache: HTTP-кешування через Cache-Control","/csharp/aspnet/caching/response-cache","01.csharp/11.aspnet/06.caching/04.response-cache",{"title":964,"path":965,"stem":966},"Output Cache: серверний кеш HTTP-відповідей (.NET 7+)","/csharp/aspnet/caching/output-cache","01.csharp/11.aspnet/06.caching/05.output-cache",{"title":968,"icon":969,"path":970,"stem":971,"children":972,"page":59},"Тестування","i-lucide-test-tube","/csharp/aspnet/testing","01.csharp/11.aspnet/07.testing",[973,977,981,985,989,993,997,1001,1005,1009,1013,1017,1021,1025,1029,1033,1037,1041,1045,1049,1053],{"title":974,"path":975,"stem":976},"Що таке тестування? Від інтуїції до науки","/csharp/aspnet/testing/what-is-testing","01.csharp/11.aspnet/07.testing/01.what-is-testing",{"title":978,"path":979,"stem":980},"Піраміда тестування — Стратегія, а не Догма","/csharp/aspnet/testing/testing-pyramid","01.csharp/11.aspnet/07.testing/02.testing-pyramid",{"title":982,"path":983,"stem":984},"Дві Школи Тестування — Лондон проти Детройту","/csharp/aspnet/testing/testing-schools","01.csharp/11.aspnet/07.testing/03.testing-schools",{"title":986,"path":987,"stem":988},"TDD та BDD — Тести як Дизайн-інструмент","/csharp/aspnet/testing/tdd-and-bdd","01.csharp/11.aspnet/07.testing/04.tdd-and-bdd",{"title":990,"path":991,"stem":992},"Що саме тестувати — Техніки аналізу та Циклomatична складність","/csharp/aspnet/testing/what-to-test","01.csharp/11.aspnet/07.testing/05.what-to-test",{"title":994,"path":995,"stem":996},"Тестові Фреймворки — Навіщо вони і що всередині","/csharp/aspnet/testing/test-frameworks","01.csharp/11.aspnet/07.testing/06.test-frameworks",{"title":998,"path":999,"stem":1000},"xUnit — Факти, Теорії та Lifecycle тестів","/csharp/aspnet/testing/xunit-basics","01.csharp/11.aspnet/07.testing/07.xunit-basics",{"title":1002,"path":1003,"stem":1004},"xUnit Advanced — Fixtures, Кастомізація та Розширення","/csharp/aspnet/testing/xunit-advanced","01.csharp/11.aspnet/07.testing/08.xunit-advanced",{"title":1006,"path":1007,"stem":1008},"Moq — Глибоке занурення в мокування","/csharp/aspnet/testing/mocking-with-moq","01.csharp/11.aspnet/07.testing/09.mocking-with-moq",{"title":1010,"path":1011,"stem":1012},"Тестування Баз Даних — EF Core, SQLite та Testcontainers","/csharp/aspnet/testing/database-testing","01.csharp/11.aspnet/07.testing/10.database-testing",{"title":1014,"path":1015,"stem":1016},"Integration Testing — Частина 1 [Теорія та WebApplicationFactory]","/csharp/aspnet/testing/integration-testing","01.csharp/11.aspnet/07.testing/11.integration-testing",{"title":1018,"path":1019,"stem":1020},"Інтеграційне тестування — Практика","/csharp/aspnet/testing/11a.integration-testing-practice","01.csharp/11.aspnet/07.testing/11a.integration-testing-practice",{"title":1022,"path":1023,"stem":1024},"Integration Testing — Частина 2 [Просунуті Сценарії та Testcontainers]","/csharp/aspnet/testing/integration-testing-advanced","01.csharp/11.aspnet/07.testing/12.integration-testing-advanced",{"title":1026,"path":1027,"stem":1028},"Професійний Postman: Колекції, Змінні та GitHub Інтеграція","/csharp/aspnet/testing/postman-professional","01.csharp/11.aspnet/07.testing/13.postman-professional",{"title":1030,"path":1031,"stem":1032},"HttpClient у Тестах Частина 1: Архітектура та MockHttpMessageHandler","/csharp/aspnet/testing/httpclient-testing","01.csharp/11.aspnet/07.testing/14.httpclient-testing",{"title":1034,"path":1035,"stem":1036},"HttpClient у Тестах Частина 2: WireMock.Net та Resilience","/csharp/aspnet/testing/wiremock-net","01.csharp/11.aspnet/07.testing/15.wiremock-net",{"title":1038,"path":1039,"stem":1040},"Патерни та Анти-патерни Тестування: Test Smells","/csharp/aspnet/testing/testing-patterns","01.csharp/11.aspnet/07.testing/16.testing-patterns",{"title":1042,"path":1043,"stem":1044},"Просунуті інструменти: Time, Snapshots та Властивості","/csharp/aspnet/testing/advanced-testing-tools","01.csharp/11.aspnet/07.testing/17.advanced-testing-tools",{"title":1046,"path":1047,"stem":1048},"Тестування Архітектури з NetArchTest","/csharp/aspnet/testing/architecture-testing","01.csharp/11.aspnet/07.testing/18.architecture-testing",{"title":1050,"path":1051,"stem":1052},"Тестування Продуктивності: BenchmarkDotNet, NBomber та k6","/csharp/aspnet/testing/performance-testing","01.csharp/11.aspnet/07.testing/19.performance-testing",{"title":1054,"path":1055,"stem":1056},"Залишок плану для курсу \"Тестування ASP.NET Minimal API\"","/csharp/aspnet/testing/remaining_plan","01.csharp/11.aspnet/07.testing/remaining_plan",{"title":1058,"icon":1059,"path":1060,"stem":1061,"children":1062,"page":59},"Платежі","i-lucide-credit-card","/csharp/aspnet/payments","01.csharp/11.aspnet/08.payments",[1063,1067,1071,1075,1079,1083,1087,1091,1095,1099,1103,1107],{"title":1064,"path":1065,"stem":1066},"Основи платіжної інфраструктури","/csharp/aspnet/payments/payment-fundamentals","01.csharp/11.aspnet/08.payments/01.payment-fundamentals",{"title":1068,"path":1069,"stem":1070},"Методи оплати в Україні","/csharp/aspnet/payments/payment-methods-ukraine","01.csharp/11.aspnet/08.payments/02.payment-methods-ukraine",{"title":1072,"path":1073,"stem":1074},"PCI DSS та безпека платежів","/csharp/aspnet/payments/pci-dss-security","01.csharp/11.aspnet/08.payments/03.pci-dss-security",{"title":1076,"path":1077,"stem":1078},"Архітектура платіжної підсистеми","/csharp/aspnet/payments/payment-architecture","01.csharp/11.aspnet/08.payments/04.payment-architecture",{"title":1080,"path":1081,"stem":1082},"Інтеграція LiqPay (ПриватБанк)","/csharp/aspnet/payments/liqpay-integration","01.csharp/11.aspnet/08.payments/05.liqpay-integration",{"title":1084,"path":1085,"stem":1086},"Інтеграція Monobank Acquiring API","/csharp/aspnet/payments/monobank-acquiring","01.csharp/11.aspnet/08.payments/06.monobank-acquiring",{"title":1088,"path":1089,"stem":1090},"Інтеграція Stripe","/csharp/aspnet/payments/stripe-integration","01.csharp/11.aspnet/08.payments/07.stripe-integration",{"title":1092,"path":1093,"stem":1094},"Webhooks — глибоке занурення","/csharp/aspnet/payments/webhooks-deep-dive","01.csharp/11.aspnet/08.payments/08.webhooks-deep-dive",{"title":1096,"path":1097,"stem":1098},"Підписки та рекурентні платежі","/csharp/aspnet/payments/subscriptions-recurring","01.csharp/11.aspnet/08.payments/09.subscriptions-recurring",{"title":1100,"path":1101,"stem":1102},"Повернення коштів та диспути","/csharp/aspnet/payments/refunds-disputes","01.csharp/11.aspnet/08.payments/10.refunds-disputes",{"title":1104,"path":1105,"stem":1106},"Тестування платіжних інтеграцій","/csharp/aspnet/payments/testing-payments","01.csharp/11.aspnet/08.payments/11.testing-payments",{"title":1108,"path":1109,"stem":1110},"Чекліст виходу в Production","/csharp/aspnet/payments/production-checklist","01.csharp/11.aspnet/08.payments/12.production-checklist",{"title":1112,"icon":1113,"items":1114,"path":1127,"stem":1128,"children":1129,"page":59},"Популярні бібліотеки","lucide:box",[1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126],"01.fluent-validation","02.mapster","03.erroror-result-pattern","04.serilog","05.mediatr","06.polly","07.health-checks","08.feature-management","09.fluent-email","10.quest-pdf","11.bogus","12.humanizer-guard","/csharp/aspnet/libraries","01.csharp/11.aspnet/09.libraries",[1130,1134,1138,1142,1146,1150,1154,1158,1162,1166,1170,1174,1178],{"title":1131,"path":1132,"stem":1133},"Валідація з FluentValidation в ASP.NET Core","/csharp/aspnet/libraries/fluent-validation","01.csharp/11.aspnet/09.libraries/01.fluent-validation",{"title":1135,"path":1136,"stem":1137},"Маппінг об","/csharp/aspnet/libraries/mapster","01.csharp/11.aspnet/09.libraries/02.mapster",{"title":1139,"path":1140,"stem":1141},"Обробка помилок з ErrorOr та Result Pattern в ASP.NET Core","/csharp/aspnet/libraries/erroror-result-pattern","01.csharp/11.aspnet/09.libraries/03.erroror-result-pattern",{"title":1143,"path":1144,"stem":1145},"Структуроване логування з Serilog в ASP.NET Core","/csharp/aspnet/libraries/serilog","01.csharp/11.aspnet/09.libraries/04.serilog",{"title":1147,"path":1148,"stem":1149},"CQRS та Mediator з MediatR в ASP.NET Core","/csharp/aspnet/libraries/mediatr","01.csharp/11.aspnet/09.libraries/05.mediatr",{"title":1151,"path":1152,"stem":1153},"Відмовостійкість з Polly в ASP.NET Core","/csharp/aspnet/libraries/polly","01.csharp/11.aspnet/09.libraries/06.polly",{"title":1155,"path":1156,"stem":1157},"Health Checks в ASP.NET Core","/csharp/aspnet/libraries/health-checks","01.csharp/11.aspnet/09.libraries/07.health-checks",{"title":1159,"path":1160,"stem":1161},"Feature Management та Feature Flags в ASP.NET Core","/csharp/aspnet/libraries/feature-management","01.csharp/11.aspnet/09.libraries/08.feature-management",{"title":1163,"path":1164,"stem":1165},"Відправка Email з FluentEmail в ASP.NET Core","/csharp/aspnet/libraries/fluent-email","01.csharp/11.aspnet/09.libraries/09.fluent-email",{"title":1167,"path":1168,"stem":1169},"Генерація PDF з QuestPDF в ASP.NET Core","/csharp/aspnet/libraries/quest-pdf","01.csharp/11.aspnet/09.libraries/10.quest-pdf",{"title":1171,"path":1172,"stem":1173},"Генерація тестових даних з Bogus в ASP.NET Core","/csharp/aspnet/libraries/bogus","01.csharp/11.aspnet/09.libraries/11.bogus",{"title":1175,"path":1176,"stem":1177},"Humanizer та Guard Clauses в ASP.NET Core","/csharp/aspnet/libraries/humanizer-guard","01.csharp/11.aspnet/09.libraries/12.humanizer-guard",{"title":1179,"path":1180,"stem":1181},"План модуля 10.libraries — Популярні бібліотеки ASP.NET","/csharp/aspnet/libraries/plan","01.csharp/11.aspnet/09.libraries/plan",{"title":1183,"icon":1184,"path":1185,"stem":1186,"children":1187,"page":59},"Razor Pages","i-lucide-layout-template","/csharp/aspnet/razor-pages","01.csharp/11.aspnet/10.razor-pages",[1188,1192,1196,1200,1204,1208],{"title":1189,"path":1190,"stem":1191},"Від Minimal API до Razor Pages: концептуальний перехід","/csharp/aspnet/razor-pages/from-minimal-api","01.csharp/11.aspnet/10.razor-pages/01.from-minimal-api",{"title":1193,"path":1194,"stem":1195},"PageModel: логіка сторінки Razor Pages","/csharp/aspnet/razor-pages/page-model","01.csharp/11.aspnet/10.razor-pages/02.page-model",{"title":1197,"path":1198,"stem":1199},"Razor синтаксис: шаблонізатор у .cshtml","/csharp/aspnet/razor-pages/razor-syntax","01.csharp/11.aspnet/10.razor-pages/03.razor-syntax",{"title":1201,"path":1202,"stem":1203},"Tag Helpers: типізований HTML","/csharp/aspnet/razor-pages/tag-helpers","01.csharp/11.aspnet/10.razor-pages/04.tag-helpers",{"title":1205,"path":1206,"stem":1207},"Форми і валідація: повний цикл обробки даних","/csharp/aspnet/razor-pages/forms-validation","01.csharp/11.aspnet/10.razor-pages/05.forms-validation",{"title":1209,"path":1210,"stem":1211},"Практичний проєкт: TaskManager на Razor Pages","/csharp/aspnet/razor-pages/project-task-manager","01.csharp/11.aspnet/10.razor-pages/06.project-task-manager",{"title":1213,"path":1214,"stem":1215,"children":1216,"page":59},"ASP.NET Core MVC","/csharp/aspnet/mvc","01.csharp/11.aspnet/11.mvc",[1217,1221,1225,1229,1233,1237,1241,1245,1249,1253,1257,1261,1265,1269,1273,1277,1281,1285],{"title":1218,"path":1219,"stem":1220},"Патерн MVC: архітектура, що змінила веб","/csharp/aspnet/mvc/mvc-pattern","01.csharp/11.aspnet/11.mvc/01.mvc-pattern",{"title":1222,"path":1223,"stem":1224},"Від Razor Pages до MVC: концептуальний перехід","/csharp/aspnet/mvc/from-razor-pages","01.csharp/11.aspnet/11.mvc/02.from-razor-pages",{"title":1226,"path":1227,"stem":1228},"Controllers та Actions: серце MVC","/csharp/aspnet/mvc/controllers-actions","01.csharp/11.aspnet/11.mvc/03.controllers-actions",{"title":1230,"path":1231,"stem":1232},"Маршрутизація в MVC: Convention vs Attribute Routing","/csharp/aspnet/mvc/routing-mvc","01.csharp/11.aspnet/11.mvc/04.routing-mvc",{"title":1234,"path":1235,"stem":1236},"Model Binding: від HTTP до C#","/csharp/aspnet/mvc/model-binding","01.csharp/11.aspnet/11.mvc/05.model-binding",{"title":1238,"path":1239,"stem":1240},"Views, ViewData, ViewBag, TempData і ViewModel","/csharp/aspnet/mvc/views-viewdata-tempdata","01.csharp/11.aspnet/11.mvc/06.views-viewdata-tempdata",{"title":1242,"path":1243,"stem":1244},"Filters: аспектно-орієнтоване програмування в MVC","/csharp/aspnet/mvc/filters","01.csharp/11.aspnet/11.mvc/07.filters",{"title":1246,"path":1247,"stem":1248},"Areas: структурування великих застосунків","/csharp/aspnet/mvc/areas","01.csharp/11.aspnet/11.mvc/08.areas",{"title":1250,"path":1251,"stem":1252},"View Components: повторювані незалежні блоки UI","/csharp/aspnet/mvc/view-components","01.csharp/11.aspnet/11.mvc/09.view-components",{"title":1254,"path":1255,"stem":1256},"Display та Editor Templates","/csharp/aspnet/mvc/display-editor-templates","01.csharp/11.aspnet/11.mvc/10.display-editor-templates",{"title":1258,"path":1259,"stem":1260},"Валідація: IValidatableObject та FluentValidation","/csharp/aspnet/mvc/validation-advanced","01.csharp/11.aspnet/11.mvc/11.validation-advanced",{"title":1262,"path":1263,"stem":1264},"HTMX: інтерактивність через HTML-атрибути","/csharp/aspnet/mvc/htmx","01.csharp/11.aspnet/11.mvc/12.htmx",{"title":1266,"path":1267,"stem":1268},"HTMX у ASP.NET Core MVC: серверна інтеграція","/csharp/aspnet/mvc/ajax-htmx-mvc","01.csharp/11.aspnet/11.mvc/13.ajax-htmx-mvc",{"title":1270,"path":1271,"stem":1272},"Практичний проєкт: Каталог товарів з HTMX","/csharp/aspnet/mvc/htmx-project","01.csharp/11.aspnet/11.mvc/14.htmx-project",{"title":1274,"path":1275,"stem":1276},"Завантаження та обробка файлів","/csharp/aspnet/mvc/file-upload","01.csharp/11.aspnet/11.mvc/15.file-upload",{"title":1278,"path":1279,"stem":1280},"Глобалізація та Локалізація MVC","/csharp/aspnet/mvc/globalization-localization","01.csharp/11.aspnet/11.mvc/16.globalization-localization",{"title":1282,"path":1283,"stem":1284},"Підсумковий проєкт: Блог-платформа","/csharp/aspnet/mvc/mvc-project","01.csharp/11.aspnet/11.mvc/17.mvc-project",{"title":1286,"path":1287,"stem":1288},"План курсу: ASP.NET Core MVC","/csharp/aspnet/mvc/plan","01.csharp/11.aspnet/11.mvc/plan",{"title":1290,"path":1291,"stem":1292,"children":1293,"page":59},"Web Api","/csharp/aspnet/web-api","01.csharp/11.aspnet/12.web-api",[1294,1298,1302,1306,1310,1314,1318,1322,1326,1330,1334,1338,1342],{"title":1295,"path":1296,"stem":1297},"Від Minimal API до Controller-based API","/csharp/aspnet/web-api/from-minimal-api-to-controllers","01.csharp/11.aspnet/12.web-api/01.from-minimal-api-to-controllers",{"title":1299,"path":1300,"stem":1301},"ControllerBase, ActionResult\u003CT> та Response Types","/csharp/aspnet/web-api/controller-base-actionresult","01.csharp/11.aspnet/12.web-api/02.controller-base-actionresult",{"title":1303,"path":1304,"stem":1305},"Content Negotiation - JSON, XML та власні форматери","/csharp/aspnet/web-api/content-negotiation","01.csharp/11.aspnet/12.web-api/03.content-negotiation",{"title":1307,"path":1308,"stem":1309},"Версіонування API","/csharp/aspnet/web-api/api-versioning","01.csharp/11.aspnet/12.web-api/04.api-versioning",{"title":1311,"path":1312,"stem":1313},"ProblemDetails та структурована обробка помилок","/csharp/aspnet/web-api/problemdetails-error-handling","01.csharp/11.aspnet/12.web-api/05.problemdetails-error-handling",{"title":1315,"path":1316,"stem":1317},"Фільтри у Web API контексті","/csharp/aspnet/web-api/filters-for-api","01.csharp/11.aspnet/12.web-api/06.filters-for-api",{"title":1319,"path":1320,"stem":1321},"Пагінація, фільтрація та сортування","/csharp/aspnet/web-api/pagination-filtering-sorting","01.csharp/11.aspnet/12.web-api/07.pagination-filtering-sorting",{"title":1323,"path":1324,"stem":1325},"HATEOAS та Resource Expansion","/csharp/aspnet/web-api/hateoas-resource-expansion","01.csharp/11.aspnet/12.web-api/08.hateoas-resource-expansion",{"title":1327,"path":1328,"stem":1329},"Гібридна архітектура - Minimal API + Controllers","/csharp/aspnet/web-api/minimal-api-vs-controllers-hybrid","01.csharp/11.aspnet/12.web-api/09.minimal-api-vs-controllers-hybrid",{"title":1331,"path":1332,"stem":1333},"Документація API - Swashbuckle, NSwag та генерація клієнтів","/csharp/aspnet/web-api/api-documentation-generation","01.csharp/11.aspnet/12.web-api/10.api-documentation-generation",{"title":1335,"path":1336,"stem":1337},"Health Checks та моніторинг API","/csharp/aspnet/web-api/health-checks-monitoring","01.csharp/11.aspnet/12.web-api/11.health-checks-monitoring",{"title":1339,"path":1340,"stem":1341},"Підсумковий проєкт - Production-Ready REST API","/csharp/aspnet/web-api/web-api-project","01.csharp/11.aspnet/12.web-api/12.web-api-project",{"title":1343,"path":1344,"stem":1345},"План курсу: ASP.NET Core Web API (Controllers)","/csharp/aspnet/web-api/plan","01.csharp/11.aspnet/12.web-api/plan",{"title":1347,"icon":1348,"path":1349,"stem":1350,"children":1351,"page":59},"Desktop UI","i-lucide-app-window","/csharp/desktop-ui","01.csharp/12.desktop-ui",[1352,1356,1360,1364,1368,1372,1376,1380,1384,1388,1392,1396,1400,1404,1408,1412,1416,1420,1424,1428,1432,1436,1440,1444,1448,1452,1456,1460,1464,1468,1472,1476,1480,1484,1488,1492,1496,1500,1504,1508,1512,1516,1520,1524,1528,1532,1536,1540,1544,1548,1552,1556,1560,1564,1568,1572,1576,1580,1584,1588,1592,1596,1600,1604,1608,1612,1616,1620,1624,1628,1632],{"title":1353,"path":1354,"stem":1355},"Що таке десктопна розробка?","/csharp/desktop-ui/what-is-desktop-dev","01.csharp/12.desktop-ui/01.what-is-desktop-dev",{"title":1357,"path":1358,"stem":1359},"Архітектура WPF — як влаштований графічний інтерфейс","/csharp/desktop-ui/wpf-architecture","01.csharp/12.desktop-ui/02.wpf-architecture",{"title":1361,"path":1362,"stem":1363},"Перший WPF-проєкт — від нуля до вікна","/csharp/desktop-ui/first-wpf-app","01.csharp/12.desktop-ui/03.first-wpf-app",{"title":1365,"path":1366,"stem":1367},"Перший Avalonia-проєкт: WPF для всіх платформ","/csharp/desktop-ui/03a.first-avalonia-app","01.csharp/12.desktop-ui/03a.first-avalonia-app",{"title":1369,"path":1370,"stem":1371},"XAML: декларативний інтерфейс","/csharp/desktop-ui/xaml-basics","01.csharp/12.desktop-ui/04.xaml-basics",{"title":1373,"path":1374,"stem":1375},"Fluent UI у WPF — сучасний дизайн Windows 11","/csharp/desktop-ui/04a.wpf-fluent-ui","01.csharp/12.desktop-ui/04a.wpf-fluent-ui",{"title":1377,"path":1378,"stem":1379},"WPF UI — сучасна бібліотека Fluent контролів","/csharp/desktop-ui/04b.wpf-ui-library","01.csharp/12.desktop-ui/04b.wpf-ui-library",{"title":1381,"path":1382,"stem":1383},"HandyControl — велика бібліотека UI контролів для WPF","/csharp/desktop-ui/04c.handycontrol-library","01.csharp/12.desktop-ui/04c.handycontrol-library",{"title":1385,"path":1386,"stem":1387},"Простори імен та ресурси XAML","/csharp/desktop-ui/xaml-namespaces-resources","01.csharp/12.desktop-ui/05.xaml-namespaces-resources",{"title":1389,"path":1390,"stem":1391},"XAML в Avalonia: ключові відмінності від WPF","/csharp/desktop-ui/05a.avalonia-xaml-differences","01.csharp/12.desktop-ui/05a.avalonia-xaml-differences",{"title":1393,"path":1394,"stem":1395},"Розширення розмітки XAML (Markup Extensions)","/csharp/desktop-ui/xaml-markup-extensions","01.csharp/12.desktop-ui/06.xaml-markup-extensions",{"title":1397,"path":1398,"stem":1399},"Панелі Layout: StackPanel, WrapPanel, DockPanel","/csharp/desktop-ui/layout-panels-part1","01.csharp/12.desktop-ui/07.layout-panels-part1",{"title":1401,"path":1402,"stem":1403},"Grid, Canvas, UniformGrid","/csharp/desktop-ui/layout-panels-part2","01.csharp/12.desktop-ui/07.layout-panels-part2",{"title":1405,"path":1406,"stem":1407},"Просунуті техніки Layout","/csharp/desktop-ui/layout-advanced","01.csharp/12.desktop-ui/08.layout-advanced",{"title":1409,"path":1410,"stem":1411},"Адаптивний Layout та найкращі практики","/csharp/desktop-ui/layout-responsive","01.csharp/12.desktop-ui/09.layout-responsive",{"title":1413,"path":1414,"stem":1415},"Layout в Avalonia: відмінності та нові можливості","/csharp/desktop-ui/09a.layout-avalonia","01.csharp/12.desktop-ui/09a.layout-avalonia",{"title":1417,"path":1418,"stem":1419},"Button, Image, ProgressBar та інші базові контроли","/csharp/desktop-ui/basic-controls","01.csharp/12.desktop-ui/10.basic-controls",{"title":1421,"path":1422,"stem":1423},"Контроли в Avalonia: відмінності від WPF","/csharp/desktop-ui/10a.controls-avalonia","01.csharp/12.desktop-ui/10a.controls-avalonia",{"title":1425,"path":1426,"stem":1427},"Текстові контроли — TextBlock, TextBox, RichTextBox","/csharp/desktop-ui/text-controls","01.csharp/12.desktop-ui/11.text-controls",{"title":1429,"path":1430,"stem":1431},"Контроли вибору — CheckBox, RadioButton, ComboBox, ListBox, DatePicker","/csharp/desktop-ui/selection-controls","01.csharp/12.desktop-ui/12.selection-controls",{"title":1433,"path":1434,"stem":1435},"Content Model — GroupBox, Expander, TabControl, StatusBar","/csharp/desktop-ui/content-controls","01.csharp/12.desktop-ui/13.content-controls",{"title":1437,"path":1438,"stem":1439},"UI/UX принципи десктопних застосунків","/csharp/desktop-ui/13a.ui-ux-principles","01.csharp/12.desktop-ui/13a.ui-ux-principles",{"title":1441,"path":1442,"stem":1443},"Dependency Properties — Концепція та Value Resolution","/csharp/desktop-ui/dependency-properties-part1","01.csharp/12.desktop-ui/14.dependency-properties-part1",{"title":1445,"path":1446,"stem":1447},"Avalonia Property System — StyledProperty та DirectProperty","/csharp/desktop-ui/14a.avalonia-property-system","01.csharp/12.desktop-ui/14a.avalonia-property-system",{"title":1449,"path":1450,"stem":1451},"Attached Properties — Властивості без меж","/csharp/desktop-ui/attached-properties","01.csharp/12.desktop-ui/15.attached-properties",{"title":1453,"path":1454,"stem":1455},"Routed Events — Маршрутизація подій у WPF","/csharp/desktop-ui/routed-events","01.csharp/12.desktop-ui/16.routed-events",{"title":1457,"path":1458,"stem":1459},"Data Binding — Від Code-Behind до Декларативності","/csharp/desktop-ui/data-binding-basics-part1","01.csharp/12.desktop-ui/17.data-binding-basics-part1",{"title":1461,"path":1462,"stem":1463},"INotifyPropertyChanged — Живе оновлення UI","/csharp/desktop-ui/data-binding-basics-part2","01.csharp/12.desktop-ui/17.data-binding-basics-part2",{"title":1465,"path":1466,"stem":1467},"Compiled Bindings в Avalonia — Безпека на етапі компіляції","/csharp/desktop-ui/17a.avalonia-compiled-bindings","01.csharp/12.desktop-ui/17a.avalonia-compiled-bindings",{"title":1469,"path":1470,"stem":1471},"Просунутий Data Binding — ElementName, RelativeSource, MultiBinding","/csharp/desktop-ui/data-binding-advanced","01.csharp/12.desktop-ui/18.data-binding-advanced",{"title":1473,"path":1474,"stem":1475},"Value Converters — Перетворення типів даних у Data Binding","/csharp/desktop-ui/value-converters","01.csharp/12.desktop-ui/19.value-converters",{"title":1477,"path":1478,"stem":1479},"Data Templates — Візуалізація об'єктів у WPF","/csharp/desktop-ui/data-templates","01.csharp/12.desktop-ui/20.data-templates",{"title":1481,"path":1482,"stem":1483},"Collections Binding Part 1 — ObservableCollection та ItemsControl","/csharp/desktop-ui/collections-binding-part1","01.csharp/12.desktop-ui/21.collections-binding-part1",{"title":1485,"path":1486,"stem":1487},"Collections Binding Part 2 — ICollectionView, Filtering, Sorting та Virtualization","/csharp/desktop-ui/collections-binding-part2","01.csharp/12.desktop-ui/21.collections-binding-part2",{"title":1489,"path":1490,"stem":1491},"MVVM Pattern — Від Spaghetti Code до архітектури","/csharp/desktop-ui/mvvm-pattern","01.csharp/12.desktop-ui/22.mvvm-pattern",{"title":1493,"path":1494,"stem":1495},"ViewModel Implementation — Від BaseViewModel до валідації","/csharp/desktop-ui/viewmodel-implementation","01.csharp/12.desktop-ui/23.viewmodel-implementation",{"title":1497,"path":1498,"stem":1499},"Commands — Від event handlers до декларативних команд","/csharp/desktop-ui/commands","01.csharp/12.desktop-ui/24.commands",{"title":1501,"path":1502,"stem":1503},"MVVM Toolkit — MVVM без boilerplate через Source Generators","/csharp/desktop-ui/mvvm-toolkit","01.csharp/12.desktop-ui/25.mvvm-toolkit",{"title":1505,"path":1506,"stem":1507},"Messenger Pattern — Комунікація між ViewModel без прямих посилань","/csharp/desktop-ui/messenger-pattern","01.csharp/12.desktop-ui/26.messenger-pattern",{"title":1509,"path":1510,"stem":1511},"Стилі WPF — CSS для десктопу","/csharp/desktop-ui/styles-basics","01.csharp/12.desktop-ui/27.styles-basics",{"title":1513,"path":1514,"stem":1515},"CSS-like стилі Avalonia","/csharp/desktop-ui/27a.avalonia-css-styling","01.csharp/12.desktop-ui/27a.avalonia-css-styling",{"title":1517,"path":1518,"stem":1519},"Control Templates — Частина 1. Концепція та TemplateBinding","/csharp/desktop-ui/control-templates-part1","01.csharp/12.desktop-ui/28.control-templates-part1",{"title":1521,"path":1522,"stem":1523},"Control Templates — Частина 2. Named Parts та ContentPresenter","/csharp/desktop-ui/control-templates-part2","01.csharp/12.desktop-ui/28.control-templates-part2",{"title":1525,"path":1526,"stem":1527},"Control Themes в Avalonia — нова ера стилізації","/csharp/desktop-ui/28a.avalonia-control-themes","01.csharp/12.desktop-ui/28a.avalonia-control-themes",{"title":1529,"path":1530,"stem":1531},"Triggers та Visual State Manager у WPF","/csharp/desktop-ui/triggers-visual-states","01.csharp/12.desktop-ui/29.triggers-visual-states",{"title":1533,"path":1534,"stem":1535},"Pseudo-classes в Avalonia — замість WPF Triggers","/csharp/desktop-ui/29a.avalonia-pseudo-classes","01.csharp/12.desktop-ui/29a.avalonia-pseudo-classes",{"title":1537,"path":1538,"stem":1539},"Теми та ресурсні словники у WPF","/csharp/desktop-ui/resources-themes","01.csharp/12.desktop-ui/30.resources-themes",{"title":1541,"path":1542,"stem":1543},"Avalonia Themes — Fluent Design та система тематизації","/csharp/desktop-ui/30a.avalonia-themes-fluent","01.csharp/12.desktop-ui/30a.avalonia-themes-fluent",{"title":1545,"path":1546,"stem":1547},"Контроли колекцій — глибоке занурення","/csharp/desktop-ui/collection-controls","01.csharp/12.desktop-ui/31.collection-controls",{"title":1549,"path":1550,"stem":1551},"DataGrid — колонки та базове відображення","/csharp/desktop-ui/datagrid-part1","01.csharp/12.desktop-ui/32.datagrid-part1",{"title":1553,"path":1554,"stem":1555},"DataGrid — сортування, фільтрація, редагування","/csharp/desktop-ui/datagrid-part2","01.csharp/12.desktop-ui/32.datagrid-part2",{"title":1557,"path":1558,"stem":1559},"TreeView та GridView","/csharp/desktop-ui/treeview-listview","01.csharp/12.desktop-ui/33.treeview-listview",{"title":1561,"path":1562,"stem":1563},"Меню, Toolbar, ContextMenu, StatusBar","/csharp/desktop-ui/menus-toolbars","01.csharp/12.desktop-ui/34.menus-toolbars",{"title":1565,"path":1566,"stem":1567},"Навігація та керування вікнами. Частина 1: вікна та сторінки","/csharp/desktop-ui/navigation-windows-part1","01.csharp/12.desktop-ui/35.navigation-windows-part1",{"title":1569,"path":1570,"stem":1571},"Навігація та керування вікнами. Частина 2: MVVM-навігація","/csharp/desktop-ui/navigation-windows-part2","01.csharp/12.desktop-ui/35.navigation-windows-part2",{"title":1573,"path":1574,"stem":1575},"Avalonia — Навігація та діалоги","/csharp/desktop-ui/35a.avalonia-navigation-dialogs","01.csharp/12.desktop-ui/35a.avalonia-navigation-dialogs",{"title":1577,"path":1578,"stem":1579},"Діалоги та File Pickers у WPF","/csharp/desktop-ui/dialogs-file-pickers","01.csharp/12.desktop-ui/36.dialogs-file-pickers",{"title":1581,"path":1582,"stem":1583},"UserControl: компонентний підхід у WPF","/csharp/desktop-ui/user-controls","01.csharp/12.desktop-ui/37.user-controls",{"title":1585,"path":1586,"stem":1587},"Custom Controls: Lookless Controls у WPF","/csharp/desktop-ui/custom-controls","01.csharp/12.desktop-ui/38.custom-controls",{"title":1589,"path":1590,"stem":1591},"Avalonia TemplatedControl — Lookless Controls","/csharp/desktop-ui/38a.avalonia-templated-controls","01.csharp/12.desktop-ui/38a.avalonia-templated-controls",{"title":1593,"path":1594,"stem":1595},"Анімації у WPF: Storyboard та Easing Functions","/csharp/desktop-ui/animations-transitions","01.csharp/12.desktop-ui/39.animations-transitions",{"title":1597,"path":1598,"stem":1599},"Анімації в Avalonia","/csharp/desktop-ui/39a.avalonia-animations","01.csharp/12.desktop-ui/39a.avalonia-animations",{"title":1601,"path":1602,"stem":1603},"2D Графіка та Мультимедіа у WPF","/csharp/desktop-ui/media-graphics","01.csharp/12.desktop-ui/40.media-graphics",{"title":1605,"path":1606,"stem":1607},"Dependency Injection у WPF та Avalonia","/csharp/desktop-ui/di-integration","01.csharp/12.desktop-ui/41.di-integration",{"title":1609,"path":1610,"stem":1611},"SQLite та EF Core у десктопних додатках","/csharp/desktop-ui/data-persistence-part1","01.csharp/12.desktop-ui/42.data-persistence-part1",{"title":1613,"path":1614,"stem":1615},"Repository Pattern та Unit of Work","/csharp/desktop-ui/data-persistence-part2","01.csharp/12.desktop-ui/43.data-persistence-part2",{"title":1617,"path":1618,"stem":1619},"Тестування ViewModels","/csharp/desktop-ui/viewmodel-testing","01.csharp/12.desktop-ui/44.viewmodel-testing",{"title":1621,"path":1622,"stem":1623},"Avalonia Headless Testing — тестування UI без вікон","/csharp/desktop-ui/44a.avalonia-headless-testing","01.csharp/12.desktop-ui/44a.avalonia-headless-testing",{"title":1625,"path":1626,"stem":1627},"Кросплатформна розробка з Avalonia","/csharp/desktop-ui/avalonia-cross-platform","01.csharp/12.desktop-ui/45.avalonia-cross-platform",{"title":1629,"path":1630,"stem":1631},"Пакування та розгортання Avalonia додатків","/csharp/desktop-ui/avalonia-packaging-deployment","01.csharp/12.desktop-ui/46.avalonia-packaging-deployment",{"title":1633,"path":1634,"stem":1635},"Розгортання WPF застосунків","/csharp/desktop-ui/wpf-packaging-deployment","01.csharp/12.desktop-ui/47.wpf-packaging-deployment",{"title":1637,"path":1638,"stem":1639},"C# & .NET: The Ultimate Roadmap","/csharp/roadmap","01.csharp/roadmap",{"title":1641,"icon":1642,"path":1643,"stem":1644,"children":1645,"page":59},"C++","i-devicon-cplusplus","/cpp","02.cpp",[1646,1650,1654,1658,1662,1666,1670,1674,1678,1681,1685,1689,1693,1697,1701,1705,1709,1713,1717,1721,1725,1729,1733,1737,1741,1745,1749,1753,1757,1761],{"title":1647,"path":1648,"stem":1649},"Вступ у програмування та алгоритми","/cpp/intro-algorithms","02.cpp/01.intro-algorithms",{"title":1651,"path":1652,"stem":1653},"Code Style: угоди про оформлення коду","/cpp/code-style","02.cpp/02.code-style",{"title":1655,"path":1656,"stem":1657},"Середовище розробки та перший проєкт","/cpp/ide-setup","02.cpp/03.ide-setup",{"title":1659,"path":1660,"stem":1661},"Вивід даних на екран","/cpp/data-output","02.cpp/04.data-output",{"title":1663,"path":1664,"stem":1665},"Типи даних, змінні та константи","/cpp/data-types-variables","02.cpp/05.data-types-variables",{"title":1667,"path":1668,"stem":1669},"Ввід даних з клавіатури","/cpp/data-input","02.cpp/06.data-input",{"title":1671,"path":1672,"stem":1673},"Оператори, перетворення типів та логічні операції","/cpp/operators-type-conversion","02.cpp/07.operators-type-conversion",{"title":1675,"path":1676,"stem":1677},"Цикли","/cpp/loops","02.cpp/08.loops",{"title":32,"path":1679,"stem":1680},"/cpp/arrays","02.cpp/09.arrays",{"title":1682,"path":1683,"stem":1684},"Алгоритми сортування та аналіз складності","/cpp/sorting","02.cpp/10.sorting",{"title":1686,"path":1687,"stem":1688},"Алгоритми пошуку","/cpp/searching","02.cpp/11.searching",{"title":1690,"path":1691,"stem":1692},"Функції: основи","/cpp/functions-basics","02.cpp/12.functions-basics",{"title":1694,"path":1695,"stem":1696},"Функції: прототипи, область видимості та додаткові можливості","/cpp/functions-scope","02.cpp/13.functions-scope",{"title":1698,"path":1699,"stem":1700},"Функції: перевантаження та шаблони","/cpp/functions-overloading-templates","02.cpp/14.functions-overloading-templates",{"title":1702,"path":1703,"stem":1704},"Вказівники: основи","/cpp/pointers-basics","02.cpp/15.pointers-basics",{"title":1706,"path":1707,"stem":1708},"Посилання (References)","/cpp/references","02.cpp/16.references",{"title":1710,"path":1711,"stem":1712},"Вказівники, const і масиви","/cpp/pointers-const-arrays","02.cpp/17.pointers-const-arrays",{"title":1714,"path":1715,"stem":1716},"Адресна арифметика","/cpp/pointer-arithmetic","02.cpp/18.pointer-arithmetic",{"title":1718,"path":1719,"stem":1720},"Динамічна пам'ять","/cpp/dynamic-memory","02.cpp/19.dynamic-memory",{"title":1722,"path":1723,"stem":1724},"Вказівники типу void","/cpp/void-pointers","02.cpp/20.void-pointers",{"title":1726,"path":1727,"stem":1728},"Вказівники на вказівники","/cpp/pointers-to-pointers","02.cpp/21.pointers-to-pointers",{"title":1730,"path":1731,"stem":1732},"Оператор доступу до членів через вказівник (->)","/cpp/member-access-operator","02.cpp/22.member-access-operator",{"title":1734,"path":1735,"stem":1736},"Цикл for-each (Range-based for)","/cpp/foreach-loop","02.cpp/23.foreach-loop",{"title":1738,"path":1739,"stem":1740},"Вказівники на функції","/cpp/function-pointers","02.cpp/24.function-pointers",{"title":1742,"path":1743,"stem":1744},"Лямбда-вирази","/cpp/lambdas","02.cpp/25.lambdas",{"title":1746,"path":1747,"stem":1748},"Лямбда-захоплення","/cpp/lambda-captures","02.cpp/26.lambda-captures",{"title":1750,"path":1751,"stem":1752},"Еліпсис","/cpp/ellipsis","02.cpp/27.ellipsis",{"title":1754,"path":1755,"stem":1756},"Аргументи командного рядка","/cpp/command-line-arguments","02.cpp/28.command-line-arguments",{"title":1758,"path":1759,"stem":1760},"Перерахування (enum)","/cpp/enum","02.cpp/29.enum",{"title":1762,"path":1763,"stem":1764},"План навчання: Курс C++ — Продовження (Статті 29–60+)","/cpp/curriculum-plan","02.cpp/curriculum-plan",{"title":1766,"icon":1767,"path":1768,"stem":1769,"children":1770,"page":59},"JavaScript","i-devicon-javascript","/javascript","03.javascript",[1771,1797,1851,1873,2177,2215],{"title":1772,"icon":1773,"path":1774,"stem":1775,"children":1776,"page":59},"Events","i-lucide-mouse-pointer-click","/javascript/events","03.javascript/01.events",[1777,1781,1785,1789,1793],{"title":1778,"path":1779,"stem":1780},"Вступ до подій браузера","/javascript/events/intro","03.javascript/01.events/01.intro",{"title":1782,"path":1783,"stem":1784},"Бульбашковий механізм (Bubbling) та занурення (Capturing)","/javascript/events/bubbling-capturing","03.javascript/01.events/02.bubbling-capturing",{"title":1786,"path":1787,"stem":1788},"Делегування подій (Event Delegation)","/javascript/events/delegate-events","03.javascript/01.events/03.delegate-events",{"title":1790,"path":1791,"stem":1792},"Типові дії браузера та preventDefault()","/javascript/events/prevent-default","03.javascript/01.events/04.prevent-default",{"title":1794,"path":1795,"stem":1796},"Запуск користувацьких подій (Custom Events)","/javascript/events/custom-events","03.javascript/01.events/05.custom-events",{"title":1798,"icon":1799,"path":1800,"stem":1801,"children":1802,"page":59},"Network","i-lucide-globe","/javascript/network","03.javascript/02.network",[1803,1807,1811,1815,1819,1823,1827,1831,1835,1839,1843,1847],{"title":1804,"path":1805,"stem":1806},"Fetch API - Сучасний підхід до HTTP-запитів","/javascript/network/01-fetch-api","03.javascript/02.network/01-fetch-api",{"title":1808,"path":1809,"stem":1810},"FormData - Робота з формами та файлами","/javascript/network/02-formdata","03.javascript/02.network/02-formdata",{"title":1812,"path":1813,"stem":1814},"Відстеження прогресу завантаження","/javascript/network/03-download-progress","03.javascript/02.network/03-download-progress",{"title":1816,"path":1817,"stem":1818},"Переривання fetch-запитів","/javascript/network/04-abort-requests","03.javascript/02.network/04-abort-requests",{"title":1820,"path":1821,"stem":1822},"CORS - Запити між різними джерелами","/javascript/network/05-cors","03.javascript/02.network/05-cors",{"title":1824,"path":1825,"stem":1826},"Fetch API - Повний довідник опцій","/javascript/network/06-fetch-options","03.javascript/02.network/06-fetch-options",{"title":1828,"path":1829,"stem":1830},"URL Objects - Робота з посиланнями","/javascript/network/07-url-objects","03.javascript/02.network/07-url-objects",{"title":1832,"path":1833,"stem":1834},"XMLHttpRequest - AJAX та низькорівневі запити","/javascript/network/08-xmlhttprequest","03.javascript/02.network/08-xmlhttprequest",{"title":1836,"path":1837,"stem":1838},"Відновлюване завантаження файлів","/javascript/network/09-resumable-upload","03.javascript/02.network/09-resumable-upload",{"title":1840,"path":1841,"stem":1842},"Cookies, document.cookie та світ після \"Cookiepocalypse\"","/javascript/network/10-cookies","03.javascript/02.network/10-cookies",{"title":1844,"path":1845,"stem":1846},"js-cookie: Керування Cookies без Болю","/javascript/network/11-js-cookie","03.javascript/02.network/11-js-cookie",{"title":1848,"path":1849,"stem":1850},"Axios: Потужний HTTP-клієнт для JavaScript","/javascript/network/12-axios","03.javascript/02.network/12-axios",{"title":1852,"icon":1853,"path":1854,"stem":1855,"children":1856,"page":59},"Bom","i-lucide-monitor","/javascript/bom","03.javascript/03.bom",[1857,1861,1865,1869],{"title":1858,"path":1859,"stem":1860},"LocalStorage, SessionStorage та patterns збереження даних","/javascript/bom/01-localstorage","03.javascript/03.bom/01-localstorage",{"title":1862,"path":1863,"stem":1864},"Location Object - Керування адресою сторінки","/javascript/bom/02-location-object","03.javascript/03.bom/02-location-object",{"title":1866,"path":1867,"stem":1868},"History API - Керування історією браузера","/javascript/bom/03-history-api","03.javascript/03.bom/03-history-api",{"title":1870,"path":1871,"stem":1872},"Navigator Object - Ідентифікація та Можливості Пристрою","/javascript/bom/04-navigator-object","03.javascript/03.bom/04-navigator-object",{"title":1874,"icon":1875,"path":1876,"stem":1877,"children":1878},"React","i-devicon-react","/javascript/react","03.javascript/04.react/index",[1879,1880,1884,1888,1892,1896,1959,1994,2146],{"title":1874,"path":1876,"stem":1877},{"title":1881,"path":1882,"stem":1883},"Робота з Формами в React","/javascript/react/react-forms","03.javascript/04.react/01.react-forms",{"title":1885,"path":1886,"stem":1887},"React Hook Form: Професійна Робота з Формами","/javascript/react/react-hook-form","03.javascript/04.react/02.react-hook-form",{"title":1889,"path":1890,"stem":1891},"React Hook Form: Глибоке Розуміння Архітектури та Оптимізації","/javascript/react/react-hook-form-new","03.javascript/04.react/02.react-hook-form-new",{"title":1893,"path":1894,"stem":1895},"Axios та React: Професійна Архітектура Запитів","/javascript/react/data-fetching-axios","03.javascript/04.react/03.data-fetching-axios",{"title":1897,"icon":132,"path":1898,"stem":1899,"children":1900},"Tanstack Query","/javascript/react/tanstack-query","03.javascript/04.react/04.tanstack-query/index",[1901,1903,1907,1911,1915,1919,1923,1927,1931,1935,1939,1943,1947,1951,1955],{"title":1902,"path":1898,"stem":1899},"TanStack Query: Майстерність Керування Станом Сервера",{"title":1904,"path":1905,"stem":1906},"Парадигма Server State: Чому useEffect недостатньо","/javascript/react/tanstack-query/server-state-paradigm","03.javascript/04.react/04.tanstack-query/01.server-state-paradigm",{"title":1908,"path":1909,"stem":1910},"Встановлення та Налаштування: Фундамент","/javascript/react/tanstack-query/installation-and-devtools","03.javascript/04.react/04.tanstack-query/02.installation-and-devtools",{"title":1912,"path":1913,"stem":1914},"Основи Запитів та Магія Ключів","/javascript/react/tanstack-query/query-basics-and-keys","03.javascript/04.react/04.tanstack-query/03.query-basics-and-keys",{"title":1916,"path":1917,"stem":1918},"Синхронізація Даних: Життєвий Цикл Запиту","/javascript/react/tanstack-query/data-synchronization","03.javascript/04.react/04.tanstack-query/04.data-synchronization",{"title":1920,"path":1921,"stem":1922},"Мутації та Інвалідація: Зміна Даних","/javascript/react/tanstack-query/mutations-and-invalidation","03.javascript/04.react/04.tanstack-query/05.mutations-and-invalidation",{"title":1924,"path":1925,"stem":1926},"Оптимістичні Оновлення: Швидше за Світло","/javascript/react/tanstack-query/optimistic-updates","03.javascript/04.react/04.tanstack-query/06.optimistic-updates",{"title":1928,"path":1929,"stem":1930},"Пагінація та Infinite Scroll","/javascript/react/tanstack-query/pagination-and-load-more","03.javascript/04.react/04.tanstack-query/07.pagination-and-load-more",{"title":1932,"path":1933,"stem":1934},"Просунуті Патерни та Оптимізація","/javascript/react/tanstack-query/advanced-patterns","03.javascript/04.react/04.tanstack-query/08.advanced-patterns",{"title":1936,"path":1937,"stem":1938},"Архітектура та Best Practices","/javascript/react/tanstack-query/architecture-and-best-practices","03.javascript/04.react/04.tanstack-query/09.architecture-and-best-practices",{"title":1940,"path":1941,"stem":1942},"Server-Side Rendering (SSR) та Гідратація","/javascript/react/tanstack-query/server-side-rendering","03.javascript/04.react/04.tanstack-query/10.server-side-rendering",{"title":1944,"path":1945,"stem":1946},"Стратегії Тестування","/javascript/react/tanstack-query/testing-strategies","03.javascript/04.react/04.tanstack-query/11.testing-strategies",{"title":1948,"path":1949,"stem":1950},"Аутентифікація та Обробка Помилок","/javascript/react/tanstack-query/authentication-and-errors","03.javascript/04.react/04.tanstack-query/12.authentication-and-errors",{"title":1952,"path":1953,"stem":1954},"React Suspense та Майбутнє","/javascript/react/tanstack-query/react-suspense","03.javascript/04.react/04.tanstack-query/13.react-suspense",{"title":1956,"path":1957,"stem":1958},"Глибоке Занурення в Продуктивність","/javascript/react/tanstack-query/performance-deep-dive","03.javascript/04.react/04.tanstack-query/14.performance-deep-dive",{"title":1960,"icon":1875,"path":1961,"stem":1962,"children":1963},"React Router","/javascript/react/react-router","03.javascript/04.react/05.react-router/index",[1964,1966,1970,1974,1978,1982,1986,1990],{"title":1965,"path":1961,"stem":1962},"React Router: Навігаційна система сучасного вебу",{"title":1967,"path":1968,"stem":1969},"Налаштування та Базовий Роутинг","/javascript/react/react-router/setup-and-basic-routing","03.javascript/04.react/05.react-router/01.setup-and-basic-routing",{"title":1971,"path":1972,"stem":1973},"Динамічна Навігація","/javascript/react/react-router/navigation-and-links","03.javascript/04.react/05.react-router/02.navigation-and-links",{"title":1975,"path":1976,"stem":1977},"Вкладені Маршрути та Макети","/javascript/react/react-router/nested-routes-and-layouts","03.javascript/04.react/05.react-router/03.nested-routes-and-layouts",{"title":1979,"path":1980,"stem":1981},"Динамічні Маршрути та Параметри","/javascript/react/react-router/dynamic-routing","03.javascript/04.react/05.react-router/04.dynamic-routing",{"title":1983,"path":1984,"stem":1985},"Data APIs: Loaders та Actions","/javascript/react/react-router/data-loading","03.javascript/04.react/05.react-router/05.data-loading",{"title":1987,"path":1988,"stem":1989},"Просунуті Патерни","/javascript/react/react-router/advanced-patterns","03.javascript/04.react/05.react-router/06.advanced-patterns",{"title":1991,"path":1992,"stem":1993},"Legacy Routing: Компонентний підхід","/javascript/react/react-router/legacy-routing","03.javascript/04.react/05.react-router/07.legacy-routing",{"title":1995,"icon":132,"path":1996,"stem":1997,"children":1998},"Redux","/javascript/react/redux","03.javascript/04.react/06.redux/index",[1999,2001,2017,2046,2055,2076,2092,2121],{"title":2000,"path":1996,"stem":1997},"Redux: Еволюція управління станом",{"title":14,"icon":15,"path":2002,"stem":2003,"children":2004,"page":59},"/javascript/react/redux/fundamentals","03.javascript/04.react/06.redux/01.fundamentals",[2005,2009,2013],{"title":2006,"path":2007,"stem":2008},"Вступ до State Management","/javascript/react/redux/fundamentals/intro-state-management","03.javascript/04.react/06.redux/01.fundamentals/01.intro-state-management",{"title":2010,"path":2011,"stem":2012},"Філософія Redux та Три Принципи","/javascript/react/redux/fundamentals/redux-philosophy","03.javascript/04.react/06.redux/01.fundamentals/02.redux-philosophy",{"title":2014,"path":2015,"stem":2016},"Чисті функції та Іммутабельність","/javascript/react/redux/fundamentals/pure-functions-immutability","03.javascript/04.react/06.redux/01.fundamentals/03.pure-functions-immutability",{"title":2018,"icon":132,"path":2019,"stem":2020,"children":2021,"page":59},"Classic Redux","/javascript/react/redux/classic-redux","03.javascript/04.react/06.redux/02.classic-redux",[2022,2026,2030,2034,2038,2042],{"title":2023,"path":2024,"stem":2025},"Створення Store (Classic Redux)","/javascript/react/redux/classic-redux/store-setup","03.javascript/04.react/06.redux/02.classic-redux/01.store-setup",{"title":2027,"path":2028,"stem":2029},"Actions, Constants та Action Creators","/javascript/react/redux/classic-redux/actions-constants","03.javascript/04.react/06.redux/02.classic-redux/02.actions-constants",{"title":2031,"path":2032,"stem":2033},"Логіка Reducers","/javascript/react/redux/classic-redux/reducers","03.javascript/04.react/06.redux/02.classic-redux/03.reducers",{"title":2035,"path":2036,"stem":2037},"Комбінування Reducers (Root Reducer)","/javascript/react/redux/classic-redux/data-flow","03.javascript/04.react/06.redux/02.classic-redux/04.data-flow",{"title":2039,"path":2040,"stem":2041},"Підключення до React (React-Redux)","/javascript/react/redux/classic-redux/react-redux-connection","03.javascript/04.react/06.redux/02.classic-redux/05.react-redux-connection",{"title":2043,"path":2044,"stem":2045},"Middleware та Асинхронність (Redux Thunk)","/javascript/react/redux/classic-redux/middleware-thunk","03.javascript/04.react/06.redux/02.classic-redux/06.middleware-thunk",{"title":2047,"icon":132,"path":2048,"stem":2049,"children":2050,"page":59},"Transition To Rtk","/javascript/react/redux/transition-to-rtk","03.javascript/04.react/06.redux/03.transition-to-rtk",[2051],{"title":2052,"path":2053,"stem":2054},"Проблеми класичного Redux","/javascript/react/redux/transition-to-rtk/problems-with-classic","03.javascript/04.react/06.redux/03.transition-to-rtk/01.problems-with-classic",{"title":2056,"icon":132,"path":2057,"stem":2058,"children":2059,"page":59},"Redux Toolkit","/javascript/react/redux/redux-toolkit","03.javascript/04.react/06.redux/04.redux-toolkit",[2060,2064,2068,2072],{"title":2061,"path":2062,"stem":2063},"Налаштування Store з configureStore","/javascript/react/redux/redux-toolkit/configure-store","03.javascript/04.react/06.redux/04.redux-toolkit/01.configure-store",{"title":2065,"path":2066,"stem":2067},"createSlice: Революція в Redux","/javascript/react/redux/redux-toolkit/create-slice","03.javascript/04.react/06.redux/04.redux-toolkit/02.create-slice",{"title":2069,"path":2070,"stem":2071},"Асинхронність з createAsyncThunk","/javascript/react/redux/redux-toolkit/async-thunks","03.javascript/04.react/06.redux/04.redux-toolkit/03.async-thunks",{"title":2073,"path":2074,"stem":2075},"04. Entity Adapter: Керування нормалізованим станом","/javascript/react/redux/redux-toolkit/entity-adapter","03.javascript/04.react/06.redux/04.redux-toolkit/04.entity-adapter",{"title":2077,"icon":92,"path":2078,"stem":2079,"children":2080,"page":59},"Advanced","/javascript/react/redux/advanced","03.javascript/04.react/06.redux/05.advanced",[2081,2085,2089],{"title":2082,"path":2083,"stem":2084},"Мемоізація та Селектори: Повний Гайд по Reselect","/javascript/react/redux/advanced/selectors-reselect","03.javascript/04.react/06.redux/05.advanced/01.selectors-reselect",{"title":2086,"path":2087,"stem":2088},"RTK Query: Архітектура Серверного Кешу","/javascript/react/redux/advanced/rtk-query-intro","03.javascript/04.react/06.redux/05.advanced/02.rtk-query-intro",{"title":1936,"path":2090,"stem":2091},"/javascript/react/redux/advanced/architecture-best-practices","03.javascript/04.react/06.redux/05.advanced/03.architecture-best-practices",{"title":2093,"icon":132,"path":2094,"stem":2095,"children":2096,"page":59},"Project Kanban","/javascript/react/redux/project-kanban","03.javascript/04.react/06.redux/06.project-kanban",[2097,2101,2105,2109,2113,2117],{"title":2098,"path":2099,"stem":2100},"Проєкт: Kanban Board (Trello Clone)","/javascript/react/redux/project-kanban/project-overview","03.javascript/04.react/06.redux/06.project-kanban/01.project-overview",{"title":2102,"path":2103,"stem":2104},"Налаштування та Типізація","/javascript/react/redux/project-kanban/setup-and-types","03.javascript/04.react/06.redux/06.project-kanban/02.setup-and-types",{"title":2106,"path":2107,"stem":2108},"Board Slice: Серце Дошки","/javascript/react/redux/project-kanban/board-slice","03.javascript/04.react/06.redux/06.project-kanban/03.board-slice",{"title":2110,"path":2111,"stem":2112},"Логіка Drag & Drop","/javascript/react/redux/project-kanban/drag-and-drop-logic","03.javascript/04.react/06.redux/06.project-kanban/04.drag-and-drop-logic",{"title":2114,"path":2115,"stem":2116},"Інтеграція з RTK Query","/javascript/react/redux/project-kanban/rtk-query-integration","03.javascript/04.react/06.redux/06.project-kanban/05.rtk-query-integration",{"title":2118,"path":2119,"stem":2120},"Optimistic Updates","/javascript/react/redux/project-kanban/optimistic-updates","03.javascript/04.react/06.redux/06.project-kanban/06.optimistic-updates",{"title":2122,"icon":132,"path":2123,"stem":2124,"children":2125,"page":59},"Testing","/javascript/react/redux/testing","03.javascript/04.react/06.redux/07.testing",[2126,2130,2134,2138,2142],{"title":2127,"path":2128,"stem":2129},"Тестування Redux","/javascript/react/redux/testing/intro-testing","03.javascript/04.react/06.redux/07.testing/01.intro-testing",{"title":2131,"path":2132,"stem":2133},"Тестування Reducers","/javascript/react/redux/testing/testing-reducers","03.javascript/04.react/06.redux/07.testing/02.testing-reducers",{"title":2135,"path":2136,"stem":2137},"Тестування Селекторів","/javascript/react/redux/testing/testing-selectors","03.javascript/04.react/06.redux/07.testing/03.testing-selectors",{"title":2139,"path":2140,"stem":2141},"Тестування Компонентів (Integration)","/javascript/react/redux/testing/testing-components","03.javascript/04.react/06.redux/07.testing/04.testing-components",{"title":2143,"path":2144,"stem":2145},"Тестування Async Thunks","/javascript/react/redux/testing/testing-thunks","03.javascript/04.react/06.redux/07.testing/05.testing-thunks",{"title":2147,"icon":132,"path":2148,"stem":2149,"children":2150},"Ui Libraries","/javascript/react/ui-libraries","03.javascript/04.react/07.ui-libraries/index",[2151,2153,2157,2161,2165,2169,2173],{"title":2152,"path":2148,"stem":2149},"UI Бібліотеки в React",{"title":2154,"path":2155,"stem":2156},"Вступ до UI Бібліотек: Навіщо Винаходити Велосипед Двічі?","/javascript/react/ui-libraries/introduction-to-ui-libraries","03.javascript/04.react/07.ui-libraries/01.introduction-to-ui-libraries",{"title":2158,"path":2159,"stem":2160},"Філософія shadcn/ui: \"Not a Component Library\"","/javascript/react/ui-libraries/shadcn-philosophy","03.javascript/04.react/07.ui-libraries/02.shadcn-philosophy",{"title":2162,"path":2163,"stem":2164},"Установка та Налаштування shadcn/ui","/javascript/react/ui-libraries/shadcn-installation","03.javascript/04.react/07.ui-libraries/03.shadcn-installation",{"title":2166,"path":2167,"stem":2168},"Базові Компоненти shadcn/ui: Фундамент Інтерфейсу","/javascript/react/ui-libraries/shadcn-components-basics","03.javascript/04.react/07.ui-libraries/04.shadcn-components-basics",{"title":2170,"path":2171,"stem":2172},"Компоненти Форм: Побудова Інтерактивних Form","/javascript/react/ui-libraries/shadcn-components-forms","03.javascript/04.react/07.ui-libraries/05.shadcn-components-forms",{"title":2174,"path":2175,"stem":2176},"Складні Компоненти: Dialog, Dropdown, Table та Command","/javascript/react/ui-libraries/shadcn-components-advanced","03.javascript/04.react/07.ui-libraries/06.shadcn-components-advanced",{"title":2178,"icon":2179,"path":2180,"stem":2181,"children":2182,"page":59},"TypeScript","i-devicon-typescript","/javascript/typescript","03.javascript/05.typescript",[2183,2187,2191,2195,2199,2203,2207,2211],{"title":2184,"path":2185,"stem":2186},"TypeScript: Броня для вашого коду","/javascript/typescript/intro-and-basic-types","03.javascript/05.typescript/01.intro-and-basic-types",{"title":2188,"path":2189,"stem":2190},"Майстерність Моделювання Даних: Інтерфейси та Просунуті Типи","/javascript/typescript/interfaces-and-advanced-types","03.javascript/05.typescript/02.interfaces-and-advanced-types",{"title":2192,"path":2193,"stem":2194},"Алхімія Типів: Generics та Utility Types","/javascript/typescript/generics-and-utilities","03.javascript/05.typescript/03.generics-and-utilities",{"title":2196,"path":2197,"stem":2198},"Архітектура та Шаблони: Класи в TypeScript","/javascript/typescript/classes-and-oop","03.javascript/05.typescript/04.classes-and-oop",{"title":2200,"path":2201,"stem":2202},"Продакшн та Екосистема: Advanced Config & Workflow","/javascript/typescript/advanced-patterns-and-config","03.javascript/05.typescript/05.advanced-patterns-and-config",{"title":2204,"path":2205,"stem":2206},"TypeScript у світі React","/javascript/typescript/react-basics","03.javascript/05.typescript/06.react-basics",{"title":2208,"path":2209,"stem":2210},"React + TypeScript: Продвинуті патерни","/javascript/typescript/react-advanced","03.javascript/05.typescript/07.react-advanced",{"title":2212,"path":2213,"stem":2214},"React + TypeScript: Екосистема та бібліотеки","/javascript/typescript/react-ecosystem","03.javascript/05.typescript/08.react-ecosystem",{"title":2216,"path":2217,"stem":2218},"Atomic Design","/javascript/atomic-design","03.javascript/2.atomic-design",{"title":2220,"icon":2221,"path":2222,"stem":2223,"children":2224,"page":59},"Java","i-devicon-java","/java","04.java",[2225,2228,2231,2235,2239,2243,2247],{"title":162,"path":2226,"stem":2227},"/java/data-mapper-part1","04.java/01.data-mapper-part1",{"title":166,"path":2229,"stem":2230},"/java/data-mapper-part2","04.java/02.data-mapper-part2",{"title":2232,"path":2233,"stem":2234},"Service Layer: Організація бізнес-логіки","/java/service-layer","04.java/03.service-layer",{"title":2236,"path":2237,"stem":2238},"Rich Domain Model та State Pattern","/java/rich-domain-model","04.java/04.rich-domain-model",{"title":2240,"path":2241,"stem":2242},"Патерни для складної бізнес-логіки","/java/business-logic-patterns","04.java/05.business-logic-patterns",{"title":2244,"path":2245,"stem":2246},"Обробка помилок та валідація","/java/error-handling-validation","04.java/06.error-handling-validation",{"title":2248,"path":2249,"stem":2250,"children":2251,"page":59},"Проектування баз даних","/java/pr2","04.java/pr2",[2252,2256,2260,2264,2268,2272,2276,2280,2284,2288,2292,2296,2300,2304,2308,2312,2316,2320,2324,2328,2332,2336,2340,2344,2348],{"title":2253,"path":2254,"stem":2255},"Концептуальне моделювання: Мистецтво розуміння предметної області","/java/pr2/conceptual-modeling","04.java/pr2/01.conceptual-modeling",{"title":2257,"path":2258,"stem":2259},"Логічне моделювання: Від бізнес-ідей до структур даних","/java/pr2/logical-modeling","04.java/pr2/02.logical-modeling",{"title":2261,"path":2262,"stem":2263},"Нормалізація: Гігієна даних та боротьба з аномаліями","/java/pr2/normalization","04.java/pr2/03.normalization",{"title":2265,"path":2266,"stem":2267},"Фізична схема: Від абстракції до DDL","/java/pr2/physical-schema","04.java/pr2/04.physical-schema",{"title":2269,"path":2270,"stem":2271},"Архітектурна класифікація таблиць","/java/pr2/table-classification","04.java/pr2/05.table-classification",{"title":2273,"path":2274,"stem":2275},"Database Migrations: Версіонування схеми з Flyway","/java/pr2/database-migrations","04.java/pr2/06.database-migrations",{"title":2277,"path":2278,"stem":2279},"А що, якби це була не реляційна БД?","/java/pr2/beyond-relational","04.java/pr2/07.beyond-relational",{"title":2281,"path":2282,"stem":2283},"Object-Relational Impedance Mismatch: Два світи, що не хочуть дружити","/java/pr2/impedance-mismatch","04.java/pr2/09.impedance-mismatch",{"title":2285,"path":2286,"stem":2287},"JDBC: Перший контакт із базою даних","/java/pr2/jdbc-fundamentals","04.java/pr2/10.jdbc-fundamentals",{"title":2289,"path":2290,"stem":2291},"Якість коду: Spotless, SpotBugs та SonarQube","/java/pr2/10a.code-quality","04.java/pr2/10a.code-quality",{"title":2293,"path":2294,"stem":2295},"Connection Pool: Патерн Object Pool для JDBC-з'єднань","/java/pr2/connection-pool","04.java/pr2/11.connection-pool",{"title":2297,"path":2298,"stem":2299},"Row Data Gateway: Об'єкт як обгортка рядка таблиці","/java/pr2/row-data-gateway","04.java/pr2/12.row-data-gateway",{"title":2301,"path":2302,"stem":2303},"Table Data Gateway: Фасад таблиці як архітектурний відступ","/java/pr2/table-data-gateway","04.java/pr2/13.table-data-gateway",{"title":2305,"path":2306,"stem":2307},"Repository + Data Mapper: Правильна шарова архітектура з JDBC","/java/pr2/repository-data-mapper","04.java/pr2/14.repository-data-mapper",{"title":2309,"path":2310,"stem":2311},"Identity Map: Кешування сутностей у рамках сесії","/java/pr2/identity-map","04.java/pr2/15.identity-map",{"title":2313,"path":2314,"stem":2315},"Unit of Work: Відстеження змін і координація JDBC-транзакцій","/java/pr2/unit-of-work","04.java/pr2/16.unit-of-work",{"title":2317,"path":2318,"stem":2319},"Strategy: Замінювані SQL-стратегії для підтримки різних СУБД","/java/pr2/strategy-sql","04.java/pr2/17.strategy-sql",{"title":2321,"path":2322,"stem":2323},"Proxy: Lazy Loading для One-To-Many колекцій","/java/pr2/proxy-lazy-loading","04.java/pr2/18.proxy-lazy-loading",{"title":2325,"path":2326,"stem":2327},"Generic Repository через Java Reflection: анотації та динамічний SQL","/java/pr2/generic-repository-reflection","04.java/pr2/19.generic-repository-reflection",{"title":2329,"path":2330,"stem":2331},"Specification Pattern: Композиція бізнес-правил для складних запитів","/java/pr2/specification-pattern","04.java/pr2/20.specification-pattern",{"title":2333,"path":2334,"stem":2335},"Розширені можливості Specification Pattern: підзапити, агрегації та гібридний підхід","/java/pr2/20a.advanced-specifications","04.java/pr2/20a.advanced-specifications",{"title":2337,"path":2338,"stem":2339},"Асинхронність у JDBC: Від блокуючих викликів до CompletableFuture","/java/pr2/asynchronous-jdbc","04.java/pr2/21.asynchronous-jdbc",{"title":2341,"path":2342,"stem":2343},"Інтеграційне тестування JDBC-репозиторіїв: Embedded H2 та патерн AAA","/java/pr2/integration-testing-h2","04.java/pr2/22.integration-testing-h2",{"title":2345,"path":2346,"stem":2347},"Testcontainers: Тестування з реальною PostgreSQL у Docker-контейнерах","/java/pr2/integration-testing-testcontainers","04.java/pr2/23.integration-testing-testcontainers",{"title":2349,"path":2350,"stem":2351},"Модуль \"Проектування реляційних баз даних\" для 04.java/pr2","/java/pr2/implementation_plan","04.java/pr2/implementation_plan",{"title":2353,"icon":2354,"path":2355,"stem":2356,"children":2357,"page":59},"Бази даних","i-lucide-database","/databases","06.databases",[2358,2388,2411,2448,2477,2495,2529,2541,2550],{"title":2359,"icon":2360,"path":2361,"stem":2362,"children":2363,"page":59},"Intro","i-lucide-play","/databases/intro","06.databases/01.intro",[2364,2368,2372,2376,2380,2384],{"title":2365,"path":2366,"stem":2367},"Введення в теорію баз даних","/databases/intro/introduction-to-databases","06.databases/01.intro/01.introduction-to-databases",{"title":2369,"path":2370,"stem":2371},"Реляційна модель даних","/databases/intro/relational-model-theory","06.databases/01.intro/02.relational-model-theory",{"title":2373,"path":2374,"stem":2375},"ER-моделювання","/databases/intro/er-modeling","06.databases/01.intro/03.er-modeling",{"title":2377,"path":2378,"stem":2379},"Логічне проектування БД","/databases/intro/logical-schema","06.databases/01.intro/04.logical-schema",{"title":2381,"path":2382,"stem":2383},"Класифікація таблиць","/databases/intro/table-classification","06.databases/01.intro/05.table-classification",{"title":2385,"path":2386,"stem":2387},"PlantUML для баз даних","/databases/intro/plantuml-diagrams","06.databases/01.intro/06.plantuml-diagrams",{"title":2389,"icon":2354,"path":2390,"stem":2391,"children":2392,"page":59},"MS SQL Server Start","/databases/ms-sql-server-start","06.databases/02.ms-sql-server-start",[2393,2397,2403,2407],{"title":2394,"path":2395,"stem":2396},"Типи даних у MS SQL Server","/databases/ms-sql-server-start/data-types","06.databases/02.ms-sql-server-start/01.data-types",{"title":2398,"path":2399,"stem":2400,"children":2401},"Індекси у MS SQL Server","/databases/ms-sql-server-start/sql-indexes","06.databases/02.ms-sql-server-start/02.sql-indexes",[2402],{"title":2398,"path":2399,"stem":2400},{"title":2404,"path":2405,"stem":2406},"Системні бази даних MS SQL Server","/databases/ms-sql-server-start/system-databases","06.databases/02.ms-sql-server-start/03.system-databases",{"title":2408,"path":2409,"stem":2410},"Огляд мови SQL та запитів","/databases/ms-sql-server-start/sql-queries-overview","06.databases/02.ms-sql-server-start/04.sql-queries-overview",{"title":2412,"icon":2354,"path":2413,"stem":2414,"children":2415,"page":59},"SQL","/databases/sql","06.databases/03.sql",[2416,2420,2424,2428,2432,2436,2440,2444],{"title":2417,"path":2418,"stem":2419},"Налаштування демонстраційної бази даних","/databases/sql/sample-database-setup","06.databases/03.sql/00.sample-database-setup",{"title":2421,"path":2422,"stem":2423},"DDL - Створення таблиць (CREATE TABLE)","/databases/sql/ddl-create-table","06.databases/03.sql/01.ddl-create-table",{"title":2425,"path":2426,"stem":2427},"DDL - Зміна та видалення таблиць (ALTER, DROP)","/databases/sql/ddl-alter-drop-table","06.databases/03.sql/02.ddl-alter-drop-table",{"title":2429,"path":2430,"stem":2431},"SELECT запити - Основи","/databases/sql/select-queries-fundamentals","06.databases/03.sql/03.select-queries-fundamentals",{"title":2433,"path":2434,"stem":2435},"SELECT запити - Розширені можливості","/databases/sql/select-queries-advanced","06.databases/03.sql/04.select-queries-advanced",{"title":2437,"path":2438,"stem":2439},"INSERT запити - Додавання даних","/databases/sql/insert-queries","06.databases/03.sql/05.insert-queries",{"title":2441,"path":2442,"stem":2443},"UPDATE та DELETE запити","/databases/sql/update-delete-queries","06.databases/03.sql/06.update-delete-queries",{"title":2445,"path":2446,"stem":2447},"Транзакції в SQL","/databases/sql/transactions","06.databases/03.sql/07.transactions",{"title":2449,"icon":2354,"path":2450,"stem":2451,"children":2452,"page":59},"Multi Table Databases","/databases/multi-table-databases","06.databases/04.multi-table-databases",[2453,2457,2461,2465,2469,2473],{"title":2454,"path":2455,"stem":2456},"Зв'язки та нормалізація БД","/databases/multi-table-databases/relationships-and-normalization","06.databases/04.multi-table-databases/00.relationships-and-normalization",{"title":2458,"path":2459,"stem":2460},"INNER JOIN - З'єднання таблиць","/databases/multi-table-databases/inner-join","06.databases/04.multi-table-databases/01.inner-join",{"title":2462,"path":2463,"stem":2464},"OUTER JOINs - LEFT, RIGHT, FULL","/databases/multi-table-databases/outer-joins","06.databases/04.multi-table-databases/02.outer-joins",{"title":2466,"path":2467,"stem":2468},"CROSS та SELF JOINs","/databases/multi-table-databases/cross-self-joins","06.databases/04.multi-table-databases/03.cross-self-joins",{"title":2470,"path":2471,"stem":2472},"Підзапити (Subqueries)","/databases/multi-table-databases/subqueries","06.databases/04.multi-table-databases/04.subqueries",{"title":2474,"path":2475,"stem":2476},"Агрегації з JOIN","/databases/multi-table-databases/aggregations-with-joins","06.databases/04.multi-table-databases/05.aggregations-with-joins",{"title":2478,"icon":2479,"path":2480,"stem":2481,"children":2482,"page":59},"Aggregate Functions","i-lucide-calculator","/databases/aggregate-functions","06.databases/05.aggregate-functions",[2483,2487,2491],{"title":2484,"path":2485,"stem":2486},"Функції агрегування в MS SQL Server","/databases/aggregate-functions/introduction-aggregate-functions","06.databases/05.aggregate-functions/01.introduction-aggregate-functions",{"title":2488,"path":2489,"stem":2490},"Групування даних в MS SQL Server","/databases/aggregate-functions/grouping-data","06.databases/05.aggregate-functions/02.grouping-data",{"title":2492,"path":2493,"stem":2494},"Підзапити з агрегатними функціями","/databases/aggregate-functions/subqueries-aggregates","06.databases/05.aggregate-functions/03.subqueries-aggregates",{"title":2496,"icon":2497,"path":2498,"stem":2499,"children":2500,"page":59},"Тригери та зберігаємі процедури","i-lucide-database-zap","/databases/triggers-stored-procedures","06.databases/07.triggers-stored-procedures",[2501,2505,2509,2513,2517,2521,2525],{"title":2502,"path":2503,"stem":2504},"DML-тригери","/databases/triggers-stored-procedures/dml-triggers","06.databases/07.triggers-stored-procedures/01.dml-triggers",{"title":2506,"path":2507,"stem":2508},"DDL-тригери","/databases/triggers-stored-procedures/ddl-triggers","06.databases/07.triggers-stored-procedures/02.ddl-triggers",{"title":2510,"path":2511,"stem":2512},"Transact-SQL розширення","/databases/triggers-stored-procedures/transact-sql-extensions","06.databases/07.triggers-stored-procedures/03.transact-sql-extensions",{"title":2514,"path":2515,"stem":2516},"Транзакції","/databases/triggers-stored-procedures/transactions","06.databases/07.triggers-stored-procedures/04.transactions",{"title":2518,"path":2519,"stem":2520},"Зберігаємі процедури","/databases/triggers-stored-procedures/stored-procedures","06.databases/07.triggers-stored-procedures/05.stored-procedures",{"title":2522,"path":2523,"stem":2524},"Користувацькі функції","/databases/triggers-stored-procedures/user-defined-functions","06.databases/07.triggers-stored-procedures/06.user-defined-functions",{"title":2526,"path":2527,"stem":2528},"Безпека баз даних","/databases/triggers-stored-procedures/security","06.databases/07.triggers-stored-procedures/08.security",{"title":2526,"icon":793,"path":2530,"stem":2531,"children":2532,"page":59},"/databases/security","06.databases/08.security",[2533,2537],{"title":2534,"path":2535,"stem":2536},"Вступ до безпеки баз даних","/databases/security/introduction","06.databases/08.security/01.introduction",{"title":2538,"path":2539,"stem":2540},"Системні представлення та метадані","/databases/security/system-views","06.databases/08.security/02.system-views",{"title":2542,"icon":2543,"path":2544,"stem":2545,"children":2546,"page":59},"Резервне копіювання та відновлення","i-lucide-database-backup","/databases/backup-recovery","06.databases/09.backup-recovery",[2547],{"title":2542,"path":2548,"stem":2549},"/databases/backup-recovery/backup-restore","06.databases/09.backup-recovery/01.backup-restore",{"title":2551,"icon":2552,"path":2553,"stem":2554,"children":2555,"page":59},"Повнотекстовий пошук","i-lucide-search","/databases/full-text-search","06.databases/10.full-text-search",[2556],{"title":2551,"path":2557,"stem":2558},"/databases/full-text-search/full-text-search","06.databases/10.full-text-search/01.full-text-search",{"title":2560,"icon":2561,"path":2562,"stem":2563,"children":2564,"page":59},"Tools","i-lucide-wrench","/tools","07.tools",[2565],{"title":2566,"icon":2567,"path":2568,"stem":2569,"children":2570},"Docker","i-simple-icons-docker","/tools/docker","07.tools/01.docker/index",[2571,2573,2577,2581,2585,2589,2593,2597,2601,2605,2609,2613,2617,2621,2625,2629,2633,2637],{"title":2572,"path":2568,"stem":2569},"Docker: від нуля до production",{"title":2574,"path":2575,"stem":2576},"Контейнеризація — від проблеми до рішення","/tools/docker/containerization-concept","07.tools/01.docker/01.containerization-concept",{"title":2578,"path":2579,"stem":2580},"Docker — що це і навіщо?","/tools/docker/docker-what-and-why","07.tools/01.docker/02.docker-what-and-why",{"title":2582,"path":2583,"stem":2584},"Архітектура Docker Engine","/tools/docker/docker-architecture","07.tools/01.docker/03.docker-architecture",{"title":2586,"path":2587,"stem":2588},"Встановлення Docker","/tools/docker/installation","07.tools/01.docker/04.installation",{"title":2590,"path":2591,"stem":2592},"Перший контейнер — docker run","/tools/docker/first-container","07.tools/01.docker/05.first-container",{"title":2594,"path":2595,"stem":2596},"Життєвий цикл контейнера","/tools/docker/container-lifecycle","07.tools/01.docker/06.container-lifecycle",{"title":2598,"path":2599,"stem":2600},"Docker Images — фундаментальні концепції","/tools/docker/docker-images-fundamentals","07.tools/01.docker/07.docker-images-fundamentals",{"title":2602,"path":2603,"stem":2604},"Dockerfile — основи","/tools/docker/dockerfile-basics","07.tools/01.docker/08.dockerfile-basics",{"title":2606,"path":2607,"stem":2608},"Dockerfile — просунуті техніки","/tools/docker/dockerfile-advanced","07.tools/01.docker/09.dockerfile-advanced",{"title":2610,"path":2611,"stem":2612},"Build Context та кешування шарів","/tools/docker/build-context-and-cache","07.tools/01.docker/10.build-context-and-cache",{"title":2614,"path":2615,"stem":2616},"Реєстри Docker-образів","/tools/docker/image-registries","07.tools/01.docker/11.image-registries",{"title":2618,"path":2619,"stem":2620},"Контейнеризація .NET додатків","/tools/docker/dotnet-containerization","07.tools/01.docker/12.dotnet-containerization",{"title":2622,"path":2623,"stem":2624},"Томи та збереження даних","/tools/docker/volumes-and-data","07.tools/01.docker/13.volumes-and-data",{"title":2626,"path":2627,"stem":2628},"Основи мережі в Docker","/tools/docker/networking-basics","07.tools/01.docker/14.networking-basics",{"title":2630,"path":2631,"stem":2632},"Змінні оточення та конфігурація","/tools/docker/environment-and-configuration","07.tools/01.docker/15.environment-and-configuration",{"title":2634,"path":2635,"stem":2636},"Docker Compose — оркестрація контейнерів","/tools/docker/docker-compose-basics","07.tools/01.docker/16.docker-compose-basics",{"title":2638,"path":2639,"stem":2640},"Docker Compose — Multi-Service застосунки","/tools/docker/compose-multi-service","07.tools/01.docker/17.compose-multi-service",{"title":2642,"icon":2643,"path":2644,"stem":2645,"children":2646,"page":59},"Software Engineering","i-lucide-code-2","/software-engineering","09.software-engineering",[2647,2651,2655,2659,2663,2667,2671,2675,2679,2683,2687],{"title":2648,"path":2649,"stem":2650},"1. Аналіз предметної області. Експертні знання та складність","/software-engineering/intro.subdomains","09.software-engineering/01.intro.subdomains",{"title":2652,"path":2653,"stem":2654},"2. Обмежені контексти. Інтеграція обмежених контекстів","/software-engineering/integrating-limited-contexts","09.software-engineering/02.integrating-limited-contexts",{"title":2656,"path":2657,"stem":2658},"3. Реалізація простої бізнес-логіки","/software-engineering/simple","09.software-engineering/03.simple",{"title":2660,"path":2661,"stem":2662},"4. Опрацювання складної бізнес-логіки","/software-engineering/complex-business-logic","09.software-engineering/04.complex-business-logic",{"title":2664,"path":2665,"stem":2666},"5. Моделювання фактора часу. Подієво-орієнтована архітектура.","/software-engineering/modelling-the-time-factor","09.software-engineering/05.modelling-the-time-factor",{"title":2668,"path":2669,"stem":2670},"6. Архітектурні патерни","/software-engineering/architectural-patterns","09.software-engineering/06.architectural-patterns",{"title":2672,"path":2673,"stem":2674},"Паттерни взаємодії","/software-engineering/patterns-of-interaction","09.software-engineering/07.patterns-of-interaction",{"title":2676,"path":2677,"stem":2678},"Евристика проєктування","/software-engineering/design-heuristics","09.software-engineering/08.design-heuristics",{"title":2680,"path":2681,"stem":2682},"Еволюція проєктних рішень","/software-engineering/evolution-of-design-solutions","09.software-engineering/09.evolution-of-design-solutions",{"title":2684,"path":2685,"stem":2686},"EventStorming","/software-engineering/eventstorming","09.software-engineering/10.eventstorming",{"title":2688,"path":2689,"stem":2690},"DDD на практиці","/software-engineering/ddd-in-practice","09.software-engineering/11.ddd-in-practice",{"title":2692,"icon":943,"path":2693,"stem":2694,"children":2695,"page":59},"DDD","/ddd","10.ddd",[2696,2700,2704,2708,2712,2716,2720,2724,2728,2732,2736,2740,2744],{"title":2697,"path":2698,"stem":2699},"Аналіз предметної області","/ddd/domain-analysis","10.ddd/01.domain-analysis",{"title":2701,"path":2702,"stem":2703},"Експертні знання про предметну область","/ddd/domain-expert-knowledge","10.ddd/02.domain-expert-knowledge",{"title":2705,"path":2706,"stem":2707},"Як осмислити складність предметної області","/ddd/managing-domain-complexity","10.ddd/03.managing-domain-complexity",{"title":2709,"path":2710,"stem":2711},"Інтеграція обмежених контекстів","/ddd/bounded-context-integration","10.ddd/04.bounded-context-integration",{"title":2713,"path":2714,"stem":2715},"Реалізація простої бізнес-логіки","/ddd/simple-business-logic","10.ddd/05.simple-business-logic",{"title":2717,"path":2718,"stem":2719},"Обробка складної бізнес-логіки","/ddd/complex-business-logic","10.ddd/06.complex-business-logic",{"title":2721,"path":2722,"stem":2723},"Моделювання фактора часу","/ddd/time-modeling","10.ddd/07.time-modeling",{"title":2725,"path":2726,"stem":2727},"Глава 8. Архітектурні Патерни","/ddd/architectural-patterns","10.ddd/08.architectural-patterns",{"title":2729,"path":2730,"stem":2731},"Глава 9. Патерни Взаємодії","/ddd/interaction-patterns","10.ddd/09.interaction-patterns",{"title":2733,"path":2734,"stem":2735},"Глава 10. Проектні Евристики","/ddd/design-heuristics","10.ddd/10.design-heuristics",{"title":2737,"path":2738,"stem":2739},"Глава 11. Еволюція Проектних Рішень","/ddd/evolution-of-design-decisions","10.ddd/11.evolution-of-design-decisions",{"title":2741,"path":2742,"stem":2743},"Глава 12. EventStorming","/ddd/event-storming","10.ddd/12.event-storming",{"title":2745,"path":2746,"stem":2747},"Глава 13. DDD на Практиці","/ddd/ddd-in-practice","10.ddd/13.ddd-in-practice",{"title":2749,"icon":2750,"path":2751,"stem":2752,"children":2753,"page":59},"Media Streaming","i-lucide-video","/media-streaming","11.media-streaming",[2754,2758,2762,2766,2770,2774,2778],{"title":2755,"path":2756,"stem":2757},"01. Магія Стрімінгу: Що відбувається, коли ви натискаєте \"Play\"","/media-streaming/introduction","11.media-streaming/01.introduction",{"title":2759,"path":2760,"stem":2761},"02. Анатомія Медіа: Кодеки, Контейнери та Стиснення","/media-streaming/audio-video-anatomy","11.media-streaming/02.audio-video-anatomy",{"title":2763,"path":2764,"stem":2765},"03. The Gym: FFmpeg Deep Dive","/media-streaming/ffmpeg-gym","11.media-streaming/03.ffmpeg-gym",{"title":2767,"path":2768,"stem":2769},"04. HLS Protocol: HTTP Live Streaming у Деталях","/media-streaming/hls-protocol","11.media-streaming/04.hls-protocol",{"title":2771,"path":2772,"stem":2773},"05. DASH Protocol: Відкритий Стандарт","/media-streaming/dash-protocol","11.media-streaming/05.dash-protocol",{"title":2775,"path":2776,"stem":2777},"06. Масштабування: CDN та Adaptive Bitrate","/media-streaming/cdn-and-adaptive-bitrate","11.media-streaming/06.cdn-and-adaptive-bitrate",{"title":2779,"path":2780,"stem":2781},"07. Війна із Затримкою (Latency)","/media-streaming/realtime-latency","11.media-streaming/07.realtime-latency",{"title":2783,"icon":2784,"path":2785,"stem":2786,"children":2787,"page":59},"HTML & CSS","i-devicon-html5","/html-css","12.html-css",[2788,2792,2796,2800,2804,2808,2812,2816,2820,2824,2828,2832,2836,2840,2844,2848,2852,2856,2860,2864,2868,2872,2876,2880,2884,2888,2892,2896,2900,2904],{"title":2789,"path":2790,"stem":2791},"Вступ до HTML. Структура документа","/html-css/intro-html-structure","12.html-css/01.intro-html-structure",{"title":2793,"path":2794,"stem":2795},"Форматування тексту в HTML","/html-css/html-text-formatting","12.html-css/02.html-text-formatting",{"title":2797,"path":2798,"stem":2799},"Посилання та зображення в HTML","/html-css/html-links-images","12.html-css/03.html-links-images",{"title":2801,"path":2802,"stem":2803},"Списки та таблиці в HTML","/html-css/html-lists-tables","12.html-css/04.html-lists-tables",{"title":2805,"path":2806,"stem":2807},"Форми в HTML","/html-css/html-forms","12.html-css/05.html-forms",{"title":2809,"path":2810,"stem":2811},"Семантичні елементи HTML5","/html-css/html-semantic-elements","12.html-css/06.html-semantic-elements",{"title":2813,"path":2814,"stem":2815},"Мультимедіа та розширені елементи HTML","/html-css/html-multimedia-advanced","12.html-css/07.html-multimedia-advanced",{"title":2817,"path":2818,"stem":2819},"Мікророзмітка та SEO в HTML","/html-css/html-microdata-seo","12.html-css/08.html-microdata-seo",{"title":2821,"path":2822,"stem":2823},"Вступ до CSS. Селектори та специфічність","/html-css/css-intro-selectors","12.html-css/09.css-intro-selectors",{"title":2825,"path":2826,"stem":2827},"Блокова модель CSS. Відступи. Box Sizing","/html-css/css-box-model","12.html-css/10.css-box-model",{"title":2829,"path":2830,"stem":2831},"Розміри у CSS: повний довідник одиниць і ключових слів","/html-css/10a.css-sizing","12.html-css/10a.css-sizing",{"title":2833,"path":2834,"stem":2835},"Типографіка в CSS. Шрифти та текст","/html-css/css-typography","12.html-css/11.css-typography",{"title":2837,"path":2838,"stem":2839},"Кольори та фони в CSS","/html-css/css-colors-backgrounds","12.html-css/12.css-colors-backgrounds",{"title":2841,"path":2842,"stem":2843},"Тіні та фільтри в CSS","/html-css/12b.css-shadows-filters","12.html-css/12b.css-shadows-filters",{"title":2845,"path":2846,"stem":2847},"CSS Flexbox: Фундамент гнучких макетів","/html-css/css-flexbox-fundamentals","12.html-css/13.css-flexbox-fundamentals",{"title":2849,"path":2850,"stem":2851},"CSS Flexbox: Вирівнювання та Позиціонування","/html-css/css-flexbox-alignment-sizing-and-patterns","12.html-css/14.css-flexbox-alignment-sizing-and-patterns",{"title":2853,"path":2854,"stem":2855},"CSS Grid. Двовимірний макет. Частина 1","/html-css/css-layout-grid","12.html-css/15.css-layout-grid",{"title":2857,"path":2858,"stem":2859},"CSS Grid. Двовимірний макет. Частина 2","/html-css/css-layout-grid-advanced","12.html-css/16.css-layout-grid-advanced",{"title":2861,"path":2862,"stem":2863},"Позиціонування в CSS. Z-index. Stacking Context","/html-css/css-positioning","12.html-css/17.css-positioning",{"title":2865,"path":2866,"stem":2867},"CSS Анімації та Переходи","/html-css/css-animations-transitions","12.html-css/18.css-animations-transitions",{"title":2869,"path":2870,"stem":2871},"Адаптивний дизайн. Media Queries. Частина 1","/html-css/css-responsive-media-queries","12.html-css/19.css-responsive-media-queries",{"title":2873,"path":2874,"stem":2875},"Адаптивний дизайн. Частина 2: clamp(), Container Queries, @layer","/html-css/css-responsive-advanced","12.html-css/20.css-responsive-advanced",{"title":2877,"path":2878,"stem":2879},"CSS Custom Properties. Методології. Сучасний CSS","/html-css/css-variables-methodologies","12.html-css/21.css-variables-methodologies",{"title":2881,"path":2882,"stem":2883},"Сучасний CSS 2023–2025: Нові можливості","/html-css/css-modern-features","12.html-css/22.css-modern-features",{"title":2885,"path":2886,"stem":2887},"CSS Nesting, @layer, @scope та @property: нативний препроцесор","/html-css/22a.css-nesting-modern-syntax","12.html-css/22a.css-nesting-modern-syntax",{"title":2889,"path":2890,"stem":2891},"CSS для форм та інтерактивних станів","/html-css/css-forms-interactive-states","12.html-css/23.css-forms-interactive-states",{"title":2893,"path":2894,"stem":2895},"Доступність у CSS (CSS Accessibility)","/html-css/css-accessibility","12.html-css/24.css-accessibility",{"title":2897,"path":2898,"stem":2899},"CSS-функції та сучасні sizing primitives","/html-css/css-functions-sizing","12.html-css/25.css-functions-sizing",{"title":2901,"path":2902,"stem":2903},"Rendering Pipeline і CSS Performance","/html-css/css-rendering-performance","12.html-css/26.css-rendering-performance",{"title":2905,"path":2906,"stem":2907},"CSS Best Practices: типові ситуації та правильні рішення","/html-css/css-best-practices","12.html-css/27.css-best-practices",{"title":2909,"path":2910,"stem":2911,"children":2912,"page":59},"Tailwind","/tailwind","21.tailwind",[2913,2917,2921,2925,2929,2933,2937,2941],{"title":2914,"path":2915,"stem":2916},"Що таке Tailwind CSS і навіщо він потрібен","/tailwind/tailwind-intro-philosophy","21.tailwind/01.tailwind-intro-philosophy",{"title":2918,"path":2919,"stem":2920},"Встановлення та налаштування Tailwind CSS v4","/tailwind/tailwind-installation-setup","21.tailwind/02.tailwind-installation-setup",{"title":2922,"path":2923,"stem":2924},"Utility-класи: основи та система Tailwind","/tailwind/tailwind-utility-classes-core","21.tailwind/03.tailwind-utility-classes-core",{"title":2926,"path":2927,"stem":2928},"Layout: Flexbox та Grid через Tailwind","/tailwind/tailwind-flexbox-grid","21.tailwind/04.tailwind-flexbox-grid",{"title":2930,"path":2931,"stem":2932},"Кастомізація теми через @theme у Tailwind v4","/tailwind/tailwind-theme-customization","21.tailwind/05.tailwind-theme-customization",{"title":2934,"path":2935,"stem":2936},"Варіанти: hover, focus, responsive, dark mode та нові v4","/tailwind/tailwind-variants-states","21.tailwind/06.tailwind-variants-states",{"title":2938,"path":2939,"stem":2940},"Типографіка та система кольорів у Tailwind v4","/tailwind/tailwind-typography-colors","21.tailwind/07.tailwind-typography-colors",{"title":2942,"path":2943,"stem":2944},"Компоненти та повторюваність: @apply, @utility та патерни","/tailwind/tailwind-components-patterns","21.tailwind/08.tailwind-components-patterns",{"title":2946,"path":2947,"stem":2948},"Showcase Компонентів kostyl.dev","/test-new-components","98.test-new-components",{"id":2950,"title":2341,"body":2951,"description":18477,"extension":18478,"links":18479,"meta":18480,"navigation":3152,"path":2342,"seo":18481,"stem":2343,"__hash__":18482},"docs/04.java/pr2/22.integration-testing-h2.md",{"type":2952,"value":2953,"toc":18435},"minimark",[2954,2958,2963,2986,2992,3311,3331,3352,3375,3381,3634,3638,3659,3662,3666,3767,3787,3793,3795,3799,3809,3814,3820,3841,3847,3909,3914,3944,3949,3970,3986,3988,3992,4002,4007,4217,4222,4482,4598,4600,4604,4608,4611,5023,5028,5064,5066,5070,5073,5079,5088,5623,5665,5667,5671,5677,5681,5768,5773,5977,5982,6142,6267,6269,6273,6279,6285,6290,6332,6337,6466,6471,6514,6532,6534,6538,6541,8153,8158,8199,8231,8233,8237,8243,11525,11530,11574,11676,11678,11682,11689,11693,12385,12387,12391,14065,14067,14071,14973,14978,15010,15050,15052,15056,15059,15115,15122,15126,15815,15819,16309,16314,16348,16352,16680,17391,17396,17770,17900,17902,17906,17910,17991,17995,18051,18055,18058,18064,18303,18305,18309,18312,18361,18363,18367,18417,18419,18425,18431],[2955,2956,2341],"h1",{"id":2957},"інтеграційне-тестування-jdbc-репозиторіїв-embedded-h2-та-патерн-aaa",[2959,2960,2962],"h2",{"id":2961},"вступ-чому-unit-тести-недостатні","Вступ: Чому unit-тести недостатні",[2964,2965,2966,2967,2971,2972,2975,2976,2975,2979,2975,2982,2985],"p",{},"Повернімося до репозиторіїв зі статті 14. Ми реалізували ",[2968,2969,2970],"code",{},"JdbcAuthorRepository"," з методами ",[2968,2973,2974],{},"save()",", ",[2968,2977,2978],{},"findById()",[2968,2980,2981],{},"update()",[2968,2983,2984],{},"deleteById()",". Як переконатися, що ці методи працюють коректно?",[2964,2987,2988],{},[2989,2990,2991],"strong",{},"Наївний підхід — unit-тести з mock-об'єктами:",[2993,2994,2999],"pre",{"className":2995,"code":2996,"language":2997,"meta":2998,"style":2998},"language-java shiki shiki-themes light-plus dark-plus dark-plus","@Test\nvoid save_shouldInsertAuthor() {\n    // Arrange\n    Connection mockConn = mock(Connection.class);\n    PreparedStatement mockStmt = mock(PreparedStatement.class);\n    when(mockConn.prepareStatement(anyString())).thenReturn(mockStmt);\n    when(mockStmt.executeUpdate()).thenReturn(1);\n\n    ConnectionManager mockCm = mock(ConnectionManager.class);\n    when(mockCm.getConnection()).thenReturn(mockConn);\n\n    AuthorRepository repo = new JdbcAuthorRepository(mockCm);\n    Author author = new Author(\"Іван\", \"Франко\");\n\n    // Act\n    repo.save(author);\n\n    // Assert\n    verify(mockStmt).executeUpdate();\n}\n","java","",[2968,3000,3001,3014,3027,3034,3065,3089,3119,3147,3154,3178,3200,3205,3226,3255,3260,3266,3280,3285,3291,3305],{"__ignoreMap":2998},[3002,3003,3006,3010],"span",{"class":3004,"line":3005},"line",1,[3002,3007,3009],{"class":3008},"sHH4Y","@",[3002,3011,3013],{"class":3012},"sN1BT","Test\n",[3002,3015,3017,3020,3024],{"class":3004,"line":3016},2,[3002,3018,3019],{"class":3012},"void",[3002,3021,3023],{"class":3022},"s8Opu"," save_shouldInsertAuthor",[3002,3025,3026],{"class":3008},"() {\n",[3002,3028,3030],{"class":3004,"line":3029},3,[3002,3031,3033],{"class":3032},"spJ8K","    // Arrange\n",[3002,3035,3037,3040,3044,3047,3050,3053,3056,3059,3062],{"class":3004,"line":3036},4,[3002,3038,3039],{"class":3012},"    Connection",[3002,3041,3043],{"class":3042},"siwwj"," mockConn",[3002,3045,3046],{"class":3008}," = ",[3002,3048,3049],{"class":3022},"mock",[3002,3051,3052],{"class":3008},"(",[3002,3054,3055],{"class":3042},"Connection",[3002,3057,3058],{"class":3008},".",[3002,3060,3061],{"class":3042},"class",[3002,3063,3064],{"class":3008},");\n",[3002,3066,3068,3071,3074,3076,3078,3080,3083,3085,3087],{"class":3004,"line":3067},5,[3002,3069,3070],{"class":3012},"    PreparedStatement",[3002,3072,3073],{"class":3042}," mockStmt",[3002,3075,3046],{"class":3008},[3002,3077,3049],{"class":3022},[3002,3079,3052],{"class":3008},[3002,3081,3082],{"class":3042},"PreparedStatement",[3002,3084,3058],{"class":3008},[3002,3086,3061],{"class":3042},[3002,3088,3064],{"class":3008},[3002,3090,3092,3095,3097,3100,3102,3105,3107,3110,3113,3116],{"class":3004,"line":3091},6,[3002,3093,3094],{"class":3022},"    when",[3002,3096,3052],{"class":3008},[3002,3098,3099],{"class":3042},"mockConn",[3002,3101,3058],{"class":3008},[3002,3103,3104],{"class":3022},"prepareStatement",[3002,3106,3052],{"class":3008},[3002,3108,3109],{"class":3022},"anyString",[3002,3111,3112],{"class":3008},"())).",[3002,3114,3115],{"class":3022},"thenReturn",[3002,3117,3118],{"class":3008},"(mockStmt);\n",[3002,3120,3122,3124,3126,3129,3131,3134,3137,3139,3141,3145],{"class":3004,"line":3121},7,[3002,3123,3094],{"class":3022},[3002,3125,3052],{"class":3008},[3002,3127,3128],{"class":3042},"mockStmt",[3002,3130,3058],{"class":3008},[3002,3132,3133],{"class":3022},"executeUpdate",[3002,3135,3136],{"class":3008},"()).",[3002,3138,3115],{"class":3022},[3002,3140,3052],{"class":3008},[3002,3142,3144],{"class":3143},"sJj4R","1",[3002,3146,3064],{"class":3008},[3002,3148,3150],{"class":3004,"line":3149},8,[3002,3151,3153],{"emptyLinePlaceholder":3152},true,"\n",[3002,3155,3157,3160,3163,3165,3167,3169,3172,3174,3176],{"class":3004,"line":3156},9,[3002,3158,3159],{"class":3012},"    ConnectionManager",[3002,3161,3162],{"class":3042}," mockCm",[3002,3164,3046],{"class":3008},[3002,3166,3049],{"class":3022},[3002,3168,3052],{"class":3008},[3002,3170,3171],{"class":3042},"ConnectionManager",[3002,3173,3058],{"class":3008},[3002,3175,3061],{"class":3042},[3002,3177,3064],{"class":3008},[3002,3179,3181,3183,3185,3188,3190,3193,3195,3197],{"class":3004,"line":3180},10,[3002,3182,3094],{"class":3022},[3002,3184,3052],{"class":3008},[3002,3186,3187],{"class":3042},"mockCm",[3002,3189,3058],{"class":3008},[3002,3191,3192],{"class":3022},"getConnection",[3002,3194,3136],{"class":3008},[3002,3196,3115],{"class":3022},[3002,3198,3199],{"class":3008},"(mockConn);\n",[3002,3201,3203],{"class":3004,"line":3202},11,[3002,3204,3153],{"emptyLinePlaceholder":3152},[3002,3206,3208,3211,3214,3216,3220,3223],{"class":3004,"line":3207},12,[3002,3209,3210],{"class":3012},"    AuthorRepository",[3002,3212,3213],{"class":3042}," repo",[3002,3215,3046],{"class":3008},[3002,3217,3219],{"class":3218},"sCDza","new",[3002,3221,3222],{"class":3022}," JdbcAuthorRepository",[3002,3224,3225],{"class":3008},"(mockCm);\n",[3002,3227,3229,3232,3235,3237,3239,3242,3244,3248,3250,3253],{"class":3004,"line":3228},13,[3002,3230,3231],{"class":3012},"    Author",[3002,3233,3234],{"class":3042}," author",[3002,3236,3046],{"class":3008},[3002,3238,3219],{"class":3218},[3002,3240,3241],{"class":3022}," Author",[3002,3243,3052],{"class":3008},[3002,3245,3247],{"class":3246},"sbdoH","\"Іван\"",[3002,3249,2975],{"class":3008},[3002,3251,3252],{"class":3246},"\"Франко\"",[3002,3254,3064],{"class":3008},[3002,3256,3258],{"class":3004,"line":3257},14,[3002,3259,3153],{"emptyLinePlaceholder":3152},[3002,3261,3263],{"class":3004,"line":3262},15,[3002,3264,3265],{"class":3032},"    // Act\n",[3002,3267,3269,3272,3274,3277],{"class":3004,"line":3268},16,[3002,3270,3271],{"class":3042},"    repo",[3002,3273,3058],{"class":3008},[3002,3275,3276],{"class":3022},"save",[3002,3278,3279],{"class":3008},"(author);\n",[3002,3281,3283],{"class":3004,"line":3282},17,[3002,3284,3153],{"emptyLinePlaceholder":3152},[3002,3286,3288],{"class":3004,"line":3287},18,[3002,3289,3290],{"class":3032},"    // Assert\n",[3002,3292,3294,3297,3300,3302],{"class":3004,"line":3293},19,[3002,3295,3296],{"class":3022},"    verify",[3002,3298,3299],{"class":3008},"(mockStmt).",[3002,3301,3133],{"class":3022},[3002,3303,3304],{"class":3008},"();\n",[3002,3306,3308],{"class":3004,"line":3307},20,[3002,3309,3310],{"class":3008},"}\n",[2964,3312,3313,3316,3317,3319,3320,3323,3324,3326,3327,3330],{},[2989,3314,3315],{},"Що перевіряє цей тест?"," Він перевіряє, що метод ",[2968,3318,2974],{}," викликає ",[2968,3321,3322],{},"executeUpdate()"," на ",[2968,3325,3082],{},". Але він ",[2989,3328,3329],{},"не перевіряє",":",[3332,3333,3334,3338,3343,3346,3349],"ul",{},[3335,3336,3337],"li",{},"Чи правильний SQL-запит (синтаксис, назви стовпців)",[3335,3339,3340,3341],{},"Чи коректно встановлені параметри ",[2968,3342,3082],{},[3335,3344,3345],{},"Чи дані дійсно збережені у БД",[3335,3347,3348],{},"Чи працюють FK-обмеження",[3335,3350,3351],{},"Чи коректно обробляються SQL-виключення",[3353,3354,3355,3358,3359,3362,3363,3366,3367,3370,3371,3374],"warning",{},[2989,3356,3357],{},"Проблема mock-тестів:"," Вони перевіряють ",[2989,3360,3361],{},"взаємодію з API",", а не ",[2989,3364,3365],{},"коректність роботи з БД",". Якщо у SQL-запиті помилка (",[2968,3368,3369],{},"INSER INTO"," замість ",[2968,3372,3373],{},"INSERT INTO",") — mock-тест пройде успішно, але код не працюватиме у production.",[2964,3376,3377,3380],{},[2989,3378,3379],{},"Інтеграційний тест"," вирішує цю проблему: він виконує реальний SQL на реальній БД і перевіряє реальний результат.",[2993,3382,3384],{"className":2995,"code":3383,"language":2997,"meta":2998,"style":2998},"@Test\nvoid save_shouldInsertNewAuthor_whenValidData() {\n    // Arrange: створити реальну БД, виконати DDL\n    ConnectionManager cm = ConnectionManager.forH2InMemory();\n    executeDdlScript(cm, \"ddl_h2.sql\");\n    AuthorRepository repo = new JdbcAuthorRepository(cm);\n    \n    Author author = new Author(\"Іван\", \"Франко\");\n    author.setBio(\"Український письменник\");\n\n    // Act: виконати реальний INSERT\n    repo.save(author);\n\n    // Assert: перевірити, що дані у БД\n    Author loaded = repo.findById(author.getId()).orElseThrow();\n    assertThat(loaded.getFirstName()).isEqualTo(\"Іван\");\n    assertThat(loaded.getLastName()).isEqualTo(\"Франко\");\n    assertThat(loaded.getBio()).isEqualTo(\"Український письменник\");\n}\n",[2968,3385,3386,3392,3401,3406,3424,3437,3452,3457,3479,3496,3500,3505,3515,3519,3524,3558,3584,3607,3630],{"__ignoreMap":2998},[3002,3387,3388,3390],{"class":3004,"line":3005},[3002,3389,3009],{"class":3008},[3002,3391,3013],{"class":3012},[3002,3393,3394,3396,3399],{"class":3004,"line":3016},[3002,3395,3019],{"class":3012},[3002,3397,3398],{"class":3022}," save_shouldInsertNewAuthor_whenValidData",[3002,3400,3026],{"class":3008},[3002,3402,3403],{"class":3004,"line":3029},[3002,3404,3405],{"class":3032},"    // Arrange: створити реальну БД, виконати DDL\n",[3002,3407,3408,3410,3413,3415,3417,3419,3422],{"class":3004,"line":3036},[3002,3409,3159],{"class":3012},[3002,3411,3412],{"class":3042}," cm",[3002,3414,3046],{"class":3008},[3002,3416,3171],{"class":3042},[3002,3418,3058],{"class":3008},[3002,3420,3421],{"class":3022},"forH2InMemory",[3002,3423,3304],{"class":3008},[3002,3425,3426,3429,3432,3435],{"class":3004,"line":3067},[3002,3427,3428],{"class":3022},"    executeDdlScript",[3002,3430,3431],{"class":3008},"(cm, ",[3002,3433,3434],{"class":3246},"\"ddl_h2.sql\"",[3002,3436,3064],{"class":3008},[3002,3438,3439,3441,3443,3445,3447,3449],{"class":3004,"line":3091},[3002,3440,3210],{"class":3012},[3002,3442,3213],{"class":3042},[3002,3444,3046],{"class":3008},[3002,3446,3219],{"class":3218},[3002,3448,3222],{"class":3022},[3002,3450,3451],{"class":3008},"(cm);\n",[3002,3453,3454],{"class":3004,"line":3121},[3002,3455,3456],{"class":3008},"    \n",[3002,3458,3459,3461,3463,3465,3467,3469,3471,3473,3475,3477],{"class":3004,"line":3149},[3002,3460,3231],{"class":3012},[3002,3462,3234],{"class":3042},[3002,3464,3046],{"class":3008},[3002,3466,3219],{"class":3218},[3002,3468,3241],{"class":3022},[3002,3470,3052],{"class":3008},[3002,3472,3247],{"class":3246},[3002,3474,2975],{"class":3008},[3002,3476,3252],{"class":3246},[3002,3478,3064],{"class":3008},[3002,3480,3481,3484,3486,3489,3491,3494],{"class":3004,"line":3156},[3002,3482,3483],{"class":3042},"    author",[3002,3485,3058],{"class":3008},[3002,3487,3488],{"class":3022},"setBio",[3002,3490,3052],{"class":3008},[3002,3492,3493],{"class":3246},"\"Український письменник\"",[3002,3495,3064],{"class":3008},[3002,3497,3498],{"class":3004,"line":3180},[3002,3499,3153],{"emptyLinePlaceholder":3152},[3002,3501,3502],{"class":3004,"line":3202},[3002,3503,3504],{"class":3032},"    // Act: виконати реальний INSERT\n",[3002,3506,3507,3509,3511,3513],{"class":3004,"line":3207},[3002,3508,3271],{"class":3042},[3002,3510,3058],{"class":3008},[3002,3512,3276],{"class":3022},[3002,3514,3279],{"class":3008},[3002,3516,3517],{"class":3004,"line":3228},[3002,3518,3153],{"emptyLinePlaceholder":3152},[3002,3520,3521],{"class":3004,"line":3257},[3002,3522,3523],{"class":3032},"    // Assert: перевірити, що дані у БД\n",[3002,3525,3526,3528,3531,3533,3536,3538,3541,3543,3546,3548,3551,3553,3556],{"class":3004,"line":3262},[3002,3527,3231],{"class":3012},[3002,3529,3530],{"class":3042}," loaded",[3002,3532,3046],{"class":3008},[3002,3534,3535],{"class":3042},"repo",[3002,3537,3058],{"class":3008},[3002,3539,3540],{"class":3022},"findById",[3002,3542,3052],{"class":3008},[3002,3544,3545],{"class":3042},"author",[3002,3547,3058],{"class":3008},[3002,3549,3550],{"class":3022},"getId",[3002,3552,3136],{"class":3008},[3002,3554,3555],{"class":3022},"orElseThrow",[3002,3557,3304],{"class":3008},[3002,3559,3560,3563,3565,3568,3570,3573,3575,3578,3580,3582],{"class":3004,"line":3268},[3002,3561,3562],{"class":3022},"    assertThat",[3002,3564,3052],{"class":3008},[3002,3566,3567],{"class":3042},"loaded",[3002,3569,3058],{"class":3008},[3002,3571,3572],{"class":3022},"getFirstName",[3002,3574,3136],{"class":3008},[3002,3576,3577],{"class":3022},"isEqualTo",[3002,3579,3052],{"class":3008},[3002,3581,3247],{"class":3246},[3002,3583,3064],{"class":3008},[3002,3585,3586,3588,3590,3592,3594,3597,3599,3601,3603,3605],{"class":3004,"line":3282},[3002,3587,3562],{"class":3022},[3002,3589,3052],{"class":3008},[3002,3591,3567],{"class":3042},[3002,3593,3058],{"class":3008},[3002,3595,3596],{"class":3022},"getLastName",[3002,3598,3136],{"class":3008},[3002,3600,3577],{"class":3022},[3002,3602,3052],{"class":3008},[3002,3604,3252],{"class":3246},[3002,3606,3064],{"class":3008},[3002,3608,3609,3611,3613,3615,3617,3620,3622,3624,3626,3628],{"class":3004,"line":3287},[3002,3610,3562],{"class":3022},[3002,3612,3052],{"class":3008},[3002,3614,3567],{"class":3042},[3002,3616,3058],{"class":3008},[3002,3618,3619],{"class":3022},"getBio",[3002,3621,3136],{"class":3008},[3002,3623,3577],{"class":3022},[3002,3625,3052],{"class":3008},[3002,3627,3493],{"class":3246},[3002,3629,3064],{"class":3008},[3002,3631,3632],{"class":3004,"line":3293},[3002,3633,3310],{"class":3008},[2964,3635,3636],{},[2989,3637,3315],{},[3332,3639,3640,3643,3646,3649,3656],{},[3335,3641,3642],{},"✅ SQL-запит синтаксично коректний",[3335,3644,3645],{},"✅ Параметри встановлені правильно",[3335,3647,3648],{},"✅ Дані збережені у БД",[3335,3650,3651,3652,3655],{},"✅ Маппінг ",[2968,3653,3654],{},"ResultSet → Author"," працює",[3335,3657,3658],{},"✅ UUID генерується коректно",[3660,3661],"hr",{},[2959,3663,3665],{"id":3664},"різниця-між-unit-та-integration-тестами","Різниця між Unit та Integration тестами",[3667,3668,3669,3685],"table",{},[3670,3671,3672],"thead",{},[3673,3674,3675,3679,3682],"tr",{},[3676,3677,3678],"th",{},"Критерій",[3676,3680,3681],{},"Unit Test",[3676,3683,3684],{},"Integration Test",[3686,3687,3688,3702,3715,3728,3741,3754],"tbody",{},[3673,3689,3690,3696,3699],{},[3691,3692,3693],"td",{},[2989,3694,3695],{},"Що тестується",[3691,3697,3698],{},"Окремий метод/клас ізольовано",[3691,3700,3701],{},"Взаємодія кількох компонентів",[3673,3703,3704,3709,3712],{},[3691,3705,3706],{},[2989,3707,3708],{},"Залежності",[3691,3710,3711],{},"Mock-об'єкти (Mockito)",[3691,3713,3714],{},"Реальні залежності (БД, файли)",[3673,3716,3717,3722,3725],{},[3691,3718,3719],{},[2989,3720,3721],{},"Швидкість",[3691,3723,3724],{},"⚡ Дуже швидкі (мілісекунди)",[3691,3726,3727],{},"🐢 Повільніші (секунди)",[3673,3729,3730,3735,3738],{},[3691,3731,3732],{},[2989,3733,3734],{},"Складність налаштування",[3691,3736,3737],{},"Проста (без зовнішніх ресурсів)",[3691,3739,3740],{},"Складніша (потрібна БД)",[3673,3742,3743,3748,3751],{},[3691,3744,3745],{},[2989,3746,3747],{},"Що виявляють",[3691,3749,3750],{},"Логічні помилки у коді",[3691,3752,3753],{},"Помилки інтеграції (SQL, схема БД)",[3673,3755,3756,3761,3764],{},[3691,3757,3758],{},[2989,3759,3760],{},"Коли запускати",[3691,3762,3763],{},"При кожному збереженні файлу",[3691,3765,3766],{},"Перед commit, у CI/CD",[3768,3769,3770,3776,3784],"tip",{},[2964,3771,3772,3775],{},[2989,3773,3774],{},"Піраміда тестування"," (Mike Cohn, 2009):",[2993,3777,3782],{"className":3778,"code":3780,"language":3781},[3779],"language-text","        /\\\n       /  \\  E2E Tests (UI, API)\n      /____\\\n     /      \\\n    / Integr \\  Integration Tests\n   /__________\\\n  /            \\\n /  Unit Tests  \\  Unit Tests\n/________________\\\n\n70% Unit | 20% Integration | 10% E2E\n","text",[2968,3783,3780],{"__ignoreMap":2998},[2964,3785,3786],{},"Більшість тестів мають бути unit-тестами (швидкі, ізольовані). Integration тести покривають критичні шляхи взаємодії з БД. E2E тести перевіряють систему цілком.",[2964,3788,3789,3792],{},[2989,3790,3791],{},"Для JDBC-репозиторіїв integration тести є критично важливими",", оскільки основна логіка — це SQL-запити, що не можуть бути адекватно протестовані через mock.",[3660,3794],{},[2959,3796,3798],{"id":3797},"архітектура-тестового-оточення","Архітектура тестового оточення",[2964,3800,3801,3802,3805,3806,3058],{},"Для інтеграційних тестів нам потрібна ",[2989,3803,3804],{},"реальна БД",". Але використовувати production БД для тестів — небезпечно (можна випадково видалити дані). Рішення — ",[2989,3807,3808],{},"Embedded H2 Database",[3810,3811,3813],"h3",{"id":3812},"що-таке-embedded-h2","Що таке Embedded H2?",[2964,3815,3816,3819],{},[2989,3817,3818],{},"H2"," — це легковагова Java-БД, що може працювати у трьох режимах:",[3821,3822,3823,3829,3835],"ol",{},[3335,3824,3825,3828],{},[2989,3826,3827],{},"Server mode:"," Окремий процес, до якого підключаються клієнти (як PostgreSQL)",[3335,3830,3831,3834],{},[2989,3832,3833],{},"Embedded mode:"," БД працює у тому ж JVM-процесі, що й додаток",[3335,3836,3837,3840],{},[2989,3838,3839],{},"In-memory mode:"," БД існує лише у RAM, видаляється після завершення JVM",[2964,3842,3843,3844,3330],{},"Для тестів ми використовуємо ",[2989,3845,3846],{},"in-memory mode",[2993,3848,3850],{"className":2995,"code":3849,"language":2997,"meta":2998,"style":2998},"// Кожен тест отримує нову порожню БД у пам'яті\nString url = \"jdbc:h2:mem:test_db_\" + UUID.randomUUID() + \";DB_CLOSE_DELAY=-1\";\nConnection conn = DriverManager.getConnection(url);\n",[2968,3851,3852,3857,3890],{"__ignoreMap":2998},[3002,3853,3854],{"class":3004,"line":3005},[3002,3855,3856],{"class":3032},"// Кожен тест отримує нову порожню БД у пам'яті\n",[3002,3858,3859,3862,3865,3867,3870,3873,3876,3878,3881,3884,3887],{"class":3004,"line":3016},[3002,3860,3861],{"class":3012},"String",[3002,3863,3864],{"class":3042}," url",[3002,3866,3046],{"class":3008},[3002,3868,3869],{"class":3246},"\"jdbc:h2:mem:test_db_\"",[3002,3871,3872],{"class":3008}," + ",[3002,3874,3875],{"class":3042},"UUID",[3002,3877,3058],{"class":3008},[3002,3879,3880],{"class":3022},"randomUUID",[3002,3882,3883],{"class":3008},"() + ",[3002,3885,3886],{"class":3246},"\";DB_CLOSE_DELAY=-1\"",[3002,3888,3889],{"class":3008},";\n",[3002,3891,3892,3894,3897,3899,3902,3904,3906],{"class":3004,"line":3029},[3002,3893,3055],{"class":3012},[3002,3895,3896],{"class":3042}," conn",[3002,3898,3046],{"class":3008},[3002,3900,3901],{"class":3042},"DriverManager",[3002,3903,3058],{"class":3008},[3002,3905,3192],{"class":3022},[3002,3907,3908],{"class":3008},"(url);\n",[2964,3910,3911],{},[2989,3912,3913],{},"Переваги in-memory H2 для тестів:",[3332,3915,3916,3923,3930,3937],{},[3335,3917,3918,3919,3922],{},"⚡ ",[2989,3920,3921],{},"Швидкість:"," БД у RAM — INSERT/SELECT виконуються за мікросекунди",[3335,3924,3925,3926,3929],{},"🔒 ",[2989,3927,3928],{},"Ізоляція:"," Кожен тест отримує нову БД — тести не впливають один на одного",[3335,3931,3932,3933,3936],{},"🧹 ",[2989,3934,3935],{},"Автоочищення:"," БД видаляється після завершення тесту — не потрібно cleanup",[3335,3938,3939,3940,3943],{},"📦 ",[2989,3941,3942],{},"Без установки:"," H2 — це JAR-файл, додається як Maven-залежність",[2964,3945,3946],{},[2989,3947,3948],{},"Недоліки H2:",[3332,3950,3951,3958,3964],{},[3335,3952,3953,3954,3957],{},"⚠️ ",[2989,3955,3956],{},"Діалектні відмінності:"," H2 SQL не на 100% сумісний з PostgreSQL/MySQL",[3335,3959,3953,3960,3963],{},[2989,3961,3962],{},"Обмежені типи:"," Немає ENUM (у H2 2.x є, але з обмеженнями), JSON, масивів",[3335,3965,3953,3966,3969],{},[2989,3967,3968],{},"Інша продуктивність:"," Оптимізатор запитів відрізняється від production СУБД",[3971,3972,3973,3980],"note",{},[2964,3974,3975,3976,3979],{},"У наступній статті (23) ми розглянемо ",[2989,3977,3978],{},"Testcontainers"," — підхід, що запускає реальну PostgreSQL у Docker-контейнері для тестів. Це усуває діалектні відмінності, але повільніше за H2.",[2964,3981,3982,3985],{},[2989,3983,3984],{},"Рекомендація:"," Використовуйте H2 для швидких тестів базової функціональності, Testcontainers — для тестування PostgreSQL-специфічних функцій (ENUM, JSON, full-text search).",[3660,3987],{},[2959,3989,3991],{"id":3990},"ізоляція-тестів-кожен-тест-нова-бд","Ізоляція тестів: Кожен тест = нова БД",[2964,3993,3994,3997,3998,4001],{},[2989,3995,3996],{},"Золоте правило інтеграційних тестів:"," Тести мають бути ",[2989,3999,4000],{},"незалежними"," — порядок виконання не повинен впливати на результат.",[2964,4003,4004],{},[2989,4005,4006],{},"Антипатерн — спільна БД для всіх тестів:",[2993,4008,4010],{"className":2995,"code":4009,"language":2997,"meta":2998,"style":2998},"// ❌ ПОГАНО: всі тести використовують одну БД\nstatic Connection sharedConnection;\n\n@BeforeAll\nstatic void setupDatabase() {\n    sharedConnection = DriverManager.getConnection(\"jdbc:h2:mem:shared_db\");\n    executeDdl(sharedConnection);\n}\n\n@Test\nvoid test1() {\n    repo.save(new Author(\"Автор 1\", \"Прізвище 1\"));\n    // БД тепер містить 1 автора\n}\n\n@Test\nvoid test2() {\n    List\u003CAuthor> all = repo.findAll();\n    // Скільки авторів? Залежить від того, чи виконався test1 перед test2!\n    assertThat(all).hasSize(???); // Непередбачувано\n}\n",[2968,4011,4012,4017,4031,4035,4042,4054,4072,4080,4084,4088,4094,4103,4130,4135,4139,4143,4149,4158,4186,4191,4212],{"__ignoreMap":2998},[3002,4013,4014],{"class":3004,"line":3005},[3002,4015,4016],{"class":3032},"// ❌ ПОГАНО: всі тести використовують одну БД\n",[3002,4018,4019,4023,4026,4029],{"class":3004,"line":3016},[3002,4020,4022],{"class":4021},"su1O8","static",[3002,4024,4025],{"class":3012}," Connection",[3002,4027,4028],{"class":3042}," sharedConnection",[3002,4030,3889],{"class":3008},[3002,4032,4033],{"class":3004,"line":3029},[3002,4034,3153],{"emptyLinePlaceholder":3152},[3002,4036,4037,4039],{"class":3004,"line":3036},[3002,4038,3009],{"class":3008},[3002,4040,4041],{"class":3012},"BeforeAll\n",[3002,4043,4044,4046,4049,4052],{"class":3004,"line":3067},[3002,4045,4022],{"class":4021},[3002,4047,4048],{"class":3012}," void",[3002,4050,4051],{"class":3022}," setupDatabase",[3002,4053,3026],{"class":3008},[3002,4055,4056,4059,4061,4063,4065,4067,4070],{"class":3004,"line":3091},[3002,4057,4058],{"class":3008},"    sharedConnection = ",[3002,4060,3901],{"class":3042},[3002,4062,3058],{"class":3008},[3002,4064,3192],{"class":3022},[3002,4066,3052],{"class":3008},[3002,4068,4069],{"class":3246},"\"jdbc:h2:mem:shared_db\"",[3002,4071,3064],{"class":3008},[3002,4073,4074,4077],{"class":3004,"line":3121},[3002,4075,4076],{"class":3022},"    executeDdl",[3002,4078,4079],{"class":3008},"(sharedConnection);\n",[3002,4081,4082],{"class":3004,"line":3149},[3002,4083,3310],{"class":3008},[3002,4085,4086],{"class":3004,"line":3156},[3002,4087,3153],{"emptyLinePlaceholder":3152},[3002,4089,4090,4092],{"class":3004,"line":3180},[3002,4091,3009],{"class":3008},[3002,4093,3013],{"class":3012},[3002,4095,4096,4098,4101],{"class":3004,"line":3202},[3002,4097,3019],{"class":3012},[3002,4099,4100],{"class":3022}," test1",[3002,4102,3026],{"class":3008},[3002,4104,4105,4107,4109,4111,4113,4115,4117,4119,4122,4124,4127],{"class":3004,"line":3207},[3002,4106,3271],{"class":3042},[3002,4108,3058],{"class":3008},[3002,4110,3276],{"class":3022},[3002,4112,3052],{"class":3008},[3002,4114,3219],{"class":3218},[3002,4116,3241],{"class":3022},[3002,4118,3052],{"class":3008},[3002,4120,4121],{"class":3246},"\"Автор 1\"",[3002,4123,2975],{"class":3008},[3002,4125,4126],{"class":3246},"\"Прізвище 1\"",[3002,4128,4129],{"class":3008},"));\n",[3002,4131,4132],{"class":3004,"line":3228},[3002,4133,4134],{"class":3032},"    // БД тепер містить 1 автора\n",[3002,4136,4137],{"class":3004,"line":3257},[3002,4138,3310],{"class":3008},[3002,4140,4141],{"class":3004,"line":3262},[3002,4142,3153],{"emptyLinePlaceholder":3152},[3002,4144,4145,4147],{"class":3004,"line":3268},[3002,4146,3009],{"class":3008},[3002,4148,3013],{"class":3012},[3002,4150,4151,4153,4156],{"class":3004,"line":3282},[3002,4152,3019],{"class":3012},[3002,4154,4155],{"class":3022}," test2",[3002,4157,3026],{"class":3008},[3002,4159,4160,4163,4166,4169,4172,4175,4177,4179,4181,4184],{"class":3004,"line":3287},[3002,4161,4162],{"class":3012},"    List",[3002,4164,4165],{"class":3008},"\u003C",[3002,4167,4168],{"class":3012},"Author",[3002,4170,4171],{"class":3008},"> ",[3002,4173,4174],{"class":3042},"all",[3002,4176,3046],{"class":3008},[3002,4178,3535],{"class":3042},[3002,4180,3058],{"class":3008},[3002,4182,4183],{"class":3022},"findAll",[3002,4185,3304],{"class":3008},[3002,4187,4188],{"class":3004,"line":3293},[3002,4189,4190],{"class":3032},"    // Скільки авторів? Залежить від того, чи виконався test1 перед test2!\n",[3002,4192,4193,4195,4198,4201,4203,4206,4209],{"class":3004,"line":3307},[3002,4194,3562],{"class":3022},[3002,4196,4197],{"class":3008},"(all).",[3002,4199,4200],{"class":3022},"hasSize",[3002,4202,3052],{"class":3008},[3002,4204,4205],{"class":3218},"???",[3002,4207,4208],{"class":3008},"); ",[3002,4210,4211],{"class":3032},"// Непередбачувано\n",[3002,4213,4215],{"class":3004,"line":4214},21,[3002,4216,3310],{"class":3008},[2964,4218,4219],{},[2989,4220,4221],{},"Правильний підхід — нова БД для кожного тесту:",[2993,4223,4225],{"className":2995,"code":4224,"language":2997,"meta":2998,"style":2998},"// ✅ ДОБРЕ: кожен тест отримує нову порожню БД\nConnectionManager cm;\n\n@BeforeEach\nvoid setupFreshDatabase() {\n    // Унікальна назва БД для кожного тесту\n    String dbName = \"test_db_\" + UUID.randomUUID();\n    cm = ConnectionManager.forH2InMemory(dbName);\n    \n    // Виконати DDL (створити таблиці)\n    executeDdlScript(cm, \"ddl_h2.sql\");\n}\n\n@AfterEach\nvoid tearDown() {\n    cm.close(); // БД видаляється з пам'яті\n}\n\n@Test\nvoid test1() {\n    repo.save(new Author(\"Автор 1\", \"Прізвище 1\"));\n    // Ця БД існує лише для test1\n}\n\n@Test\nvoid test2() {\n    List\u003CAuthor> all = repo.findAll();\n    assertThat(all).isEmpty(); // Завжди порожня — нова БД!\n}\n",[2968,4226,4227,4232,4240,4244,4251,4260,4265,4288,4302,4306,4311,4321,4325,4329,4336,4345,4361,4365,4369,4375,4383,4407,4413,4418,4423,4430,4439,4462,4477],{"__ignoreMap":2998},[3002,4228,4229],{"class":3004,"line":3005},[3002,4230,4231],{"class":3032},"// ✅ ДОБРЕ: кожен тест отримує нову порожню БД\n",[3002,4233,4234,4236,4238],{"class":3004,"line":3016},[3002,4235,3171],{"class":3012},[3002,4237,3412],{"class":3042},[3002,4239,3889],{"class":3008},[3002,4241,4242],{"class":3004,"line":3029},[3002,4243,3153],{"emptyLinePlaceholder":3152},[3002,4245,4246,4248],{"class":3004,"line":3036},[3002,4247,3009],{"class":3008},[3002,4249,4250],{"class":3012},"BeforeEach\n",[3002,4252,4253,4255,4258],{"class":3004,"line":3067},[3002,4254,3019],{"class":3012},[3002,4256,4257],{"class":3022}," setupFreshDatabase",[3002,4259,3026],{"class":3008},[3002,4261,4262],{"class":3004,"line":3091},[3002,4263,4264],{"class":3032},"    // Унікальна назва БД для кожного тесту\n",[3002,4266,4267,4270,4273,4275,4278,4280,4282,4284,4286],{"class":3004,"line":3121},[3002,4268,4269],{"class":3012},"    String",[3002,4271,4272],{"class":3042}," dbName",[3002,4274,3046],{"class":3008},[3002,4276,4277],{"class":3246},"\"test_db_\"",[3002,4279,3872],{"class":3008},[3002,4281,3875],{"class":3042},[3002,4283,3058],{"class":3008},[3002,4285,3880],{"class":3022},[3002,4287,3304],{"class":3008},[3002,4289,4290,4293,4295,4297,4299],{"class":3004,"line":3149},[3002,4291,4292],{"class":3008},"    cm = ",[3002,4294,3171],{"class":3042},[3002,4296,3058],{"class":3008},[3002,4298,3421],{"class":3022},[3002,4300,4301],{"class":3008},"(dbName);\n",[3002,4303,4304],{"class":3004,"line":3156},[3002,4305,3456],{"class":3008},[3002,4307,4308],{"class":3004,"line":3180},[3002,4309,4310],{"class":3032},"    // Виконати DDL (створити таблиці)\n",[3002,4312,4313,4315,4317,4319],{"class":3004,"line":3202},[3002,4314,3428],{"class":3022},[3002,4316,3431],{"class":3008},[3002,4318,3434],{"class":3246},[3002,4320,3064],{"class":3008},[3002,4322,4323],{"class":3004,"line":3207},[3002,4324,3310],{"class":3008},[3002,4326,4327],{"class":3004,"line":3228},[3002,4328,3153],{"emptyLinePlaceholder":3152},[3002,4330,4331,4333],{"class":3004,"line":3257},[3002,4332,3009],{"class":3008},[3002,4334,4335],{"class":3012},"AfterEach\n",[3002,4337,4338,4340,4343],{"class":3004,"line":3262},[3002,4339,3019],{"class":3012},[3002,4341,4342],{"class":3022}," tearDown",[3002,4344,3026],{"class":3008},[3002,4346,4347,4350,4352,4355,4358],{"class":3004,"line":3268},[3002,4348,4349],{"class":3042},"    cm",[3002,4351,3058],{"class":3008},[3002,4353,4354],{"class":3022},"close",[3002,4356,4357],{"class":3008},"(); ",[3002,4359,4360],{"class":3032},"// БД видаляється з пам'яті\n",[3002,4362,4363],{"class":3004,"line":3282},[3002,4364,3310],{"class":3008},[3002,4366,4367],{"class":3004,"line":3287},[3002,4368,3153],{"emptyLinePlaceholder":3152},[3002,4370,4371,4373],{"class":3004,"line":3293},[3002,4372,3009],{"class":3008},[3002,4374,3013],{"class":3012},[3002,4376,4377,4379,4381],{"class":3004,"line":3307},[3002,4378,3019],{"class":3012},[3002,4380,4100],{"class":3022},[3002,4382,3026],{"class":3008},[3002,4384,4385,4387,4389,4391,4393,4395,4397,4399,4401,4403,4405],{"class":3004,"line":4214},[3002,4386,3271],{"class":3042},[3002,4388,3058],{"class":3008},[3002,4390,3276],{"class":3022},[3002,4392,3052],{"class":3008},[3002,4394,3219],{"class":3218},[3002,4396,3241],{"class":3022},[3002,4398,3052],{"class":3008},[3002,4400,4121],{"class":3246},[3002,4402,2975],{"class":3008},[3002,4404,4126],{"class":3246},[3002,4406,4129],{"class":3008},[3002,4408,4410],{"class":3004,"line":4409},22,[3002,4411,4412],{"class":3032},"    // Ця БД існує лише для test1\n",[3002,4414,4416],{"class":3004,"line":4415},23,[3002,4417,3310],{"class":3008},[3002,4419,4421],{"class":3004,"line":4420},24,[3002,4422,3153],{"emptyLinePlaceholder":3152},[3002,4424,4426,4428],{"class":3004,"line":4425},25,[3002,4427,3009],{"class":3008},[3002,4429,3013],{"class":3012},[3002,4431,4433,4435,4437],{"class":3004,"line":4432},26,[3002,4434,3019],{"class":3012},[3002,4436,4155],{"class":3022},[3002,4438,3026],{"class":3008},[3002,4440,4442,4444,4446,4448,4450,4452,4454,4456,4458,4460],{"class":3004,"line":4441},27,[3002,4443,4162],{"class":3012},[3002,4445,4165],{"class":3008},[3002,4447,4168],{"class":3012},[3002,4449,4171],{"class":3008},[3002,4451,4174],{"class":3042},[3002,4453,3046],{"class":3008},[3002,4455,3535],{"class":3042},[3002,4457,3058],{"class":3008},[3002,4459,4183],{"class":3022},[3002,4461,3304],{"class":3008},[3002,4463,4465,4467,4469,4472,4474],{"class":3004,"line":4464},28,[3002,4466,3562],{"class":3022},[3002,4468,4197],{"class":3008},[3002,4470,4471],{"class":3022},"isEmpty",[3002,4473,4357],{"class":3008},[3002,4475,4476],{"class":3032},"// Завжди порожня — нова БД!\n",[3002,4478,4480],{"class":3004,"line":4479},29,[3002,4481,3310],{"class":3008},[4483,4484,4485],"mermaid",{},[2993,4486,4489],{"className":4487,"code":4488,"language":4483,"meta":2998,"style":2998},"language-mermaid shiki shiki-themes light-plus dark-plus dark-plus","sequenceDiagram\n    participant JUnit as JUnit 5\n    participant Test1 as test1()\n    participant Test2 as test2()\n    participant H2 as H2 Database\n\n    JUnit->>Test1: @BeforeEach\n    Test1->>H2: CREATE DATABASE test_db_abc123\n    Test1->>H2: CREATE TABLE authors...\n    Test1->>H2: INSERT INTO authors...\n    Test1->>H2: SELECT * FROM authors\n    Test1->>H2: @AfterEach → close()\n    H2-->>Test1: БД видалена з пам'яті\n\n    JUnit->>Test2: @BeforeEach\n    Test2->>H2: CREATE DATABASE test_db_xyz789 (нова!)\n    Test2->>H2: CREATE TABLE authors...\n    Test2->>H2: SELECT * FROM authors → порожня таблиця\n    Test2->>H2: @AfterEach → close()\n    H2-->>Test2: БД видалена з пам'яті\n\n    Note over Test1,Test2: Тести повністю ізольовані\n",[2968,4490,4491,4496,4501,4506,4511,4516,4520,4525,4530,4535,4540,4545,4550,4555,4559,4564,4569,4574,4579,4584,4589,4593],{"__ignoreMap":2998},[3002,4492,4493],{"class":3004,"line":3005},[3002,4494,4495],{},"sequenceDiagram\n",[3002,4497,4498],{"class":3004,"line":3016},[3002,4499,4500],{},"    participant JUnit as JUnit 5\n",[3002,4502,4503],{"class":3004,"line":3029},[3002,4504,4505],{},"    participant Test1 as test1()\n",[3002,4507,4508],{"class":3004,"line":3036},[3002,4509,4510],{},"    participant Test2 as test2()\n",[3002,4512,4513],{"class":3004,"line":3067},[3002,4514,4515],{},"    participant H2 as H2 Database\n",[3002,4517,4518],{"class":3004,"line":3091},[3002,4519,3153],{"emptyLinePlaceholder":3152},[3002,4521,4522],{"class":3004,"line":3121},[3002,4523,4524],{},"    JUnit->>Test1: @BeforeEach\n",[3002,4526,4527],{"class":3004,"line":3149},[3002,4528,4529],{},"    Test1->>H2: CREATE DATABASE test_db_abc123\n",[3002,4531,4532],{"class":3004,"line":3156},[3002,4533,4534],{},"    Test1->>H2: CREATE TABLE authors...\n",[3002,4536,4537],{"class":3004,"line":3180},[3002,4538,4539],{},"    Test1->>H2: INSERT INTO authors...\n",[3002,4541,4542],{"class":3004,"line":3202},[3002,4543,4544],{},"    Test1->>H2: SELECT * FROM authors\n",[3002,4546,4547],{"class":3004,"line":3207},[3002,4548,4549],{},"    Test1->>H2: @AfterEach → close()\n",[3002,4551,4552],{"class":3004,"line":3228},[3002,4553,4554],{},"    H2-->>Test1: БД видалена з пам'яті\n",[3002,4556,4557],{"class":3004,"line":3257},[3002,4558,3153],{"emptyLinePlaceholder":3152},[3002,4560,4561],{"class":3004,"line":3262},[3002,4562,4563],{},"    JUnit->>Test2: @BeforeEach\n",[3002,4565,4566],{"class":3004,"line":3268},[3002,4567,4568],{},"    Test2->>H2: CREATE DATABASE test_db_xyz789 (нова!)\n",[3002,4570,4571],{"class":3004,"line":3282},[3002,4572,4573],{},"    Test2->>H2: CREATE TABLE authors...\n",[3002,4575,4576],{"class":3004,"line":3287},[3002,4577,4578],{},"    Test2->>H2: SELECT * FROM authors → порожня таблиця\n",[3002,4580,4581],{"class":3004,"line":3293},[3002,4582,4583],{},"    Test2->>H2: @AfterEach → close()\n",[3002,4585,4586],{"class":3004,"line":3307},[3002,4587,4588],{},"    H2-->>Test2: БД видалена з пам'яті\n",[3002,4590,4591],{"class":3004,"line":4214},[3002,4592,3153],{"emptyLinePlaceholder":3152},[3002,4594,4595],{"class":3004,"line":4409},[3002,4596,4597],{},"    Note over Test1,Test2: Тести повністю ізольовані\n",[3660,4599],{},[2959,4601,4603],{"id":4602},"налаштування-тестового-проєкту","Налаштування тестового проєкту",[3810,4605,4607],{"id":4606},"maven-залежності","Maven залежності",[2964,4609,4610],{},"Для інтеграційних тестів потрібні наступні бібліотеки:",[2993,4612,4617],{"className":4613,"code":4614,"language":4615,"meta":4616,"style":2998},"language-xml shiki shiki-themes light-plus dark-plus dark-plus","\u003C!-- pom.xml -->\n\u003Cdependencies>\n    \u003C!-- JUnit 5 (Jupiter) — тестовий фреймворк -->\n    \u003Cdependency>\n        \u003CgroupId>org.junit.jupiter\u003C/groupId>\n        \u003CartifactId>junit-jupiter\u003C/artifactId>\n        \u003Cversion>5.10.2\u003C/version>\n        \u003Cscope>test\u003C/scope>\n    \u003C/dependency>\n\n    \u003C!-- AssertJ — fluent assertions (читабельніші за JUnit assertions) -->\n    \u003Cdependency>\n        \u003CgroupId>org.assertj\u003C/groupId>\n        \u003CartifactId>assertj-core\u003C/artifactId>\n        \u003Cversion>3.25.3\u003C/version>\n        \u003Cscope>test\u003C/scope>\n    \u003C/dependency>\n\n    \u003C!-- H2 Database — embedded БД для тестів -->\n    \u003Cdependency>\n        \u003CgroupId>com.h2database\u003C/groupId>\n        \u003CartifactId>h2\u003C/artifactId>\n        \u003Cversion>2.2.224\u003C/version>\n        \u003Cscope>test\u003C/scope>\n    \u003C/dependency>\n\n    \u003C!-- SLF4J Simple — логування для тестів (опціонально) -->\n    \u003Cdependency>\n        \u003CgroupId>org.slf4j\u003C/groupId>\n        \u003CartifactId>slf4j-simple\u003C/artifactId>\n        \u003Cversion>2.0.12\u003C/version>\n        \u003Cscope>test\u003C/scope>\n    \u003C/dependency>\n\u003C/dependencies>\n","xml","showLineNumbers",[2968,4618,4619,4624,4636,4641,4651,4672,4690,4708,4726,4735,4739,4744,4752,4769,4786,4803,4819,4827,4831,4836,4844,4861,4877,4894,4910,4918,4922,4927,4935,4952,4970,4988,5005,5014],{"__ignoreMap":2998},[3002,4620,4621],{"class":3004,"line":3005},[3002,4622,4623],{"class":3032},"\u003C!-- pom.xml -->\n",[3002,4625,4626,4629,4633],{"class":3004,"line":3016},[3002,4627,4165],{"class":4628},"s0P7L",[3002,4630,4632],{"class":4631},"sKtos","dependencies",[3002,4634,4635],{"class":4628},">\n",[3002,4637,4638],{"class":3004,"line":3029},[3002,4639,4640],{"class":3032},"    \u003C!-- JUnit 5 (Jupiter) — тестовий фреймворк -->\n",[3002,4642,4643,4646,4649],{"class":3004,"line":3036},[3002,4644,4645],{"class":4628},"    \u003C",[3002,4647,4648],{"class":4631},"dependency",[3002,4650,4635],{"class":4628},[3002,4652,4653,4656,4659,4662,4665,4668,4670],{"class":3004,"line":3067},[3002,4654,4655],{"class":4628},"        \u003C",[3002,4657,4658],{"class":4631},"groupId",[3002,4660,4661],{"class":4628},">",[3002,4663,4664],{"class":3008},"org.junit.jupiter",[3002,4666,4667],{"class":4628},"\u003C/",[3002,4669,4658],{"class":4631},[3002,4671,4635],{"class":4628},[3002,4673,4674,4676,4679,4681,4684,4686,4688],{"class":3004,"line":3091},[3002,4675,4655],{"class":4628},[3002,4677,4678],{"class":4631},"artifactId",[3002,4680,4661],{"class":4628},[3002,4682,4683],{"class":3008},"junit-jupiter",[3002,4685,4667],{"class":4628},[3002,4687,4678],{"class":4631},[3002,4689,4635],{"class":4628},[3002,4691,4692,4694,4697,4699,4702,4704,4706],{"class":3004,"line":3121},[3002,4693,4655],{"class":4628},[3002,4695,4696],{"class":4631},"version",[3002,4698,4661],{"class":4628},[3002,4700,4701],{"class":3008},"5.10.2",[3002,4703,4667],{"class":4628},[3002,4705,4696],{"class":4631},[3002,4707,4635],{"class":4628},[3002,4709,4710,4712,4715,4717,4720,4722,4724],{"class":3004,"line":3149},[3002,4711,4655],{"class":4628},[3002,4713,4714],{"class":4631},"scope",[3002,4716,4661],{"class":4628},[3002,4718,4719],{"class":3008},"test",[3002,4721,4667],{"class":4628},[3002,4723,4714],{"class":4631},[3002,4725,4635],{"class":4628},[3002,4727,4728,4731,4733],{"class":3004,"line":3156},[3002,4729,4730],{"class":4628},"    \u003C/",[3002,4732,4648],{"class":4631},[3002,4734,4635],{"class":4628},[3002,4736,4737],{"class":3004,"line":3180},[3002,4738,3153],{"emptyLinePlaceholder":3152},[3002,4740,4741],{"class":3004,"line":3202},[3002,4742,4743],{"class":3032},"    \u003C!-- AssertJ — fluent assertions (читабельніші за JUnit assertions) -->\n",[3002,4745,4746,4748,4750],{"class":3004,"line":3207},[3002,4747,4645],{"class":4628},[3002,4749,4648],{"class":4631},[3002,4751,4635],{"class":4628},[3002,4753,4754,4756,4758,4760,4763,4765,4767],{"class":3004,"line":3228},[3002,4755,4655],{"class":4628},[3002,4757,4658],{"class":4631},[3002,4759,4661],{"class":4628},[3002,4761,4762],{"class":3008},"org.assertj",[3002,4764,4667],{"class":4628},[3002,4766,4658],{"class":4631},[3002,4768,4635],{"class":4628},[3002,4770,4771,4773,4775,4777,4780,4782,4784],{"class":3004,"line":3257},[3002,4772,4655],{"class":4628},[3002,4774,4678],{"class":4631},[3002,4776,4661],{"class":4628},[3002,4778,4779],{"class":3008},"assertj-core",[3002,4781,4667],{"class":4628},[3002,4783,4678],{"class":4631},[3002,4785,4635],{"class":4628},[3002,4787,4788,4790,4792,4794,4797,4799,4801],{"class":3004,"line":3262},[3002,4789,4655],{"class":4628},[3002,4791,4696],{"class":4631},[3002,4793,4661],{"class":4628},[3002,4795,4796],{"class":3008},"3.25.3",[3002,4798,4667],{"class":4628},[3002,4800,4696],{"class":4631},[3002,4802,4635],{"class":4628},[3002,4804,4805,4807,4809,4811,4813,4815,4817],{"class":3004,"line":3268},[3002,4806,4655],{"class":4628},[3002,4808,4714],{"class":4631},[3002,4810,4661],{"class":4628},[3002,4812,4719],{"class":3008},[3002,4814,4667],{"class":4628},[3002,4816,4714],{"class":4631},[3002,4818,4635],{"class":4628},[3002,4820,4821,4823,4825],{"class":3004,"line":3282},[3002,4822,4730],{"class":4628},[3002,4824,4648],{"class":4631},[3002,4826,4635],{"class":4628},[3002,4828,4829],{"class":3004,"line":3287},[3002,4830,3153],{"emptyLinePlaceholder":3152},[3002,4832,4833],{"class":3004,"line":3293},[3002,4834,4835],{"class":3032},"    \u003C!-- H2 Database — embedded БД для тестів -->\n",[3002,4837,4838,4840,4842],{"class":3004,"line":3307},[3002,4839,4645],{"class":4628},[3002,4841,4648],{"class":4631},[3002,4843,4635],{"class":4628},[3002,4845,4846,4848,4850,4852,4855,4857,4859],{"class":3004,"line":4214},[3002,4847,4655],{"class":4628},[3002,4849,4658],{"class":4631},[3002,4851,4661],{"class":4628},[3002,4853,4854],{"class":3008},"com.h2database",[3002,4856,4667],{"class":4628},[3002,4858,4658],{"class":4631},[3002,4860,4635],{"class":4628},[3002,4862,4863,4865,4867,4869,4871,4873,4875],{"class":3004,"line":4409},[3002,4864,4655],{"class":4628},[3002,4866,4678],{"class":4631},[3002,4868,4661],{"class":4628},[3002,4870,2959],{"class":3008},[3002,4872,4667],{"class":4628},[3002,4874,4678],{"class":4631},[3002,4876,4635],{"class":4628},[3002,4878,4879,4881,4883,4885,4888,4890,4892],{"class":3004,"line":4415},[3002,4880,4655],{"class":4628},[3002,4882,4696],{"class":4631},[3002,4884,4661],{"class":4628},[3002,4886,4887],{"class":3008},"2.2.224",[3002,4889,4667],{"class":4628},[3002,4891,4696],{"class":4631},[3002,4893,4635],{"class":4628},[3002,4895,4896,4898,4900,4902,4904,4906,4908],{"class":3004,"line":4420},[3002,4897,4655],{"class":4628},[3002,4899,4714],{"class":4631},[3002,4901,4661],{"class":4628},[3002,4903,4719],{"class":3008},[3002,4905,4667],{"class":4628},[3002,4907,4714],{"class":4631},[3002,4909,4635],{"class":4628},[3002,4911,4912,4914,4916],{"class":3004,"line":4425},[3002,4913,4730],{"class":4628},[3002,4915,4648],{"class":4631},[3002,4917,4635],{"class":4628},[3002,4919,4920],{"class":3004,"line":4432},[3002,4921,3153],{"emptyLinePlaceholder":3152},[3002,4923,4924],{"class":3004,"line":4441},[3002,4925,4926],{"class":3032},"    \u003C!-- SLF4J Simple — логування для тестів (опціонально) -->\n",[3002,4928,4929,4931,4933],{"class":3004,"line":4464},[3002,4930,4645],{"class":4628},[3002,4932,4648],{"class":4631},[3002,4934,4635],{"class":4628},[3002,4936,4937,4939,4941,4943,4946,4948,4950],{"class":3004,"line":4479},[3002,4938,4655],{"class":4628},[3002,4940,4658],{"class":4631},[3002,4942,4661],{"class":4628},[3002,4944,4945],{"class":3008},"org.slf4j",[3002,4947,4667],{"class":4628},[3002,4949,4658],{"class":4631},[3002,4951,4635],{"class":4628},[3002,4953,4955,4957,4959,4961,4964,4966,4968],{"class":3004,"line":4954},30,[3002,4956,4655],{"class":4628},[3002,4958,4678],{"class":4631},[3002,4960,4661],{"class":4628},[3002,4962,4963],{"class":3008},"slf4j-simple",[3002,4965,4667],{"class":4628},[3002,4967,4678],{"class":4631},[3002,4969,4635],{"class":4628},[3002,4971,4973,4975,4977,4979,4982,4984,4986],{"class":3004,"line":4972},31,[3002,4974,4655],{"class":4628},[3002,4976,4696],{"class":4631},[3002,4978,4661],{"class":4628},[3002,4980,4981],{"class":3008},"2.0.12",[3002,4983,4667],{"class":4628},[3002,4985,4696],{"class":4631},[3002,4987,4635],{"class":4628},[3002,4989,4991,4993,4995,4997,4999,5001,5003],{"class":3004,"line":4990},32,[3002,4992,4655],{"class":4628},[3002,4994,4714],{"class":4631},[3002,4996,4661],{"class":4628},[3002,4998,4719],{"class":3008},[3002,5000,4667],{"class":4628},[3002,5002,4714],{"class":4631},[3002,5004,4635],{"class":4628},[3002,5006,5008,5010,5012],{"class":3004,"line":5007},33,[3002,5009,4730],{"class":4628},[3002,5011,4648],{"class":4631},[3002,5013,4635],{"class":4628},[3002,5015,5017,5019,5021],{"class":3004,"line":5016},34,[3002,5018,4667],{"class":4628},[3002,5020,4632],{"class":4631},[3002,5022,4635],{"class":4628},[2964,5024,5025],{},[2989,5026,5027],{},"Gradle (альтернатива):",[2993,5029,5033],{"className":5030,"code":5031,"language":5032,"meta":2998,"style":2998},"language-groovy shiki shiki-themes light-plus dark-plus dark-plus","dependencies {\n    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'\n    testImplementation 'org.assertj:assertj-core:3.25.3'\n    testImplementation 'com.h2database:h2:2.2.224'\n    testImplementation 'org.slf4j:slf4j-simple:2.0.12'\n}\n","groovy",[2968,5034,5035,5040,5045,5050,5055,5060],{"__ignoreMap":2998},[3002,5036,5037],{"class":3004,"line":3005},[3002,5038,5039],{},"dependencies {\n",[3002,5041,5042],{"class":3004,"line":3016},[3002,5043,5044],{},"    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'\n",[3002,5046,5047],{"class":3004,"line":3029},[3002,5048,5049],{},"    testImplementation 'org.assertj:assertj-core:3.25.3'\n",[3002,5051,5052],{"class":3004,"line":3036},[3002,5053,5054],{},"    testImplementation 'com.h2database:h2:2.2.224'\n",[3002,5056,5057],{"class":3004,"line":3067},[3002,5058,5059],{},"    testImplementation 'org.slf4j:slf4j-simple:2.0.12'\n",[3002,5061,5062],{"class":3004,"line":3091},[3002,5063,3310],{},[3660,5065],{},[3810,5067,5069],{"id":5068},"структура-тестових-ресурсів","Структура тестових ресурсів",[2964,5071,5072],{},"Організуємо тестові файли за наступною структурою:",[2993,5074,5077],{"className":5075,"code":5076,"language":3781},[3779],"src/\n├── main/\n│   └── java/\n│       └── com/example/audiobook/\n│           ├── domain/\n│           ├── repository/\n│           └── db/\n└── test/\n    ├── java/\n    │   └── com/example/audiobook/\n    │       ├── repository/\n    │       │   ├── AbstractRepositoryTest.java      ← базовий клас\n    │       │   ├── JdbcAuthorRepositoryTest.java\n    │       │   ├── JdbcGenreRepositoryTest.java\n    │       │   └── JdbcAudiobookRepositoryTest.java\n    │       └── testutil/\n    │           ├── TestDataFactory.java              ← фабрика тестових даних\n    │           └── DatabaseTestUtil.java             ← утиліти для БД\n    └── resources/\n        ├── ddl_h2.sql                                ← DDL-скрипт\n        ├── test-data-authors.sql                     ← тестові дані (опціонально)\n        └── logback-test.xml                          ← конфігурація логування\n",[2968,5078,5076],{"__ignoreMap":2998},[2964,5080,5081,5087],{},[2989,5082,5083,5084],{},"Файл ",[2968,5085,5086],{},"src/test/resources/ddl_h2.sql"," (адаптований для H2):",[2993,5089,5093],{"className":5090,"code":5091,"language":5092,"meta":4616,"style":2998},"language-sql shiki shiki-themes light-plus dark-plus dark-plus","-- H2 не підтримує ENUM у старих версіях — використовуємо VARCHAR з CHECK\nCREATE TABLE authors (\n    id          UUID         PRIMARY KEY,\n    first_name  VARCHAR(64)  NOT NULL,\n    last_name   VARCHAR(64)  NOT NULL,\n    bio         TEXT,\n    image_path  VARCHAR(2048)\n);\n\nCREATE TABLE genres (\n    id          UUID         PRIMARY KEY,\n    name        VARCHAR(64)  NOT NULL UNIQUE,\n    description TEXT\n);\n\nCREATE TABLE audiobooks (\n    id               UUID         PRIMARY KEY,\n    author_id        UUID         NOT NULL,\n    genre_id         UUID         NOT NULL,\n    title            VARCHAR(255) NOT NULL,\n    duration         INTEGER      NOT NULL CHECK (duration > 0),\n    release_year     INTEGER      NOT NULL \n                     CHECK (release_year >= 1900 AND release_year \u003C= YEAR(CURRENT_DATE) + 1),\n    description      TEXT,\n    cover_image_path VARCHAR(2048),\n    \n    CONSTRAINT audiobooks_author_fk \n        FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE,\n    CONSTRAINT audiobooks_genre_fk \n        FOREIGN KEY (genre_id) REFERENCES genres(id) ON DELETE CASCADE\n);\n\nCREATE INDEX audiobooks_author_id_idx ON audiobooks(author_id);\nCREATE INDEX audiobooks_genre_id_idx  ON audiobooks(genre_id);\n\nCREATE TABLE users (\n    id            UUID         PRIMARY KEY,\n    username      VARCHAR(64)  NOT NULL UNIQUE CHECK (LENGTH(TRIM(username)) > 0),\n    password_hash VARCHAR(128) NOT NULL,\n    email         VARCHAR(376),\n    avatar_path   VARCHAR(2048)\n);\n\nCREATE INDEX users_email_idx ON users(email);\n","sql",[2968,5094,5095,5100,5114,5125,5146,5163,5173,5188,5192,5196,5207,5215,5236,5244,5248,5252,5263,5272,5281,5290,5309,5332,5344,5371,5380,5393,5397,5405,5424,5431,5446,5450,5454,5470,5485,5490,5502,5512,5550,5569,5584,5598,5603,5608],{"__ignoreMap":2998},[3002,5096,5097],{"class":3004,"line":3005},[3002,5098,5099],{"class":3032},"-- H2 не підтримує ENUM у старих версіях — використовуємо VARCHAR з CHECK\n",[3002,5101,5102,5105,5108,5111],{"class":3004,"line":3016},[3002,5103,5104],{"class":4021},"CREATE",[3002,5106,5107],{"class":4021}," TABLE",[3002,5109,5110],{"class":3022}," authors",[3002,5112,5113],{"class":3008}," (\n",[3002,5115,5116,5119,5122],{"class":3004,"line":3029},[3002,5117,5118],{"class":3008},"    id          UUID         ",[3002,5120,5121],{"class":4021},"PRIMARY KEY",[3002,5123,5124],{"class":3008},",\n",[3002,5126,5127,5130,5133,5135,5138,5141,5144],{"class":3004,"line":3036},[3002,5128,5129],{"class":3008},"    first_name  ",[3002,5131,5132],{"class":4021},"VARCHAR",[3002,5134,3052],{"class":3008},[3002,5136,5137],{"class":3143},"64",[3002,5139,5140],{"class":3008},")  ",[3002,5142,5143],{"class":4021},"NOT NULL",[3002,5145,5124],{"class":3008},[3002,5147,5148,5151,5153,5155,5157,5159,5161],{"class":3004,"line":3067},[3002,5149,5150],{"class":3008},"    last_name   ",[3002,5152,5132],{"class":4021},[3002,5154,3052],{"class":3008},[3002,5156,5137],{"class":3143},[3002,5158,5140],{"class":3008},[3002,5160,5143],{"class":4021},[3002,5162,5124],{"class":3008},[3002,5164,5165,5168,5171],{"class":3004,"line":3091},[3002,5166,5167],{"class":3008},"    bio         ",[3002,5169,5170],{"class":4021},"TEXT",[3002,5172,5124],{"class":3008},[3002,5174,5175,5178,5180,5182,5185],{"class":3004,"line":3121},[3002,5176,5177],{"class":3008},"    image_path  ",[3002,5179,5132],{"class":4021},[3002,5181,3052],{"class":3008},[3002,5183,5184],{"class":3143},"2048",[3002,5186,5187],{"class":3008},")\n",[3002,5189,5190],{"class":3004,"line":3149},[3002,5191,3064],{"class":3008},[3002,5193,5194],{"class":3004,"line":3156},[3002,5195,3153],{"emptyLinePlaceholder":3152},[3002,5197,5198,5200,5202,5205],{"class":3004,"line":3180},[3002,5199,5104],{"class":4021},[3002,5201,5107],{"class":4021},[3002,5203,5204],{"class":3022}," genres",[3002,5206,5113],{"class":3008},[3002,5208,5209,5211,5213],{"class":3004,"line":3202},[3002,5210,5118],{"class":3008},[3002,5212,5121],{"class":4021},[3002,5214,5124],{"class":3008},[3002,5216,5217,5220,5223,5225,5227,5229,5231,5234],{"class":3004,"line":3207},[3002,5218,5219],{"class":4021},"    name",[3002,5221,5222],{"class":4021},"        VARCHAR",[3002,5224,3052],{"class":3008},[3002,5226,5137],{"class":3143},[3002,5228,5140],{"class":3008},[3002,5230,5143],{"class":4021},[3002,5232,5233],{"class":4021}," UNIQUE",[3002,5235,5124],{"class":3008},[3002,5237,5238,5241],{"class":3004,"line":3228},[3002,5239,5240],{"class":4021},"    description",[3002,5242,5243],{"class":4021}," TEXT\n",[3002,5245,5246],{"class":3004,"line":3257},[3002,5247,3064],{"class":3008},[3002,5249,5250],{"class":3004,"line":3262},[3002,5251,3153],{"emptyLinePlaceholder":3152},[3002,5253,5254,5256,5258,5261],{"class":3004,"line":3268},[3002,5255,5104],{"class":4021},[3002,5257,5107],{"class":4021},[3002,5259,5260],{"class":3022}," audiobooks",[3002,5262,5113],{"class":3008},[3002,5264,5265,5268,5270],{"class":3004,"line":3282},[3002,5266,5267],{"class":3008},"    id               UUID         ",[3002,5269,5121],{"class":4021},[3002,5271,5124],{"class":3008},[3002,5273,5274,5277,5279],{"class":3004,"line":3287},[3002,5275,5276],{"class":3008},"    author_id        UUID         ",[3002,5278,5143],{"class":4021},[3002,5280,5124],{"class":3008},[3002,5282,5283,5286,5288],{"class":3004,"line":3293},[3002,5284,5285],{"class":3008},"    genre_id         UUID         ",[3002,5287,5143],{"class":4021},[3002,5289,5124],{"class":3008},[3002,5291,5292,5295,5297,5299,5302,5305,5307],{"class":3004,"line":3307},[3002,5293,5294],{"class":3008},"    title            ",[3002,5296,5132],{"class":4021},[3002,5298,3052],{"class":3008},[3002,5300,5301],{"class":3143},"255",[3002,5303,5304],{"class":3008},") ",[3002,5306,5143],{"class":4021},[3002,5308,5124],{"class":3008},[3002,5310,5311,5314,5317,5320,5323,5326,5329],{"class":3004,"line":4214},[3002,5312,5313],{"class":3008},"    duration         ",[3002,5315,5316],{"class":4021},"INTEGER",[3002,5318,5319],{"class":4021},"      NOT NULL",[3002,5321,5322],{"class":4021}," CHECK",[3002,5324,5325],{"class":3008}," (duration > ",[3002,5327,5328],{"class":3143},"0",[3002,5330,5331],{"class":3008},"),\n",[3002,5333,5334,5337,5339,5341],{"class":3004,"line":4409},[3002,5335,5336],{"class":3008},"    release_year     ",[3002,5338,5316],{"class":4021},[3002,5340,5319],{"class":4021},[3002,5342,5343],{"class":3008}," \n",[3002,5345,5346,5349,5352,5355,5358,5361,5364,5367,5369],{"class":3004,"line":4415},[3002,5347,5348],{"class":4021},"                     CHECK",[3002,5350,5351],{"class":3008}," (release_year >= ",[3002,5353,5354],{"class":3143},"1900",[3002,5356,5357],{"class":4021}," AND",[3002,5359,5360],{"class":3008}," release_year \u003C= ",[3002,5362,5363],{"class":3022},"YEAR",[3002,5365,5366],{"class":3008},"(CURRENT_DATE) + ",[3002,5368,3144],{"class":3143},[3002,5370,5331],{"class":3008},[3002,5372,5373,5375,5378],{"class":3004,"line":4420},[3002,5374,5240],{"class":4021},[3002,5376,5377],{"class":4021},"      TEXT",[3002,5379,5124],{"class":3008},[3002,5381,5382,5385,5387,5389,5391],{"class":3004,"line":4425},[3002,5383,5384],{"class":3008},"    cover_image_path ",[3002,5386,5132],{"class":4021},[3002,5388,3052],{"class":3008},[3002,5390,5184],{"class":3143},[3002,5392,5331],{"class":3008},[3002,5394,5395],{"class":3004,"line":4432},[3002,5396,3456],{"class":3008},[3002,5398,5399,5402],{"class":3004,"line":4441},[3002,5400,5401],{"class":4021},"    CONSTRAINT",[3002,5403,5404],{"class":3008}," audiobooks_author_fk \n",[3002,5406,5407,5410,5413,5416,5419,5422],{"class":3004,"line":4464},[3002,5408,5409],{"class":4021},"        FOREIGN KEY",[3002,5411,5412],{"class":3008}," (author_id) ",[3002,5414,5415],{"class":4021},"REFERENCES",[3002,5417,5418],{"class":3008}," authors(id) ",[3002,5420,5421],{"class":4021},"ON DELETE CASCADE",[3002,5423,5124],{"class":3008},[3002,5425,5426,5428],{"class":3004,"line":4479},[3002,5427,5401],{"class":4021},[3002,5429,5430],{"class":3008}," audiobooks_genre_fk \n",[3002,5432,5433,5435,5438,5440,5443],{"class":3004,"line":4954},[3002,5434,5409],{"class":4021},[3002,5436,5437],{"class":3008}," (genre_id) ",[3002,5439,5415],{"class":4021},[3002,5441,5442],{"class":3008}," genres(id) ",[3002,5444,5445],{"class":4021},"ON DELETE CASCADE\n",[3002,5447,5448],{"class":3004,"line":4972},[3002,5449,3064],{"class":3008},[3002,5451,5452],{"class":3004,"line":4990},[3002,5453,3153],{"emptyLinePlaceholder":3152},[3002,5455,5456,5458,5461,5464,5467],{"class":3004,"line":5007},[3002,5457,5104],{"class":4021},[3002,5459,5460],{"class":4021}," INDEX",[3002,5462,5463],{"class":3022}," audiobooks_author_id_idx",[3002,5465,5466],{"class":4021}," ON",[3002,5468,5469],{"class":3008}," audiobooks(author_id);\n",[3002,5471,5472,5474,5476,5479,5482],{"class":3004,"line":5016},[3002,5473,5104],{"class":4021},[3002,5475,5460],{"class":4021},[3002,5477,5478],{"class":3022}," audiobooks_genre_id_idx",[3002,5480,5481],{"class":4021},"  ON",[3002,5483,5484],{"class":3008}," audiobooks(genre_id);\n",[3002,5486,5488],{"class":3004,"line":5487},35,[3002,5489,3153],{"emptyLinePlaceholder":3152},[3002,5491,5493,5495,5497,5500],{"class":3004,"line":5492},36,[3002,5494,5104],{"class":4021},[3002,5496,5107],{"class":4021},[3002,5498,5499],{"class":3022}," users",[3002,5501,5113],{"class":3008},[3002,5503,5505,5508,5510],{"class":3004,"line":5504},37,[3002,5506,5507],{"class":3008},"    id            UUID         ",[3002,5509,5121],{"class":4021},[3002,5511,5124],{"class":3008},[3002,5513,5515,5518,5520,5522,5524,5526,5528,5530,5532,5535,5538,5540,5543,5546,5548],{"class":3004,"line":5514},38,[3002,5516,5517],{"class":3008},"    username      ",[3002,5519,5132],{"class":4021},[3002,5521,3052],{"class":3008},[3002,5523,5137],{"class":3143},[3002,5525,5140],{"class":3008},[3002,5527,5143],{"class":4021},[3002,5529,5233],{"class":4021},[3002,5531,5322],{"class":4021},[3002,5533,5534],{"class":3008}," (",[3002,5536,5537],{"class":4021},"LENGTH",[3002,5539,3052],{"class":3008},[3002,5541,5542],{"class":3022},"TRIM",[3002,5544,5545],{"class":3008},"(username)) > ",[3002,5547,5328],{"class":3143},[3002,5549,5331],{"class":3008},[3002,5551,5553,5556,5558,5560,5563,5565,5567],{"class":3004,"line":5552},39,[3002,5554,5555],{"class":3008},"    password_hash ",[3002,5557,5132],{"class":4021},[3002,5559,3052],{"class":3008},[3002,5561,5562],{"class":3143},"128",[3002,5564,5304],{"class":3008},[3002,5566,5143],{"class":4021},[3002,5568,5124],{"class":3008},[3002,5570,5572,5575,5577,5579,5582],{"class":3004,"line":5571},40,[3002,5573,5574],{"class":3008},"    email         ",[3002,5576,5132],{"class":4021},[3002,5578,3052],{"class":3008},[3002,5580,5581],{"class":3143},"376",[3002,5583,5331],{"class":3008},[3002,5585,5587,5590,5592,5594,5596],{"class":3004,"line":5586},41,[3002,5588,5589],{"class":3008},"    avatar_path   ",[3002,5591,5132],{"class":4021},[3002,5593,3052],{"class":3008},[3002,5595,5184],{"class":3143},[3002,5597,5187],{"class":3008},[3002,5599,5601],{"class":3004,"line":5600},42,[3002,5602,3064],{"class":3008},[3002,5604,5606],{"class":3004,"line":5605},43,[3002,5607,3153],{"emptyLinePlaceholder":3152},[3002,5609,5611,5613,5615,5618,5620],{"class":3004,"line":5610},44,[3002,5612,5104],{"class":4021},[3002,5614,5460],{"class":4021},[3002,5616,5617],{"class":3022}," users_email_idx",[3002,5619,5466],{"class":4021},[3002,5621,5622],{"class":3008}," users(email);\n",[3971,5624,5625,5630],{},[2964,5626,5627],{},[2989,5628,5629],{},"Відмінності H2 від PostgreSQL:",[3332,5631,5632,5640,5649,5662],{},[3335,5633,5634,5636,5637,5639],{},[2968,5635,5170],{}," у H2 є аліасом для ",[2968,5638,5132],{}," (без обмеження довжини)",[3335,5641,5642,5645,5646],{},[2968,5643,5644],{},"EXTRACT(YEAR FROM CURRENT_DATE)"," → ",[2968,5647,5648],{},"YEAR(CURRENT_DATE)",[3335,5650,5651,5654,5655,5657,5658,5661],{},[2968,5652,5653],{},"ENUM"," замінено на ",[2968,5656,5132],{}," з ",[2968,5659,5660],{},"CHECK"," constraint (або використовуйте H2 2.x з підтримкою ENUM)",[3335,5663,5664],{},"Синтаксис FK трохи відрізняється (але сумісний)",[3660,5666],{},[2959,5668,5670],{"id":5669},"патерн-aaa-arrange-act-assert","Патерн AAA (Arrange-Act-Assert)",[2964,5672,5673,5676],{},[2989,5674,5675],{},"AAA"," (Arrange-Act-Assert) — це структурний патерн організації тестів, що робить їх максимально читабельними.",[3810,5678,5680],{"id":5679},"три-фази-тесту","Три фази тесту",[2993,5682,5684],{"className":2995,"code":5683,"language":2997,"meta":2998,"style":2998},"@Test\nvoid testName() {\n    // ═══════════════════════════════════════════════════════════\n    // Arrange (Підготовка)\n    // ═══════════════════════════════════════════════════════════\n    // Створити тестові дані, налаштувати залежності\n    \n    // ═══════════════════════════════════════════════════════════\n    // Act (Дія)\n    // ═══════════════════════════════════════════════════════════\n    // Виконати метод, що тестується\n    \n    // ═══════════════════════════════════════════════════════════\n    // Assert (Перевірка)\n    // ═══════════════════════════════════════════════════════════\n    // Перевірити результат\n}\n",[2968,5685,5686,5692,5701,5706,5711,5715,5720,5724,5728,5733,5737,5742,5746,5750,5755,5759,5764],{"__ignoreMap":2998},[3002,5687,5688,5690],{"class":3004,"line":3005},[3002,5689,3009],{"class":3008},[3002,5691,3013],{"class":3012},[3002,5693,5694,5696,5699],{"class":3004,"line":3016},[3002,5695,3019],{"class":3012},[3002,5697,5698],{"class":3022}," testName",[3002,5700,3026],{"class":3008},[3002,5702,5703],{"class":3004,"line":3029},[3002,5704,5705],{"class":3032},"    // ═══════════════════════════════════════════════════════════\n",[3002,5707,5708],{"class":3004,"line":3036},[3002,5709,5710],{"class":3032},"    // Arrange (Підготовка)\n",[3002,5712,5713],{"class":3004,"line":3067},[3002,5714,5705],{"class":3032},[3002,5716,5717],{"class":3004,"line":3091},[3002,5718,5719],{"class":3032},"    // Створити тестові дані, налаштувати залежності\n",[3002,5721,5722],{"class":3004,"line":3121},[3002,5723,3456],{"class":3008},[3002,5725,5726],{"class":3004,"line":3149},[3002,5727,5705],{"class":3032},[3002,5729,5730],{"class":3004,"line":3156},[3002,5731,5732],{"class":3032},"    // Act (Дія)\n",[3002,5734,5735],{"class":3004,"line":3180},[3002,5736,5705],{"class":3032},[3002,5738,5739],{"class":3004,"line":3202},[3002,5740,5741],{"class":3032},"    // Виконати метод, що тестується\n",[3002,5743,5744],{"class":3004,"line":3207},[3002,5745,3456],{"class":3008},[3002,5747,5748],{"class":3004,"line":3228},[3002,5749,5705],{"class":3032},[3002,5751,5752],{"class":3004,"line":3257},[3002,5753,5754],{"class":3032},"    // Assert (Перевірка)\n",[3002,5756,5757],{"class":3004,"line":3262},[3002,5758,5705],{"class":3032},[3002,5760,5761],{"class":3004,"line":3268},[3002,5762,5763],{"class":3032},"    // Перевірити результат\n",[3002,5765,5766],{"class":3004,"line":3282},[3002,5767,3310],{"class":3008},[2964,5769,5770],{},[2989,5771,5772],{},"Приклад:",[2993,5774,5776],{"className":2995,"code":5775,"language":2997,"meta":4616,"style":2998},"@Test\nvoid save_shouldInsertNewAuthor_whenValidData() {\n    // ═══ Arrange ═══\n    AuthorRepository repo = new JdbcAuthorRepository(connectionManager);\n    Author author = new Author(\"Тарас\", \"Шевченко\");\n    author.setBio(\"Український поет і художник\");\n    \n    // ═══ Act ═══\n    repo.save(author);\n    \n    // ═══ Assert ═══\n    Author loaded = repo.findById(author.getId()).orElseThrow();\n    assertThat(loaded.getFirstName()).isEqualTo(\"Тарас\");\n    assertThat(loaded.getLastName()).isEqualTo(\"Шевченко\");\n    assertThat(loaded.getBio()).isEqualTo(\"Український поет і художник\");\n}\n",[2968,5777,5778,5784,5792,5797,5812,5836,5851,5855,5860,5870,5874,5879,5907,5929,5951,5973],{"__ignoreMap":2998},[3002,5779,5780,5782],{"class":3004,"line":3005},[3002,5781,3009],{"class":3008},[3002,5783,3013],{"class":3012},[3002,5785,5786,5788,5790],{"class":3004,"line":3016},[3002,5787,3019],{"class":3012},[3002,5789,3398],{"class":3022},[3002,5791,3026],{"class":3008},[3002,5793,5794],{"class":3004,"line":3029},[3002,5795,5796],{"class":3032},"    // ═══ Arrange ═══\n",[3002,5798,5799,5801,5803,5805,5807,5809],{"class":3004,"line":3036},[3002,5800,3210],{"class":3012},[3002,5802,3213],{"class":3042},[3002,5804,3046],{"class":3008},[3002,5806,3219],{"class":3218},[3002,5808,3222],{"class":3022},[3002,5810,5811],{"class":3008},"(connectionManager);\n",[3002,5813,5814,5816,5818,5820,5822,5824,5826,5829,5831,5834],{"class":3004,"line":3067},[3002,5815,3231],{"class":3012},[3002,5817,3234],{"class":3042},[3002,5819,3046],{"class":3008},[3002,5821,3219],{"class":3218},[3002,5823,3241],{"class":3022},[3002,5825,3052],{"class":3008},[3002,5827,5828],{"class":3246},"\"Тарас\"",[3002,5830,2975],{"class":3008},[3002,5832,5833],{"class":3246},"\"Шевченко\"",[3002,5835,3064],{"class":3008},[3002,5837,5838,5840,5842,5844,5846,5849],{"class":3004,"line":3091},[3002,5839,3483],{"class":3042},[3002,5841,3058],{"class":3008},[3002,5843,3488],{"class":3022},[3002,5845,3052],{"class":3008},[3002,5847,5848],{"class":3246},"\"Український поет і художник\"",[3002,5850,3064],{"class":3008},[3002,5852,5853],{"class":3004,"line":3121},[3002,5854,3456],{"class":3008},[3002,5856,5857],{"class":3004,"line":3149},[3002,5858,5859],{"class":3032},"    // ═══ Act ═══\n",[3002,5861,5862,5864,5866,5868],{"class":3004,"line":3156},[3002,5863,3271],{"class":3042},[3002,5865,3058],{"class":3008},[3002,5867,3276],{"class":3022},[3002,5869,3279],{"class":3008},[3002,5871,5872],{"class":3004,"line":3180},[3002,5873,3456],{"class":3008},[3002,5875,5876],{"class":3004,"line":3202},[3002,5877,5878],{"class":3032},"    // ═══ Assert ═══\n",[3002,5880,5881,5883,5885,5887,5889,5891,5893,5895,5897,5899,5901,5903,5905],{"class":3004,"line":3207},[3002,5882,3231],{"class":3012},[3002,5884,3530],{"class":3042},[3002,5886,3046],{"class":3008},[3002,5888,3535],{"class":3042},[3002,5890,3058],{"class":3008},[3002,5892,3540],{"class":3022},[3002,5894,3052],{"class":3008},[3002,5896,3545],{"class":3042},[3002,5898,3058],{"class":3008},[3002,5900,3550],{"class":3022},[3002,5902,3136],{"class":3008},[3002,5904,3555],{"class":3022},[3002,5906,3304],{"class":3008},[3002,5908,5909,5911,5913,5915,5917,5919,5921,5923,5925,5927],{"class":3004,"line":3228},[3002,5910,3562],{"class":3022},[3002,5912,3052],{"class":3008},[3002,5914,3567],{"class":3042},[3002,5916,3058],{"class":3008},[3002,5918,3572],{"class":3022},[3002,5920,3136],{"class":3008},[3002,5922,3577],{"class":3022},[3002,5924,3052],{"class":3008},[3002,5926,5828],{"class":3246},[3002,5928,3064],{"class":3008},[3002,5930,5931,5933,5935,5937,5939,5941,5943,5945,5947,5949],{"class":3004,"line":3257},[3002,5932,3562],{"class":3022},[3002,5934,3052],{"class":3008},[3002,5936,3567],{"class":3042},[3002,5938,3058],{"class":3008},[3002,5940,3596],{"class":3022},[3002,5942,3136],{"class":3008},[3002,5944,3577],{"class":3022},[3002,5946,3052],{"class":3008},[3002,5948,5833],{"class":3246},[3002,5950,3064],{"class":3008},[3002,5952,5953,5955,5957,5959,5961,5963,5965,5967,5969,5971],{"class":3004,"line":3262},[3002,5954,3562],{"class":3022},[3002,5956,3052],{"class":3008},[3002,5958,3567],{"class":3042},[3002,5960,3058],{"class":3008},[3002,5962,3619],{"class":3022},[3002,5964,3136],{"class":3008},[3002,5966,3577],{"class":3022},[3002,5968,3052],{"class":3008},[3002,5970,5848],{"class":3246},[3002,5972,3064],{"class":3008},[3002,5974,5975],{"class":3004,"line":3268},[3002,5976,3310],{"class":3008},[2964,5978,5979],{},[2989,5980,5981],{},"Чому AAA кращий за хаотичний код:",[2993,5983,5985],{"className":2995,"code":5984,"language":2997,"meta":2998,"style":2998},"// ❌ ПОГАНО: незрозуміло, де підготовка, де дія, де перевірка\n@Test\nvoid testSave() {\n    Author author = new Author(\"Тарас\", \"Шевченко\");\n    repo.save(author);\n    assertThat(author.getId()).isNotNull();\n    author.setBio(\"Поет\");\n    Author loaded = repo.findById(author.getId()).orElseThrow();\n    assertThat(loaded.getFirstName()).isEqualTo(\"Тарас\");\n    repo.update(author); // Що це тут робить?\n}\n",[2968,5986,5987,5992,5998,6007,6029,6039,6058,6073,6101,6123,6138],{"__ignoreMap":2998},[3002,5988,5989],{"class":3004,"line":3005},[3002,5990,5991],{"class":3032},"// ❌ ПОГАНО: незрозуміло, де підготовка, де дія, де перевірка\n",[3002,5993,5994,5996],{"class":3004,"line":3016},[3002,5995,3009],{"class":3008},[3002,5997,3013],{"class":3012},[3002,5999,6000,6002,6005],{"class":3004,"line":3029},[3002,6001,3019],{"class":3012},[3002,6003,6004],{"class":3022}," testSave",[3002,6006,3026],{"class":3008},[3002,6008,6009,6011,6013,6015,6017,6019,6021,6023,6025,6027],{"class":3004,"line":3036},[3002,6010,3231],{"class":3012},[3002,6012,3234],{"class":3042},[3002,6014,3046],{"class":3008},[3002,6016,3219],{"class":3218},[3002,6018,3241],{"class":3022},[3002,6020,3052],{"class":3008},[3002,6022,5828],{"class":3246},[3002,6024,2975],{"class":3008},[3002,6026,5833],{"class":3246},[3002,6028,3064],{"class":3008},[3002,6030,6031,6033,6035,6037],{"class":3004,"line":3067},[3002,6032,3271],{"class":3042},[3002,6034,3058],{"class":3008},[3002,6036,3276],{"class":3022},[3002,6038,3279],{"class":3008},[3002,6040,6041,6043,6045,6047,6049,6051,6053,6056],{"class":3004,"line":3091},[3002,6042,3562],{"class":3022},[3002,6044,3052],{"class":3008},[3002,6046,3545],{"class":3042},[3002,6048,3058],{"class":3008},[3002,6050,3550],{"class":3022},[3002,6052,3136],{"class":3008},[3002,6054,6055],{"class":3022},"isNotNull",[3002,6057,3304],{"class":3008},[3002,6059,6060,6062,6064,6066,6068,6071],{"class":3004,"line":3121},[3002,6061,3483],{"class":3042},[3002,6063,3058],{"class":3008},[3002,6065,3488],{"class":3022},[3002,6067,3052],{"class":3008},[3002,6069,6070],{"class":3246},"\"Поет\"",[3002,6072,3064],{"class":3008},[3002,6074,6075,6077,6079,6081,6083,6085,6087,6089,6091,6093,6095,6097,6099],{"class":3004,"line":3149},[3002,6076,3231],{"class":3012},[3002,6078,3530],{"class":3042},[3002,6080,3046],{"class":3008},[3002,6082,3535],{"class":3042},[3002,6084,3058],{"class":3008},[3002,6086,3540],{"class":3022},[3002,6088,3052],{"class":3008},[3002,6090,3545],{"class":3042},[3002,6092,3058],{"class":3008},[3002,6094,3550],{"class":3022},[3002,6096,3136],{"class":3008},[3002,6098,3555],{"class":3022},[3002,6100,3304],{"class":3008},[3002,6102,6103,6105,6107,6109,6111,6113,6115,6117,6119,6121],{"class":3004,"line":3156},[3002,6104,3562],{"class":3022},[3002,6106,3052],{"class":3008},[3002,6108,3567],{"class":3042},[3002,6110,3058],{"class":3008},[3002,6112,3572],{"class":3022},[3002,6114,3136],{"class":3008},[3002,6116,3577],{"class":3022},[3002,6118,3052],{"class":3008},[3002,6120,5828],{"class":3246},[3002,6122,3064],{"class":3008},[3002,6124,6125,6127,6129,6132,6135],{"class":3004,"line":3180},[3002,6126,3271],{"class":3042},[3002,6128,3058],{"class":3008},[3002,6130,6131],{"class":3022},"update",[3002,6133,6134],{"class":3008},"(author); ",[3002,6136,6137],{"class":3032},"// Що це тут робить?\n",[3002,6139,6140],{"class":3004,"line":3202},[3002,6141,3310],{"class":3008},[3768,6143,6144,6160],{},[2964,6145,6146,6149,6150,6153,6154,6156,6157,6159],{},[2989,6147,6148],{},"Правило одного Act:"," Кожен тест має викликати ",[2989,6151,6152],{},"лише один"," метод, що тестується. Якщо потрібно протестувати ",[2968,6155,2974],{}," і ",[2968,6158,2981],{}," — створіть два окремих тести.",[2993,6161,6163],{"className":2995,"code":6162,"language":2997,"meta":2998,"style":2998},"// ✅ ДОБРЕ: один тест = один метод\n@Test\nvoid save_shouldInsertAuthor() { /* тестує save() */ }\n\n@Test\nvoid update_shouldModifyAuthor() { /* тестує update() */ }\n\n// ❌ ПОГАНО: один тест = кілька методів\n@Test\nvoid saveAndUpdate() { \n    repo.save(author);\n    repo.update(author); // Який метод провалився, якщо тест червоний?\n}\n",[2968,6164,6165,6170,6176,6191,6195,6201,6215,6219,6224,6230,6240,6250,6263],{"__ignoreMap":2998},[3002,6166,6167],{"class":3004,"line":3005},[3002,6168,6169],{"class":3032},"// ✅ ДОБРЕ: один тест = один метод\n",[3002,6171,6172,6174],{"class":3004,"line":3016},[3002,6173,3009],{"class":3008},[3002,6175,3013],{"class":3012},[3002,6177,6178,6180,6182,6185,6188],{"class":3004,"line":3029},[3002,6179,3019],{"class":3012},[3002,6181,3023],{"class":3022},[3002,6183,6184],{"class":3008},"() { ",[3002,6186,6187],{"class":3032},"/* тестує save() */",[3002,6189,6190],{"class":3008}," }\n",[3002,6192,6193],{"class":3004,"line":3036},[3002,6194,3153],{"emptyLinePlaceholder":3152},[3002,6196,6197,6199],{"class":3004,"line":3067},[3002,6198,3009],{"class":3008},[3002,6200,3013],{"class":3012},[3002,6202,6203,6205,6208,6210,6213],{"class":3004,"line":3091},[3002,6204,3019],{"class":3012},[3002,6206,6207],{"class":3022}," update_shouldModifyAuthor",[3002,6209,6184],{"class":3008},[3002,6211,6212],{"class":3032},"/* тестує update() */",[3002,6214,6190],{"class":3008},[3002,6216,6217],{"class":3004,"line":3121},[3002,6218,3153],{"emptyLinePlaceholder":3152},[3002,6220,6221],{"class":3004,"line":3149},[3002,6222,6223],{"class":3032},"// ❌ ПОГАНО: один тест = кілька методів\n",[3002,6225,6226,6228],{"class":3004,"line":3156},[3002,6227,3009],{"class":3008},[3002,6229,3013],{"class":3012},[3002,6231,6232,6234,6237],{"class":3004,"line":3180},[3002,6233,3019],{"class":3012},[3002,6235,6236],{"class":3022}," saveAndUpdate",[3002,6238,6239],{"class":3008},"() { \n",[3002,6241,6242,6244,6246,6248],{"class":3004,"line":3202},[3002,6243,3271],{"class":3042},[3002,6245,3058],{"class":3008},[3002,6247,3276],{"class":3022},[3002,6249,3279],{"class":3008},[3002,6251,6252,6254,6256,6258,6260],{"class":3004,"line":3207},[3002,6253,3271],{"class":3042},[3002,6255,3058],{"class":3008},[3002,6257,6131],{"class":3022},[3002,6259,6134],{"class":3008},[3002,6261,6262],{"class":3032},"// Який метод провалився, якщо тест червоний?\n",[3002,6264,6265],{"class":3004,"line":3228},[3002,6266,3310],{"class":3008},[3660,6268],{},[3810,6270,6272],{"id":6271},"найменування-тестів-given-when-then","Найменування тестів: Given-When-Then",[2964,6274,6275,6278],{},[2989,6276,6277],{},"Конвенція найменування"," (Roy Osherove, \"The Art of Unit Testing\"):",[2993,6280,6283],{"className":6281,"code":6282,"language":3781},[3779],"methodName_shouldExpectedBehavior_whenStateUnderTest\n",[2968,6284,6282],{"__ignoreMap":2998},[2964,6286,6287],{},[2989,6288,6289],{},"Структура:",[3332,6291,6292,6305,6317],{},[3335,6293,6294,6297,6298,2975,6300,2975,6302,6304],{},[2968,6295,6296],{},"methodName"," — метод, що тестується (",[2968,6299,3276],{},[2968,6301,3540],{},[2968,6303,6131],{},")",[3335,6306,6307,6310,6311,2975,6314,6304],{},[2968,6308,6309],{},"shouldExpectedBehavior"," — очікувана поведінка (",[2968,6312,6313],{},"shouldInsertNewAuthor",[2968,6315,6316],{},"shouldReturnEmpty",[3335,6318,6319,6322,6323,2975,6326,2975,6329,6304],{},[2968,6320,6321],{},"whenStateUnderTest"," — умова/контекст (",[2968,6324,6325],{},"whenValidData",[2968,6327,6328],{},"whenNotExists",[2968,6330,6331],{},"whenDuplicateName",[2964,6333,6334],{},[2989,6335,6336],{},"Приклади:",[2993,6338,6340],{"className":2995,"code":6339,"language":2997,"meta":2998,"style":2998},"// Позитивні сценарії\nsave_shouldInsertNewAuthor_whenValidData()\nfindById_shouldReturnAuthor_whenExists()\nupdate_shouldModifyAllFields_whenAuthorExists()\ndeleteById_shouldRemoveAuthor_whenExists()\n\n// Негативні сценарії\nsave_shouldThrowException_whenDuplicateId()\nfindById_shouldReturnEmpty_whenNotExists()\nupdate_shouldThrowException_whenAuthorNotExists()\ndeleteById_shouldReturnFalse_whenNotExists()\n\n// Граничні випадки\nsave_shouldHandleNullBio_whenBioNotProvided()\nfindByLastName_shouldReturnEmptyList_whenNoMatches()\nfindAll_shouldReturnEmptyList_whenTableEmpty()\n\n// Складні сценарії\ndeleteAuthor_shouldCascadeDeleteAudiobooks_whenAuthorHasBooks()\nsave_shouldThrowException_whenForeignKeyViolation()\n",[2968,6341,6342,6347,6355,6362,6369,6376,6380,6385,6392,6399,6406,6413,6417,6422,6429,6436,6443,6447,6452,6459],{"__ignoreMap":2998},[3002,6343,6344],{"class":3004,"line":3005},[3002,6345,6346],{"class":3032},"// Позитивні сценарії\n",[3002,6348,6349,6352],{"class":3004,"line":3016},[3002,6350,6351],{"class":3022},"save_shouldInsertNewAuthor_whenValidData",[3002,6353,6354],{"class":3008},"()\n",[3002,6356,6357,6360],{"class":3004,"line":3029},[3002,6358,6359],{"class":3022},"findById_shouldReturnAuthor_whenExists",[3002,6361,6354],{"class":3008},[3002,6363,6364,6367],{"class":3004,"line":3036},[3002,6365,6366],{"class":3022},"update_shouldModifyAllFields_whenAuthorExists",[3002,6368,6354],{"class":3008},[3002,6370,6371,6374],{"class":3004,"line":3067},[3002,6372,6373],{"class":3022},"deleteById_shouldRemoveAuthor_whenExists",[3002,6375,6354],{"class":3008},[3002,6377,6378],{"class":3004,"line":3091},[3002,6379,3153],{"emptyLinePlaceholder":3152},[3002,6381,6382],{"class":3004,"line":3121},[3002,6383,6384],{"class":3032},"// Негативні сценарії\n",[3002,6386,6387,6390],{"class":3004,"line":3149},[3002,6388,6389],{"class":3022},"save_shouldThrowException_whenDuplicateId",[3002,6391,6354],{"class":3008},[3002,6393,6394,6397],{"class":3004,"line":3156},[3002,6395,6396],{"class":3022},"findById_shouldReturnEmpty_whenNotExists",[3002,6398,6354],{"class":3008},[3002,6400,6401,6404],{"class":3004,"line":3180},[3002,6402,6403],{"class":3022},"update_shouldThrowException_whenAuthorNotExists",[3002,6405,6354],{"class":3008},[3002,6407,6408,6411],{"class":3004,"line":3202},[3002,6409,6410],{"class":3022},"deleteById_shouldReturnFalse_whenNotExists",[3002,6412,6354],{"class":3008},[3002,6414,6415],{"class":3004,"line":3207},[3002,6416,3153],{"emptyLinePlaceholder":3152},[3002,6418,6419],{"class":3004,"line":3228},[3002,6420,6421],{"class":3032},"// Граничні випадки\n",[3002,6423,6424,6427],{"class":3004,"line":3257},[3002,6425,6426],{"class":3022},"save_shouldHandleNullBio_whenBioNotProvided",[3002,6428,6354],{"class":3008},[3002,6430,6431,6434],{"class":3004,"line":3262},[3002,6432,6433],{"class":3022},"findByLastName_shouldReturnEmptyList_whenNoMatches",[3002,6435,6354],{"class":3008},[3002,6437,6438,6441],{"class":3004,"line":3268},[3002,6439,6440],{"class":3022},"findAll_shouldReturnEmptyList_whenTableEmpty",[3002,6442,6354],{"class":3008},[3002,6444,6445],{"class":3004,"line":3282},[3002,6446,3153],{"emptyLinePlaceholder":3152},[3002,6448,6449],{"class":3004,"line":3287},[3002,6450,6451],{"class":3032},"// Складні сценарії\n",[3002,6453,6454,6457],{"class":3004,"line":3293},[3002,6455,6456],{"class":3022},"deleteAuthor_shouldCascadeDeleteAudiobooks_whenAuthorHasBooks",[3002,6458,6354],{"class":3008},[3002,6460,6461,6464],{"class":3004,"line":3307},[3002,6462,6463],{"class":3022},"save_shouldThrowException_whenForeignKeyViolation",[3002,6465,6354],{"class":3008},[2964,6467,6468],{},[2989,6469,6470],{},"Альтернативний стиль (BDD — Behavior-Driven Development):",[2993,6472,6474],{"className":2995,"code":6473,"language":2997,"meta":2998,"style":2998},"@Test\n@DisplayName(\"Given valid author data, when save is called, then author should be inserted\")\nvoid givenValidAuthorData_whenSaveIsCalled_thenAuthorShouldBeInserted() {\n    // ...\n}\n",[2968,6475,6476,6482,6496,6505,6510],{"__ignoreMap":2998},[3002,6477,6478,6480],{"class":3004,"line":3005},[3002,6479,3009],{"class":3008},[3002,6481,3013],{"class":3012},[3002,6483,6484,6486,6489,6491,6494],{"class":3004,"line":3016},[3002,6485,3009],{"class":3008},[3002,6487,6488],{"class":3012},"DisplayName",[3002,6490,3052],{"class":3008},[3002,6492,6493],{"class":3246},"\"Given valid author data, when save is called, then author should be inserted\"",[3002,6495,5187],{"class":3008},[3002,6497,6498,6500,6503],{"class":3004,"line":3029},[3002,6499,3019],{"class":3012},[3002,6501,6502],{"class":3022}," givenValidAuthorData_whenSaveIsCalled_thenAuthorShouldBeInserted",[3002,6504,3026],{"class":3008},[3002,6506,6507],{"class":3004,"line":3036},[3002,6508,6509],{"class":3032},"    // ...\n",[3002,6511,6512],{"class":3004,"line":3067},[3002,6513,3310],{"class":3008},[3971,6515,6516,6526],{},[2964,6517,6518,6521,6522,6525],{},[2989,6519,6520],{},"Вибір стилю — питання команди."," У цій статті ми використовуємо ",[2968,6523,6524],{},"methodName_should_when",", оскільки він коротший і природно читається у звітах JUnit:",[2993,6527,6530],{"className":6528,"code":6529,"language":3781},[3779],"✓ save_shouldInsertNewAuthor_whenValidData\n✓ findById_shouldReturnAuthor_whenExists\n✗ update_shouldModifyAllFields_whenAuthorExists\n",[2968,6531,6529],{"__ignoreMap":2998},[3660,6533],{},[2959,6535,6537],{"id":6536},"базовий-клас-abstractrepositorytest","Базовий клас AbstractRepositoryTest",[2964,6539,6540],{},"Щоб уникнути дублювання коду ініціалізації БД у кожному тестовому класі, створимо абстрактний базовий клас:",[2993,6542,6544],{"className":2995,"code":6543,"language":2997,"meta":4616,"style":2998},"package com.example.audiobook.repository;\n\nimport com.example.audiobook.db.ConnectionManager;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.BeforeEach;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStreamReader;\nimport java.nio.charset.StandardCharsets;\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.sql.Statement;\nimport java.util.UUID;\nimport java.util.stream.Collectors;\n\n/**\n * Базовий клас для інтеграційних тестів JDBC-репозиторіїв.\n * \u003Cp>\n * Надає спільну логіку:\n * \u003Cul>\n *   \u003Cli>Створення нової in-memory H2 БД перед кожним тестом\u003C/li>\n *   \u003Cli>Виконання DDL-скрипта (створення таблиць)\u003C/li>\n *   \u003Cli>Закриття з'єднання після тесту\u003C/li>\n *   \u003Cli>Утилітні методи для роботи з БД\u003C/li>\n * \u003C/ul>\n * \u003Cp>\n * \u003Cb>Використання:\u003C/b>\n * \u003Cpre>{@code\n * class JdbcAuthorRepositoryTest extends AbstractRepositoryTest {\n *     private AuthorRepository repository;\n *\n *     @BeforeEach\n *     void setUp() {\n *         super.setUp(); // викликати базову ініціалізацію\n *         repository = new JdbcAuthorRepository(connectionManager);\n *     }\n *\n *     @Test\n *     void save_shouldInsertAuthor() {\n *         // тест...\n *     }\n * }\n * }\u003C/pre>\n */\npublic abstract class AbstractRepositoryTest {\n\n    /**\n     * ConnectionManager для тестової БД.\n     * Доступний підкласам для створення репозиторіїв.\n     */\n    protected ConnectionManager connectionManager;\n\n    /**\n     * Ініціалізує нову in-memory H2 БД перед кожним тестом.\n     * \u003Cp>\n     * Кожен тест отримує унікальну порожню БД — це гарантує ізоляцію.\n     * DDL-скрипт виконується автоматично.\n     */\n    @BeforeEach\n    void setUp() throws SQLException, IOException {\n        // Унікальна назва БД для кожного тесту\n        String dbName = \"test_db_\" + UUID.randomUUID().toString().replace(\"-\", \"\");\n        \n        // Створити ConnectionManager для in-memory H2\n        // DB_CLOSE_DELAY=-1 → БД не закривається при закритті останнього Connection\n        // (потрібно для коректної роботи з пулом з'єднань)\n        String url = \"jdbc:h2:mem:\" + dbName + \";DB_CLOSE_DELAY=-1;MODE=PostgreSQL\";\n        connectionManager = new ConnectionManager(url, \"sa\", \"\");\n\n        // Виконати DDL-скрипт (створити таблиці)\n        executeDdlScript(\"ddl_h2.sql\");\n    }\n\n    /**\n     * Закриває ConnectionManager після кожного тесту.\n     * БД автоматично видаляється з пам'яті.\n     */\n    @AfterEach\n    void tearDown() {\n        if (connectionManager != null) {\n            connectionManager.close();\n        }\n    }\n\n    // ═══════════════════════════════════════════════════════════════════════\n    // Утилітні методи для роботи з БД\n    // ═══════════════════════════════════════════════════════════════════════\n\n    /**\n     * Виконує SQL-скрипт з ресурсів.\n     * \u003Cp>\n     * Скрипт має знаходитися у {@code src/test/resources/}.\n     * Підтримує багаторядкові команди, розділені {@code ;}.\n     *\n     * @param scriptPath шлях до скрипта відносно resources (наприклад, \"ddl_h2.sql\")\n     */\n    protected void executeDdlScript(String scriptPath) throws IOException, SQLException {\n        String sql = loadResourceAsString(scriptPath);\n        \n        try (Connection conn = connectionManager.getConnection();\n             Statement stmt = conn.createStatement()) {\n            \n            // Розділити скрипт на окремі команди (за ;)\n            String[] commands = sql.split(\";\");\n            for (String command : commands) {\n                String trimmed = command.trim();\n                if (!trimmed.isEmpty() && !trimmed.startsWith(\"--\")) {\n                    stmt.execute(trimmed);\n                }\n            }\n        }\n    }\n\n    /**\n     * Завантажує файл з ресурсів як рядок.\n     *\n     * @param resourcePath шлях до ресурсу (наприклад, \"ddl_h2.sql\")\n     * @return вміст файлу як рядок\n     */\n    protected String loadResourceAsString(String resourcePath) throws IOException {\n        try (BufferedReader reader = new BufferedReader(\n                new InputStreamReader(\n                    getClass().getClassLoader().getResourceAsStream(resourcePath),\n                    StandardCharsets.UTF_8))) {\n            return reader.lines().collect(Collectors.joining(\"\\n\"));\n        }\n    }\n\n    /**\n     * Виконує довільний SQL-запит (для підготовки тестових даних).\n     * \u003Cp>\n     * Використовується у Arrange-фазі для створення складних сценаріїв.\n     *\n     * @param sql SQL-команда (INSERT, UPDATE, DELETE)\n     */\n    protected void executeSql(String sql) throws SQLException {\n        try (Connection conn = connectionManager.getConnection();\n             Statement stmt = conn.createStatement()) {\n            stmt.execute(sql);\n        }\n    }\n\n    /**\n     * Підраховує кількість рядків у таблиці.\n     * Корисно для перевірки у Assert-фазі.\n     *\n     * @param tableName назва таблиці\n     * @return кількість рядків\n     */\n    protected long countRowsInTable(String tableName) throws SQLException {\n        String sql = \"SELECT COUNT(*) FROM \" + tableName;\n        try (Connection conn = connectionManager.getConnection();\n             Statement stmt = conn.createStatement();\n             var rs = stmt.executeQuery(sql)) {\n            rs.next();\n            return rs.getLong(1);\n        }\n    }\n\n    /**\n     * Очищає всі дані з таблиці (але залишає структуру).\n     * Використовується рідко — зазвичай кожен тест отримує нову БД.\n     *\n     * @param tableName назва таблиці\n     */\n    protected void truncateTable(String tableName) throws SQLException {\n        String sql = \"TRUNCATE TABLE \" + tableName;\n        try (Connection conn = connectionManager.getConnection();\n             Statement stmt = conn.createStatement()) {\n            stmt.execute(sql);\n        }\n    }\n}\n",[2968,6545,6546,6554,6558,6566,6573,6580,6584,6591,6598,6605,6612,6619,6626,6633,6640,6647,6651,6656,6661,6666,6671,6676,6681,6686,6691,6696,6701,6705,6710,6715,6720,6725,6730,6735,6740,6748,6753,6758,6762,6767,6772,6780,6784,6789,6794,6800,6818,6823,6829,6835,6841,6847,6861,6866,6871,6877,6883,6889,6895,6900,6908,6933,6939,6982,6988,6994,7000,7006,7026,7048,7053,7059,7071,7077,7082,7087,7093,7099,7104,7111,7120,7135,7147,7153,7158,7163,7169,7175,7180,7185,7190,7196,7201,7207,7213,7219,7234,7239,7269,7285,7290,7313,7335,7341,7347,7375,7394,7415,7449,7463,7469,7475,7480,7485,7490,7495,7501,7506,7519,7530,7535,7560,7583,7594,7613,7627,7668,7673,7678,7683,7688,7694,7699,7705,7710,7722,7727,7751,7772,7789,7802,7807,7812,7817,7822,7828,7834,7839,7852,7862,7867,7892,7907,7928,7945,7967,7980,7998,8003,8008,8013,8018,8024,8030,8035,8046,8051,8075,8089,8110,8127,8138,8143,8148],{"__ignoreMap":2998},[3002,6547,6548,6551],{"class":3004,"line":3005},[3002,6549,6550],{"class":4021},"package",[3002,6552,6553],{"class":3008}," com.example.audiobook.repository;\n",[3002,6555,6556],{"class":3004,"line":3016},[3002,6557,3153],{"emptyLinePlaceholder":3152},[3002,6559,6560,6563],{"class":3004,"line":3029},[3002,6561,6562],{"class":4021},"import",[3002,6564,6565],{"class":3008}," com.example.audiobook.db.ConnectionManager;\n",[3002,6567,6568,6570],{"class":3004,"line":3036},[3002,6569,6562],{"class":4021},[3002,6571,6572],{"class":3008}," org.junit.jupiter.api.AfterEach;\n",[3002,6574,6575,6577],{"class":3004,"line":3067},[3002,6576,6562],{"class":4021},[3002,6578,6579],{"class":3008}," org.junit.jupiter.api.BeforeEach;\n",[3002,6581,6582],{"class":3004,"line":3091},[3002,6583,3153],{"emptyLinePlaceholder":3152},[3002,6585,6586,6588],{"class":3004,"line":3121},[3002,6587,6562],{"class":4021},[3002,6589,6590],{"class":3008}," java.io.BufferedReader;\n",[3002,6592,6593,6595],{"class":3004,"line":3149},[3002,6594,6562],{"class":4021},[3002,6596,6597],{"class":3008}," java.io.IOException;\n",[3002,6599,6600,6602],{"class":3004,"line":3156},[3002,6601,6562],{"class":4021},[3002,6603,6604],{"class":3008}," java.io.InputStreamReader;\n",[3002,6606,6607,6609],{"class":3004,"line":3180},[3002,6608,6562],{"class":4021},[3002,6610,6611],{"class":3008}," java.nio.charset.StandardCharsets;\n",[3002,6613,6614,6616],{"class":3004,"line":3202},[3002,6615,6562],{"class":4021},[3002,6617,6618],{"class":3008}," java.sql.Connection;\n",[3002,6620,6621,6623],{"class":3004,"line":3207},[3002,6622,6562],{"class":4021},[3002,6624,6625],{"class":3008}," java.sql.SQLException;\n",[3002,6627,6628,6630],{"class":3004,"line":3228},[3002,6629,6562],{"class":4021},[3002,6631,6632],{"class":3008}," java.sql.Statement;\n",[3002,6634,6635,6637],{"class":3004,"line":3257},[3002,6636,6562],{"class":4021},[3002,6638,6639],{"class":3008}," java.util.UUID;\n",[3002,6641,6642,6644],{"class":3004,"line":3262},[3002,6643,6562],{"class":4021},[3002,6645,6646],{"class":3008}," java.util.stream.Collectors;\n",[3002,6648,6649],{"class":3004,"line":3268},[3002,6650,3153],{"emptyLinePlaceholder":3152},[3002,6652,6653],{"class":3004,"line":3282},[3002,6654,6655],{"class":3032},"/**\n",[3002,6657,6658],{"class":3004,"line":3287},[3002,6659,6660],{"class":3032}," * Базовий клас для інтеграційних тестів JDBC-репозиторіїв.\n",[3002,6662,6663],{"class":3004,"line":3293},[3002,6664,6665],{"class":3032}," * \u003Cp>\n",[3002,6667,6668],{"class":3004,"line":3307},[3002,6669,6670],{"class":3032}," * Надає спільну логіку:\n",[3002,6672,6673],{"class":3004,"line":4214},[3002,6674,6675],{"class":3032}," * \u003Cul>\n",[3002,6677,6678],{"class":3004,"line":4409},[3002,6679,6680],{"class":3032}," *   \u003Cli>Створення нової in-memory H2 БД перед кожним тестом\u003C/li>\n",[3002,6682,6683],{"class":3004,"line":4415},[3002,6684,6685],{"class":3032}," *   \u003Cli>Виконання DDL-скрипта (створення таблиць)\u003C/li>\n",[3002,6687,6688],{"class":3004,"line":4420},[3002,6689,6690],{"class":3032}," *   \u003Cli>Закриття з'єднання після тесту\u003C/li>\n",[3002,6692,6693],{"class":3004,"line":4425},[3002,6694,6695],{"class":3032}," *   \u003Cli>Утилітні методи для роботи з БД\u003C/li>\n",[3002,6697,6698],{"class":3004,"line":4432},[3002,6699,6700],{"class":3032}," * \u003C/ul>\n",[3002,6702,6703],{"class":3004,"line":4441},[3002,6704,6665],{"class":3032},[3002,6706,6707],{"class":3004,"line":4464},[3002,6708,6709],{"class":3032}," * \u003Cb>Використання:\u003C/b>\n",[3002,6711,6712],{"class":3004,"line":4479},[3002,6713,6714],{"class":3032}," * \u003Cpre>{@code\n",[3002,6716,6717],{"class":3004,"line":4954},[3002,6718,6719],{"class":3032}," * class JdbcAuthorRepositoryTest extends AbstractRepositoryTest {\n",[3002,6721,6722],{"class":3004,"line":4972},[3002,6723,6724],{"class":3032}," *     private AuthorRepository repository;\n",[3002,6726,6727],{"class":3004,"line":4990},[3002,6728,6729],{"class":3032}," *\n",[3002,6731,6732],{"class":3004,"line":5007},[3002,6733,6734],{"class":3032}," *     @BeforeEach\n",[3002,6736,6737],{"class":3004,"line":5016},[3002,6738,6739],{"class":3032}," *     void setUp() {\n",[3002,6741,6742,6745],{"class":3004,"line":5487},[3002,6743,6744],{"class":3032}," *         super.setUp();",[3002,6746,6747],{"class":3032}," // викликати базову ініціалізацію\n",[3002,6749,6750],{"class":3004,"line":5492},[3002,6751,6752],{"class":3032}," *         repository = new JdbcAuthorRepository(connectionManager);\n",[3002,6754,6755],{"class":3004,"line":5504},[3002,6756,6757],{"class":3032}," *     }\n",[3002,6759,6760],{"class":3004,"line":5514},[3002,6761,6729],{"class":3032},[3002,6763,6764],{"class":3004,"line":5552},[3002,6765,6766],{"class":3032}," *     @Test\n",[3002,6768,6769],{"class":3004,"line":5571},[3002,6770,6771],{"class":3032}," *     void save_shouldInsertAuthor() {\n",[3002,6773,6774,6777],{"class":3004,"line":5586},[3002,6775,6776],{"class":3032}," *",[3002,6778,6779],{"class":3032},"         // тест...\n",[3002,6781,6782],{"class":3004,"line":5600},[3002,6783,6757],{"class":3032},[3002,6785,6786],{"class":3004,"line":5605},[3002,6787,6788],{"class":3032}," * }\n",[3002,6790,6791],{"class":3004,"line":5610},[3002,6792,6793],{"class":3032}," * }\u003C/pre>\n",[3002,6795,6797],{"class":3004,"line":6796},45,[3002,6798,6799],{"class":3032}," */\n",[3002,6801,6803,6806,6809,6812,6815],{"class":3004,"line":6802},46,[3002,6804,6805],{"class":4021},"public",[3002,6807,6808],{"class":4021}," abstract",[3002,6810,6811],{"class":4021}," class",[3002,6813,6814],{"class":3012}," AbstractRepositoryTest",[3002,6816,6817],{"class":3008}," {\n",[3002,6819,6821],{"class":3004,"line":6820},47,[3002,6822,3153],{"emptyLinePlaceholder":3152},[3002,6824,6826],{"class":3004,"line":6825},48,[3002,6827,6828],{"class":3032},"    /**\n",[3002,6830,6832],{"class":3004,"line":6831},49,[3002,6833,6834],{"class":3032},"     * ConnectionManager для тестової БД.\n",[3002,6836,6838],{"class":3004,"line":6837},50,[3002,6839,6840],{"class":3032},"     * Доступний підкласам для створення репозиторіїв.\n",[3002,6842,6844],{"class":3004,"line":6843},51,[3002,6845,6846],{"class":3032},"     */\n",[3002,6848,6850,6853,6856,6859],{"class":3004,"line":6849},52,[3002,6851,6852],{"class":4021},"    protected",[3002,6854,6855],{"class":3012}," ConnectionManager",[3002,6857,6858],{"class":3042}," connectionManager",[3002,6860,3889],{"class":3008},[3002,6862,6864],{"class":3004,"line":6863},53,[3002,6865,3153],{"emptyLinePlaceholder":3152},[3002,6867,6869],{"class":3004,"line":6868},54,[3002,6870,6828],{"class":3032},[3002,6872,6874],{"class":3004,"line":6873},55,[3002,6875,6876],{"class":3032},"     * Ініціалізує нову in-memory H2 БД перед кожним тестом.\n",[3002,6878,6880],{"class":3004,"line":6879},56,[3002,6881,6882],{"class":3032},"     * \u003Cp>\n",[3002,6884,6886],{"class":3004,"line":6885},57,[3002,6887,6888],{"class":3032},"     * Кожен тест отримує унікальну порожню БД — це гарантує ізоляцію.\n",[3002,6890,6892],{"class":3004,"line":6891},58,[3002,6893,6894],{"class":3032},"     * DDL-скрипт виконується автоматично.\n",[3002,6896,6898],{"class":3004,"line":6897},59,[3002,6899,6846],{"class":3032},[3002,6901,6903,6906],{"class":3004,"line":6902},60,[3002,6904,6905],{"class":3008},"    @",[3002,6907,4250],{"class":3012},[3002,6909,6911,6914,6917,6920,6923,6926,6928,6931],{"class":3004,"line":6910},61,[3002,6912,6913],{"class":3012},"    void",[3002,6915,6916],{"class":3022}," setUp",[3002,6918,6919],{"class":3008},"() ",[3002,6921,6922],{"class":4021},"throws",[3002,6924,6925],{"class":3012}," SQLException",[3002,6927,2975],{"class":3008},[3002,6929,6930],{"class":3012},"IOException",[3002,6932,6817],{"class":3008},[3002,6934,6936],{"class":3004,"line":6935},62,[3002,6937,6938],{"class":3032},"        // Унікальна назва БД для кожного тесту\n",[3002,6940,6942,6945,6947,6949,6951,6953,6955,6957,6959,6962,6965,6967,6970,6972,6975,6977,6980],{"class":3004,"line":6941},63,[3002,6943,6944],{"class":3012},"        String",[3002,6946,4272],{"class":3042},[3002,6948,3046],{"class":3008},[3002,6950,4277],{"class":3246},[3002,6952,3872],{"class":3008},[3002,6954,3875],{"class":3042},[3002,6956,3058],{"class":3008},[3002,6958,3880],{"class":3022},[3002,6960,6961],{"class":3008},"().",[3002,6963,6964],{"class":3022},"toString",[3002,6966,6961],{"class":3008},[3002,6968,6969],{"class":3022},"replace",[3002,6971,3052],{"class":3008},[3002,6973,6974],{"class":3246},"\"-\"",[3002,6976,2975],{"class":3008},[3002,6978,6979],{"class":3246},"\"\"",[3002,6981,3064],{"class":3008},[3002,6983,6985],{"class":3004,"line":6984},64,[3002,6986,6987],{"class":3008},"        \n",[3002,6989,6991],{"class":3004,"line":6990},65,[3002,6992,6993],{"class":3032},"        // Створити ConnectionManager для in-memory H2\n",[3002,6995,6997],{"class":3004,"line":6996},66,[3002,6998,6999],{"class":3032},"        // DB_CLOSE_DELAY=-1 → БД не закривається при закритті останнього Connection\n",[3002,7001,7003],{"class":3004,"line":7002},67,[3002,7004,7005],{"class":3032},"        // (потрібно для коректної роботи з пулом з'єднань)\n",[3002,7007,7009,7011,7013,7015,7018,7021,7024],{"class":3004,"line":7008},68,[3002,7010,6944],{"class":3012},[3002,7012,3864],{"class":3042},[3002,7014,3046],{"class":3008},[3002,7016,7017],{"class":3246},"\"jdbc:h2:mem:\"",[3002,7019,7020],{"class":3008}," + dbName + ",[3002,7022,7023],{"class":3246},"\";DB_CLOSE_DELAY=-1;MODE=PostgreSQL\"",[3002,7025,3889],{"class":3008},[3002,7027,7029,7032,7034,7036,7039,7042,7044,7046],{"class":3004,"line":7028},69,[3002,7030,7031],{"class":3008},"        connectionManager = ",[3002,7033,3219],{"class":3218},[3002,7035,6855],{"class":3022},[3002,7037,7038],{"class":3008},"(url, ",[3002,7040,7041],{"class":3246},"\"sa\"",[3002,7043,2975],{"class":3008},[3002,7045,6979],{"class":3246},[3002,7047,3064],{"class":3008},[3002,7049,7051],{"class":3004,"line":7050},70,[3002,7052,3153],{"emptyLinePlaceholder":3152},[3002,7054,7056],{"class":3004,"line":7055},71,[3002,7057,7058],{"class":3032},"        // Виконати DDL-скрипт (створити таблиці)\n",[3002,7060,7062,7065,7067,7069],{"class":3004,"line":7061},72,[3002,7063,7064],{"class":3022},"        executeDdlScript",[3002,7066,3052],{"class":3008},[3002,7068,3434],{"class":3246},[3002,7070,3064],{"class":3008},[3002,7072,7074],{"class":3004,"line":7073},73,[3002,7075,7076],{"class":3008},"    }\n",[3002,7078,7080],{"class":3004,"line":7079},74,[3002,7081,3153],{"emptyLinePlaceholder":3152},[3002,7083,7085],{"class":3004,"line":7084},75,[3002,7086,6828],{"class":3032},[3002,7088,7090],{"class":3004,"line":7089},76,[3002,7091,7092],{"class":3032},"     * Закриває ConnectionManager після кожного тесту.\n",[3002,7094,7096],{"class":3004,"line":7095},77,[3002,7097,7098],{"class":3032},"     * БД автоматично видаляється з пам'яті.\n",[3002,7100,7102],{"class":3004,"line":7101},78,[3002,7103,6846],{"class":3032},[3002,7105,7107,7109],{"class":3004,"line":7106},79,[3002,7108,6905],{"class":3008},[3002,7110,4335],{"class":3012},[3002,7112,7114,7116,7118],{"class":3004,"line":7113},80,[3002,7115,6913],{"class":3012},[3002,7117,4342],{"class":3022},[3002,7119,3026],{"class":3008},[3002,7121,7123,7126,7129,7132],{"class":3004,"line":7122},81,[3002,7124,7125],{"class":3218},"        if",[3002,7127,7128],{"class":3008}," (connectionManager != ",[3002,7130,7131],{"class":4021},"null",[3002,7133,7134],{"class":3008},") {\n",[3002,7136,7138,7141,7143,7145],{"class":3004,"line":7137},82,[3002,7139,7140],{"class":3042},"            connectionManager",[3002,7142,3058],{"class":3008},[3002,7144,4354],{"class":3022},[3002,7146,3304],{"class":3008},[3002,7148,7150],{"class":3004,"line":7149},83,[3002,7151,7152],{"class":3008},"        }\n",[3002,7154,7156],{"class":3004,"line":7155},84,[3002,7157,7076],{"class":3008},[3002,7159,7161],{"class":3004,"line":7160},85,[3002,7162,3153],{"emptyLinePlaceholder":3152},[3002,7164,7166],{"class":3004,"line":7165},86,[3002,7167,7168],{"class":3032},"    // ═══════════════════════════════════════════════════════════════════════\n",[3002,7170,7172],{"class":3004,"line":7171},87,[3002,7173,7174],{"class":3032},"    // Утилітні методи для роботи з БД\n",[3002,7176,7178],{"class":3004,"line":7177},88,[3002,7179,7168],{"class":3032},[3002,7181,7183],{"class":3004,"line":7182},89,[3002,7184,3153],{"emptyLinePlaceholder":3152},[3002,7186,7188],{"class":3004,"line":7187},90,[3002,7189,6828],{"class":3032},[3002,7191,7193],{"class":3004,"line":7192},91,[3002,7194,7195],{"class":3032},"     * Виконує SQL-скрипт з ресурсів.\n",[3002,7197,7199],{"class":3004,"line":7198},92,[3002,7200,6882],{"class":3032},[3002,7202,7204],{"class":3004,"line":7203},93,[3002,7205,7206],{"class":3032},"     * Скрипт має знаходитися у {@code src/test/resources/}.\n",[3002,7208,7210],{"class":3004,"line":7209},94,[3002,7211,7212],{"class":3032},"     * Підтримує багаторядкові команди, розділені {@code ;}.\n",[3002,7214,7216],{"class":3004,"line":7215},95,[3002,7217,7218],{"class":3032},"     *\n",[3002,7220,7222,7225,7228,7231],{"class":3004,"line":7221},96,[3002,7223,7224],{"class":3032},"     * ",[3002,7226,7227],{"class":4021},"@param",[3002,7229,7230],{"class":3042}," scriptPath",[3002,7232,7233],{"class":3032}," шлях до скрипта відносно resources (наприклад, \"ddl_h2.sql\")\n",[3002,7235,7237],{"class":3004,"line":7236},97,[3002,7238,6846],{"class":3032},[3002,7240,7242,7244,7246,7249,7251,7253,7255,7257,7259,7262,7264,7267],{"class":3004,"line":7241},98,[3002,7243,6852],{"class":4021},[3002,7245,4048],{"class":3012},[3002,7247,7248],{"class":3022}," executeDdlScript",[3002,7250,3052],{"class":3008},[3002,7252,3861],{"class":3012},[3002,7254,7230],{"class":3042},[3002,7256,5304],{"class":3008},[3002,7258,6922],{"class":4021},[3002,7260,7261],{"class":3012}," IOException",[3002,7263,2975],{"class":3008},[3002,7265,7266],{"class":3012},"SQLException",[3002,7268,6817],{"class":3008},[3002,7270,7272,7274,7277,7279,7282],{"class":3004,"line":7271},99,[3002,7273,6944],{"class":3012},[3002,7275,7276],{"class":3042}," sql",[3002,7278,3046],{"class":3008},[3002,7280,7281],{"class":3022},"loadResourceAsString",[3002,7283,7284],{"class":3008},"(scriptPath);\n",[3002,7286,7288],{"class":3004,"line":7287},100,[3002,7289,6987],{"class":3008},[3002,7291,7293,7296,7298,7300,7302,7304,7307,7309,7311],{"class":3004,"line":7292},101,[3002,7294,7295],{"class":3218},"        try",[3002,7297,5534],{"class":3008},[3002,7299,3055],{"class":3012},[3002,7301,3896],{"class":3042},[3002,7303,3046],{"class":3008},[3002,7305,7306],{"class":3042},"connectionManager",[3002,7308,3058],{"class":3008},[3002,7310,3192],{"class":3022},[3002,7312,3304],{"class":3008},[3002,7314,7316,7319,7322,7324,7327,7329,7332],{"class":3004,"line":7315},102,[3002,7317,7318],{"class":3012},"             Statement",[3002,7320,7321],{"class":3042}," stmt",[3002,7323,3046],{"class":3008},[3002,7325,7326],{"class":3042},"conn",[3002,7328,3058],{"class":3008},[3002,7330,7331],{"class":3022},"createStatement",[3002,7333,7334],{"class":3008},"()) {\n",[3002,7336,7338],{"class":3004,"line":7337},103,[3002,7339,7340],{"class":3008},"            \n",[3002,7342,7344],{"class":3004,"line":7343},104,[3002,7345,7346],{"class":3032},"            // Розділити скрипт на окремі команди (за ;)\n",[3002,7348,7350,7353,7356,7359,7361,7363,7365,7368,7370,7373],{"class":3004,"line":7349},105,[3002,7351,7352],{"class":3012},"            String",[3002,7354,7355],{"class":3008},"[] ",[3002,7357,7358],{"class":3042},"commands",[3002,7360,3046],{"class":3008},[3002,7362,5092],{"class":3042},[3002,7364,3058],{"class":3008},[3002,7366,7367],{"class":3022},"split",[3002,7369,3052],{"class":3008},[3002,7371,7372],{"class":3246},"\";\"",[3002,7374,3064],{"class":3008},[3002,7376,7378,7381,7383,7385,7388,7391],{"class":3004,"line":7377},106,[3002,7379,7380],{"class":3218},"            for",[3002,7382,5534],{"class":3008},[3002,7384,3861],{"class":3012},[3002,7386,7387],{"class":3042}," command",[3002,7389,7390],{"class":3218}," :",[3002,7392,7393],{"class":3008}," commands) {\n",[3002,7395,7397,7400,7403,7405,7408,7410,7413],{"class":3004,"line":7396},107,[3002,7398,7399],{"class":3012},"                String",[3002,7401,7402],{"class":3042}," trimmed",[3002,7404,3046],{"class":3008},[3002,7406,7407],{"class":3042},"command",[3002,7409,3058],{"class":3008},[3002,7411,7412],{"class":3022},"trim",[3002,7414,3304],{"class":3008},[3002,7416,7418,7421,7424,7427,7429,7431,7434,7436,7438,7441,7443,7446],{"class":3004,"line":7417},108,[3002,7419,7420],{"class":3218},"                if",[3002,7422,7423],{"class":3008}," (!",[3002,7425,7426],{"class":3042},"trimmed",[3002,7428,3058],{"class":3008},[3002,7430,4471],{"class":3022},[3002,7432,7433],{"class":3008},"() && !",[3002,7435,7426],{"class":3042},[3002,7437,3058],{"class":3008},[3002,7439,7440],{"class":3022},"startsWith",[3002,7442,3052],{"class":3008},[3002,7444,7445],{"class":3246},"\"--\"",[3002,7447,7448],{"class":3008},")) {\n",[3002,7450,7452,7455,7457,7460],{"class":3004,"line":7451},109,[3002,7453,7454],{"class":3042},"                    stmt",[3002,7456,3058],{"class":3008},[3002,7458,7459],{"class":3022},"execute",[3002,7461,7462],{"class":3008},"(trimmed);\n",[3002,7464,7466],{"class":3004,"line":7465},110,[3002,7467,7468],{"class":3008},"                }\n",[3002,7470,7472],{"class":3004,"line":7471},111,[3002,7473,7474],{"class":3008},"            }\n",[3002,7476,7478],{"class":3004,"line":7477},112,[3002,7479,7152],{"class":3008},[3002,7481,7483],{"class":3004,"line":7482},113,[3002,7484,7076],{"class":3008},[3002,7486,7488],{"class":3004,"line":7487},114,[3002,7489,3153],{"emptyLinePlaceholder":3152},[3002,7491,7493],{"class":3004,"line":7492},115,[3002,7494,6828],{"class":3032},[3002,7496,7498],{"class":3004,"line":7497},116,[3002,7499,7500],{"class":3032},"     * Завантажує файл з ресурсів як рядок.\n",[3002,7502,7504],{"class":3004,"line":7503},117,[3002,7505,7218],{"class":3032},[3002,7507,7509,7511,7513,7516],{"class":3004,"line":7508},118,[3002,7510,7224],{"class":3032},[3002,7512,7227],{"class":4021},[3002,7514,7515],{"class":3042}," resourcePath",[3002,7517,7518],{"class":3032}," шлях до ресурсу (наприклад, \"ddl_h2.sql\")\n",[3002,7520,7522,7524,7527],{"class":3004,"line":7521},119,[3002,7523,7224],{"class":3032},[3002,7525,7526],{"class":4021},"@return",[3002,7528,7529],{"class":3032}," вміст файлу як рядок\n",[3002,7531,7533],{"class":3004,"line":7532},120,[3002,7534,6846],{"class":3032},[3002,7536,7538,7540,7543,7546,7548,7550,7552,7554,7556,7558],{"class":3004,"line":7537},121,[3002,7539,6852],{"class":4021},[3002,7541,7542],{"class":3012}," String",[3002,7544,7545],{"class":3022}," loadResourceAsString",[3002,7547,3052],{"class":3008},[3002,7549,3861],{"class":3012},[3002,7551,7515],{"class":3042},[3002,7553,5304],{"class":3008},[3002,7555,6922],{"class":4021},[3002,7557,7261],{"class":3012},[3002,7559,6817],{"class":3008},[3002,7561,7563,7565,7567,7570,7573,7575,7577,7580],{"class":3004,"line":7562},122,[3002,7564,7295],{"class":3218},[3002,7566,5534],{"class":3008},[3002,7568,7569],{"class":3012},"BufferedReader",[3002,7571,7572],{"class":3042}," reader",[3002,7574,3046],{"class":3008},[3002,7576,3219],{"class":3218},[3002,7578,7579],{"class":3022}," BufferedReader",[3002,7581,7582],{"class":3008},"(\n",[3002,7584,7586,7589,7592],{"class":3004,"line":7585},123,[3002,7587,7588],{"class":3218},"                new",[3002,7590,7591],{"class":3022}," InputStreamReader",[3002,7593,7582],{"class":3008},[3002,7595,7597,7600,7602,7605,7607,7610],{"class":3004,"line":7596},124,[3002,7598,7599],{"class":3022},"                    getClass",[3002,7601,6961],{"class":3008},[3002,7603,7604],{"class":3022},"getClassLoader",[3002,7606,6961],{"class":3008},[3002,7608,7609],{"class":3022},"getResourceAsStream",[3002,7611,7612],{"class":3008},"(resourcePath),\n",[3002,7614,7616,7619,7621,7624],{"class":3004,"line":7615},125,[3002,7617,7618],{"class":3042},"                    StandardCharsets",[3002,7620,3058],{"class":3008},[3002,7622,7623],{"class":3042},"UTF_8",[3002,7625,7626],{"class":3008},"))) {\n",[3002,7628,7630,7633,7635,7637,7640,7642,7645,7647,7650,7652,7655,7657,7660,7664,7666],{"class":3004,"line":7629},126,[3002,7631,7632],{"class":3218},"            return",[3002,7634,7572],{"class":3042},[3002,7636,3058],{"class":3008},[3002,7638,7639],{"class":3022},"lines",[3002,7641,6961],{"class":3008},[3002,7643,7644],{"class":3022},"collect",[3002,7646,3052],{"class":3008},[3002,7648,7649],{"class":3042},"Collectors",[3002,7651,3058],{"class":3008},[3002,7653,7654],{"class":3022},"joining",[3002,7656,3052],{"class":3008},[3002,7658,7659],{"class":3246},"\"",[3002,7661,7663],{"class":7662},"sjcCO","\\n",[3002,7665,7659],{"class":3246},[3002,7667,4129],{"class":3008},[3002,7669,7671],{"class":3004,"line":7670},127,[3002,7672,7152],{"class":3008},[3002,7674,7676],{"class":3004,"line":7675},128,[3002,7677,7076],{"class":3008},[3002,7679,7681],{"class":3004,"line":7680},129,[3002,7682,3153],{"emptyLinePlaceholder":3152},[3002,7684,7686],{"class":3004,"line":7685},130,[3002,7687,6828],{"class":3032},[3002,7689,7691],{"class":3004,"line":7690},131,[3002,7692,7693],{"class":3032},"     * Виконує довільний SQL-запит (для підготовки тестових даних).\n",[3002,7695,7697],{"class":3004,"line":7696},132,[3002,7698,6882],{"class":3032},[3002,7700,7702],{"class":3004,"line":7701},133,[3002,7703,7704],{"class":3032},"     * Використовується у Arrange-фазі для створення складних сценаріїв.\n",[3002,7706,7708],{"class":3004,"line":7707},134,[3002,7709,7218],{"class":3032},[3002,7711,7713,7715,7717,7719],{"class":3004,"line":7712},135,[3002,7714,7224],{"class":3032},[3002,7716,7227],{"class":4021},[3002,7718,7276],{"class":3042},[3002,7720,7721],{"class":3032}," SQL-команда (INSERT, UPDATE, DELETE)\n",[3002,7723,7725],{"class":3004,"line":7724},136,[3002,7726,6846],{"class":3032},[3002,7728,7730,7732,7734,7737,7739,7741,7743,7745,7747,7749],{"class":3004,"line":7729},137,[3002,7731,6852],{"class":4021},[3002,7733,4048],{"class":3012},[3002,7735,7736],{"class":3022}," executeSql",[3002,7738,3052],{"class":3008},[3002,7740,3861],{"class":3012},[3002,7742,7276],{"class":3042},[3002,7744,5304],{"class":3008},[3002,7746,6922],{"class":4021},[3002,7748,6925],{"class":3012},[3002,7750,6817],{"class":3008},[3002,7752,7754,7756,7758,7760,7762,7764,7766,7768,7770],{"class":3004,"line":7753},138,[3002,7755,7295],{"class":3218},[3002,7757,5534],{"class":3008},[3002,7759,3055],{"class":3012},[3002,7761,3896],{"class":3042},[3002,7763,3046],{"class":3008},[3002,7765,7306],{"class":3042},[3002,7767,3058],{"class":3008},[3002,7769,3192],{"class":3022},[3002,7771,3304],{"class":3008},[3002,7773,7775,7777,7779,7781,7783,7785,7787],{"class":3004,"line":7774},139,[3002,7776,7318],{"class":3012},[3002,7778,7321],{"class":3042},[3002,7780,3046],{"class":3008},[3002,7782,7326],{"class":3042},[3002,7784,3058],{"class":3008},[3002,7786,7331],{"class":3022},[3002,7788,7334],{"class":3008},[3002,7790,7792,7795,7797,7799],{"class":3004,"line":7791},140,[3002,7793,7794],{"class":3042},"            stmt",[3002,7796,3058],{"class":3008},[3002,7798,7459],{"class":3022},[3002,7800,7801],{"class":3008},"(sql);\n",[3002,7803,7805],{"class":3004,"line":7804},141,[3002,7806,7152],{"class":3008},[3002,7808,7810],{"class":3004,"line":7809},142,[3002,7811,7076],{"class":3008},[3002,7813,7815],{"class":3004,"line":7814},143,[3002,7816,3153],{"emptyLinePlaceholder":3152},[3002,7818,7820],{"class":3004,"line":7819},144,[3002,7821,6828],{"class":3032},[3002,7823,7825],{"class":3004,"line":7824},145,[3002,7826,7827],{"class":3032},"     * Підраховує кількість рядків у таблиці.\n",[3002,7829,7831],{"class":3004,"line":7830},146,[3002,7832,7833],{"class":3032},"     * Корисно для перевірки у Assert-фазі.\n",[3002,7835,7837],{"class":3004,"line":7836},147,[3002,7838,7218],{"class":3032},[3002,7840,7842,7844,7846,7849],{"class":3004,"line":7841},148,[3002,7843,7224],{"class":3032},[3002,7845,7227],{"class":4021},[3002,7847,7848],{"class":3042}," tableName",[3002,7850,7851],{"class":3032}," назва таблиці\n",[3002,7853,7855,7857,7859],{"class":3004,"line":7854},149,[3002,7856,7224],{"class":3032},[3002,7858,7526],{"class":4021},[3002,7860,7861],{"class":3032}," кількість рядків\n",[3002,7863,7865],{"class":3004,"line":7864},150,[3002,7866,6846],{"class":3032},[3002,7868,7870,7872,7875,7878,7880,7882,7884,7886,7888,7890],{"class":3004,"line":7869},151,[3002,7871,6852],{"class":4021},[3002,7873,7874],{"class":3012}," long",[3002,7876,7877],{"class":3022}," countRowsInTable",[3002,7879,3052],{"class":3008},[3002,7881,3861],{"class":3012},[3002,7883,7848],{"class":3042},[3002,7885,5304],{"class":3008},[3002,7887,6922],{"class":4021},[3002,7889,6925],{"class":3012},[3002,7891,6817],{"class":3008},[3002,7893,7895,7897,7899,7901,7904],{"class":3004,"line":7894},152,[3002,7896,6944],{"class":3012},[3002,7898,7276],{"class":3042},[3002,7900,3046],{"class":3008},[3002,7902,7903],{"class":3246},"\"SELECT COUNT(*) FROM \"",[3002,7905,7906],{"class":3008}," + tableName;\n",[3002,7908,7910,7912,7914,7916,7918,7920,7922,7924,7926],{"class":3004,"line":7909},153,[3002,7911,7295],{"class":3218},[3002,7913,5534],{"class":3008},[3002,7915,3055],{"class":3012},[3002,7917,3896],{"class":3042},[3002,7919,3046],{"class":3008},[3002,7921,7306],{"class":3042},[3002,7923,3058],{"class":3008},[3002,7925,3192],{"class":3022},[3002,7927,3304],{"class":3008},[3002,7929,7931,7933,7935,7937,7939,7941,7943],{"class":3004,"line":7930},154,[3002,7932,7318],{"class":3012},[3002,7934,7321],{"class":3042},[3002,7936,3046],{"class":3008},[3002,7938,7326],{"class":3042},[3002,7940,3058],{"class":3008},[3002,7942,7331],{"class":3022},[3002,7944,3304],{"class":3008},[3002,7946,7948,7951,7954,7956,7959,7961,7964],{"class":3004,"line":7947},155,[3002,7949,7950],{"class":4021},"             var",[3002,7952,7953],{"class":3042}," rs",[3002,7955,3046],{"class":3008},[3002,7957,7958],{"class":3042},"stmt",[3002,7960,3058],{"class":3008},[3002,7962,7963],{"class":3022},"executeQuery",[3002,7965,7966],{"class":3008},"(sql)) {\n",[3002,7968,7970,7973,7975,7978],{"class":3004,"line":7969},156,[3002,7971,7972],{"class":3042},"            rs",[3002,7974,3058],{"class":3008},[3002,7976,7977],{"class":3022},"next",[3002,7979,3304],{"class":3008},[3002,7981,7983,7985,7987,7989,7992,7994,7996],{"class":3004,"line":7982},157,[3002,7984,7632],{"class":3218},[3002,7986,7953],{"class":3042},[3002,7988,3058],{"class":3008},[3002,7990,7991],{"class":3022},"getLong",[3002,7993,3052],{"class":3008},[3002,7995,3144],{"class":3143},[3002,7997,3064],{"class":3008},[3002,7999,8001],{"class":3004,"line":8000},158,[3002,8002,7152],{"class":3008},[3002,8004,8006],{"class":3004,"line":8005},159,[3002,8007,7076],{"class":3008},[3002,8009,8011],{"class":3004,"line":8010},160,[3002,8012,3153],{"emptyLinePlaceholder":3152},[3002,8014,8016],{"class":3004,"line":8015},161,[3002,8017,6828],{"class":3032},[3002,8019,8021],{"class":3004,"line":8020},162,[3002,8022,8023],{"class":3032},"     * Очищає всі дані з таблиці (але залишає структуру).\n",[3002,8025,8027],{"class":3004,"line":8026},163,[3002,8028,8029],{"class":3032},"     * Використовується рідко — зазвичай кожен тест отримує нову БД.\n",[3002,8031,8033],{"class":3004,"line":8032},164,[3002,8034,7218],{"class":3032},[3002,8036,8038,8040,8042,8044],{"class":3004,"line":8037},165,[3002,8039,7224],{"class":3032},[3002,8041,7227],{"class":4021},[3002,8043,7848],{"class":3042},[3002,8045,7851],{"class":3032},[3002,8047,8049],{"class":3004,"line":8048},166,[3002,8050,6846],{"class":3032},[3002,8052,8054,8056,8058,8061,8063,8065,8067,8069,8071,8073],{"class":3004,"line":8053},167,[3002,8055,6852],{"class":4021},[3002,8057,4048],{"class":3012},[3002,8059,8060],{"class":3022}," truncateTable",[3002,8062,3052],{"class":3008},[3002,8064,3861],{"class":3012},[3002,8066,7848],{"class":3042},[3002,8068,5304],{"class":3008},[3002,8070,6922],{"class":4021},[3002,8072,6925],{"class":3012},[3002,8074,6817],{"class":3008},[3002,8076,8078,8080,8082,8084,8087],{"class":3004,"line":8077},168,[3002,8079,6944],{"class":3012},[3002,8081,7276],{"class":3042},[3002,8083,3046],{"class":3008},[3002,8085,8086],{"class":3246},"\"TRUNCATE TABLE \"",[3002,8088,7906],{"class":3008},[3002,8090,8092,8094,8096,8098,8100,8102,8104,8106,8108],{"class":3004,"line":8091},169,[3002,8093,7295],{"class":3218},[3002,8095,5534],{"class":3008},[3002,8097,3055],{"class":3012},[3002,8099,3896],{"class":3042},[3002,8101,3046],{"class":3008},[3002,8103,7306],{"class":3042},[3002,8105,3058],{"class":3008},[3002,8107,3192],{"class":3022},[3002,8109,3304],{"class":3008},[3002,8111,8113,8115,8117,8119,8121,8123,8125],{"class":3004,"line":8112},170,[3002,8114,7318],{"class":3012},[3002,8116,7321],{"class":3042},[3002,8118,3046],{"class":3008},[3002,8120,7326],{"class":3042},[3002,8122,3058],{"class":3008},[3002,8124,7331],{"class":3022},[3002,8126,7334],{"class":3008},[3002,8128,8130,8132,8134,8136],{"class":3004,"line":8129},171,[3002,8131,7794],{"class":3042},[3002,8133,3058],{"class":3008},[3002,8135,7459],{"class":3022},[3002,8137,7801],{"class":3008},[3002,8139,8141],{"class":3004,"line":8140},172,[3002,8142,7152],{"class":3008},[3002,8144,8146],{"class":3004,"line":8145},173,[3002,8147,7076],{"class":3008},[3002,8149,8151],{"class":3004,"line":8150},174,[3002,8152,3310],{"class":3008},[2964,8154,8155],{},[2989,8156,8157],{},"Ключові рішення:",[3332,8159,8160,8169,8177,8190],{},[3335,8161,8162,5534,8165,8168],{},[2989,8163,8164],{},"Рядок 68",[2968,8166,8167],{},"MODE=PostgreSQL","): H2 може емулювати діалект PostgreSQL — це зменшує кількість відмінностей у SQL.",[3335,8170,8171,5534,8173,8176],{},[2989,8172,8164],{},[2968,8174,8175],{},"DB_CLOSE_DELAY=-1","): Без цього параметра H2 закриває БД одразу після закриття останнього Connection, що може призвести до помилок при використанні пулу з'єднань.",[3335,8178,8179,5534,8182,8185,8186,8189],{},[2989,8180,8181],{},"Рядки 107–117",[2968,8183,8184],{},"executeDdlScript","): Розділяє SQL-скрипт на окремі команди за ",[2968,8187,8188],{},";",". Це дозволяє виконувати багаторядкові DDL-скрипти.",[3335,8191,8192,5534,8195,8198],{},[2989,8193,8194],{},"Рядки 154–162",[2968,8196,8197],{},"countRowsInTable","): Утилітний метод для Assert-фази — перевірити, що у таблиці правильна кількість рядків.",[3353,8200,8201,8215,8226],{},[2964,8202,8203,8206,8207,8210,8211,8214],{},[2989,8204,8205],{},"SQL Injection у тестах:"," Методи ",[2968,8208,8209],{},"executeSql()"," та ",[2968,8212,8213],{},"countRowsInTable()"," використовують конкатенацію рядків, що небезпечно у production. Але у тестах це прийнятно, оскільки:",[3821,8216,8217,8220,8223],{},[3335,8218,8219],{},"Тести не виконуються у production",[3335,8221,8222],{},"Назви таблиць контролюються розробником",[3335,8224,8225],{},"Це спрощує код тестів",[2964,8227,8228],{},[2989,8229,8230],{},"Ніколи не копіюйте цей підхід у production-код!",[3660,8232],{},[2959,8234,8236],{"id":8235},"тестування-authorrepository-crud-операції","Тестування AuthorRepository: CRUD-операції",[2964,8238,8239,8240,8242],{},"Тепер створимо повний набір тестів для ",[2968,8241,2970],{},". Кожен тест покриває один сценарій.",[2993,8244,8246],{"className":2995,"code":8245,"language":2997,"meta":4616,"style":2998},"package com.example.audiobook.repository;\n\nimport com.example.audiobook.domain.Author;\nimport com.example.audiobook.repository.jdbc.JdbcAuthorRepository;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.UUID;\n\nimport static org.assertj.core.api.Assertions.*;\n\n/**\n * Інтеграційні тести для {@link JdbcAuthorRepository}.\n * \u003Cp>\n * Кожен тест виконується на новій in-memory H2 БД — тести повністю ізольовані.\n * Використовується патерн AAA (Arrange-Act-Assert) та конвенція найменування\n * {@code methodName_shouldBehavior_whenCondition}.\n */\nclass JdbcAuthorRepositoryTest extends AbstractRepositoryTest {\n\n    private AuthorRepository repository;\n\n    @BeforeEach\n    void setUpRepository() {\n        // Базовий setUp() вже виконав ініціалізацію БД\n        repository = new JdbcAuthorRepository(connectionManager);\n    }\n\n    // ═══════════════════════════════════════════════════════════════════════\n    // save() — Тести вставки нових авторів\n    // ═══════════════════════════════════════════════════════════════════════\n\n    @Test\n    void save_shouldInsertNewAuthor_whenValidData() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Іван\", \"Франко\");\n        author.setBio(\"Український письменник, поет, публіцист\");\n        author.setImagePath(\"/images/franko.jpg\");\n\n        // ═══ Act ═══\n        repository.save(author);\n\n        // ═══ Assert ═══\n        // Перевірка 1: ID має бути згенерований\n        assertThat(author.getId()).isNotNull();\n\n        // Перевірка 2: Автор має бути у БД\n        Author loaded = repository.findById(author.getId()).orElseThrow();\n        assertThat(loaded.getFirstName()).isEqualTo(\"Іван\");\n        assertThat(loaded.getLastName()).isEqualTo(\"Франко\");\n        assertThat(loaded.getBio()).isEqualTo(\"Український письменник, поет, публіцист\");\n        assertThat(loaded.getImagePath()).isEqualTo(\"/images/franko.jpg\");\n\n        // Перевірка 3: У таблиці має бути рівно 1 рядок\n        assertThat(countRowsInTable(\"authors\")).isEqualTo(1);\n    }\n\n    @Test\n    void save_shouldHandleNullBio_whenBioNotProvided() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Леся\", \"Українка\");\n        // bio та imagePath залишаються null\n\n        // ═══ Act ═══\n        repository.save(author);\n\n        // ═══ Assert ═══\n        Author loaded = repository.findById(author.getId()).orElseThrow();\n        assertThat(loaded.getBio()).isNull();\n        assertThat(loaded.getImagePath()).isNull();\n    }\n\n    @Test\n    void save_shouldThrowException_whenDuplicateId() {\n        // ═══ Arrange ═══\n        UUID sharedId = UUID.randomUUID();\n        \n        Author author1 = new Author(\"Тарас\", \"Шевченко\");\n        author1.setId(sharedId);\n        \n        Author author2 = new Author(\"Іван\", \"Франко\");\n        author2.setId(sharedId); // той самий ID!\n\n        repository.save(author1); // перший збережено успішно\n\n        // ═══ Act & Assert ═══\n        // Спроба зберегти другого автора з тим самим ID має кинути виключення\n        assertThatThrownBy(() -> repository.save(author2))\n            .isInstanceOf(com.example.audiobook.db.DatabaseException.class)\n            .hasMessageContaining(\"PRIMARY KEY\"); // H2 повідомляє про порушення PK\n    }\n\n    // ═══════════════════════════════════════════════════════════════════════\n    // findById() — Тести пошуку за ID\n    // ═══════════════════════════════════════════════════════════════════════\n\n    @Test\n    void findById_shouldReturnAuthor_whenExists() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Михайло\", \"Коцюбинський\");\n        repository.save(author);\n        UUID authorId = author.getId();\n\n        // ═══ Act ═══\n        Optional\u003CAuthor> result = repository.findById(authorId);\n\n        // ═══ Assert ═══\n        assertThat(result).isPresent();\n        assertThat(result.get().getFirstName()).isEqualTo(\"Михайло\");\n        assertThat(result.get().getLastName()).isEqualTo(\"Коцюбинський\");\n    }\n\n    @Test\n    void findById_shouldReturnEmpty_whenNotExists() {\n        // ═══ Arrange ═══\n        UUID nonExistentId = UUID.randomUUID();\n\n        // ═══ Act ═══\n        Optional\u003CAuthor> result = repository.findById(nonExistentId);\n\n        // ═══ Assert ═══\n        assertThat(result).isEmpty();\n    }\n\n    // ═══════════════════════════════════════════════════════════════════════\n    // findAll() — Тести вибірки всіх авторів\n    // ═══════════════════════════════════════════════════════════════════════\n\n    @Test\n    void findAll_shouldReturnAllAuthors_whenMultipleExist() {\n        // ═══ Arrange ═══\n        Author author1 = new Author(\"Тарас\", \"Шевченко\");\n        Author author2 = new Author(\"Іван\", \"Франко\");\n        Author author3 = new Author(\"Леся\", \"Українка\");\n        \n        repository.save(author1);\n        repository.save(author2);\n        repository.save(author3);\n\n        // ═══ Act ═══\n        List\u003CAuthor> all = repository.findAll();\n\n        // ═══ Assert ═══\n        assertThat(all).hasSize(3);\n        assertThat(all)\n            .extracting(Author::getLastName)\n            .containsExactlyInAnyOrder(\"Шевченко\", \"Франко\", \"Українка\");\n    }\n\n    @Test\n    void findAll_shouldReturnEmptyList_whenTableEmpty() {\n        // ═══ Arrange ═══\n        // Таблиця порожня (нова БД для кожного тесту)\n\n        // ═══ Act ═══\n        List\u003CAuthor> all = repository.findAll();\n\n        // ═══ Assert ═══\n        assertThat(all).isEmpty();\n    }\n\n    // ═══════════════════════════════════════════════════════════════════════\n    // update() — Тести оновлення існуючих авторів\n    // ═══════════════════════════════════════════════════════════════════════\n\n    @Test\n    void update_shouldModifyAllFields_whenAuthorExists() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Іван\", \"Франко\");\n        author.setBio(\"Стара біографія\");\n        repository.save(author);\n\n        // Змінити всі поля\n        author.setFirstName(\"Іван Якович\");\n        author.setLastName(\"Франко-Захарченко\");\n        author.setBio(\"Нова біографія\");\n        author.setImagePath(\"/images/new.jpg\");\n\n        // ═══ Act ═══\n        repository.update(author);\n\n        // ═══ Assert ═══\n        Author loaded = repository.findById(author.getId()).orElseThrow();\n        assertThat(loaded.getFirstName()).isEqualTo(\"Іван Якович\");\n        assertThat(loaded.getLastName()).isEqualTo(\"Франко-Захарченко\");\n        assertThat(loaded.getBio()).isEqualTo(\"Нова біографія\");\n        assertThat(loaded.getImagePath()).isEqualTo(\"/images/new.jpg\");\n    }\n\n    @Test\n    void update_shouldThrowException_whenAuthorNotExists() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Неіснуючий\", \"Автор\");\n        author.setId(UUID.randomUUID()); // ID, якого немає у БД\n\n        // ═══ Act & Assert ═══\n        assertThatThrownBy(() -> repository.update(author))\n            .isInstanceOf(com.example.audiobook.db.DatabaseException.class)\n            .hasMessageContaining(\"не знайдена\"); // повідомлення з AbstractJdbcRepository\n    }\n\n    // ═══════════════════════════════════════════════════════════════════════\n    // deleteById() — Тести видалення авторів\n    // ═══════════════════════════════════════════════════════════════════════\n\n    @Test\n    void deleteById_shouldRemoveAuthor_whenExists() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Панас\", \"Мирний\");\n        repository.save(author);\n        UUID authorId = author.getId();\n\n        // ═══ Act ═══\n        boolean deleted = repository.deleteById(authorId);\n\n        // ═══ Assert ═══\n        assertThat(deleted).isTrue();\n        assertThat(repository.findById(authorId)).isEmpty();\n        assertThat(countRowsInTable(\"authors\")).isEqualTo(0);\n    }\n\n    @Test\n    void deleteById_shouldReturnFalse_whenNotExists() {\n        // ═══ Arrange ═══\n        UUID nonExistentId = UUID.randomUUID();\n\n        // ═══ Act ═══\n        boolean deleted = repository.deleteById(nonExistentId);\n\n        // ═══ Assert ═══\n        assertThat(deleted).isFalse();\n    }\n\n    // ═══════════════════════════════════════════════════════════════════════\n    // findByLastName() — Тести пошуку за прізвищем\n    // ═══════════════════════════════════════════════════════════════════════\n\n    @Test\n    void findByLastName_shouldReturnMatchingAuthors_whenPartialMatch() {\n        // ═══ Arrange ═══\n        repository.save(new Author(\"Тарас\", \"Шевченко\"));\n        repository.save(new Author(\"Іван\", \"Франко\"));\n        repository.save(new Author(\"Леся\", \"Українка\"));\n        repository.save(new Author(\"Григорій\", \"Сковорода\"));\n\n        // ═══ Act ═══\n        List\u003CAuthor> result = repository.findByLastName(\"ко\"); // \"Франко\", \"Сковорода\"\n\n        // ═══ Assert ═══\n        assertThat(result).hasSize(2);\n        assertThat(result)\n            .extracting(Author::getLastName)\n            .containsExactlyInAnyOrder(\"Франко\", \"Сковорода\");\n    }\n\n    @Test\n    void findByLastName_shouldBeCaseInsensitive_whenSearching() {\n        // ═══ Arrange ═══\n        repository.save(new Author(\"Тарас\", \"Шевченко\"));\n\n        // ═══ Act ═══\n        List\u003CAuthor> result1 = repository.findByLastName(\"шевч\");\n        List\u003CAuthor> result2 = repository.findByLastName(\"ШЕВЧ\");\n        List\u003CAuthor> result3 = repository.findByLastName(\"ШеВч\");\n\n        // ═══ Assert ═══\n        assertThat(result1).hasSize(1);\n        assertThat(result2).hasSize(1);\n        assertThat(result3).hasSize(1);\n    }\n\n    @Test\n    void findByLastName_shouldReturnEmptyList_whenNoMatches() {\n        // ═══ Arrange ═══\n        repository.save(new Author(\"Тарас\", \"Шевченко\"));\n\n        // ═══ Act ═══\n        List\u003CAuthor> result = repository.findByLastName(\"Неіснуюче\");\n\n        // ═══ Assert ═══\n        assertThat(result).isEmpty();\n    }\n\n    // ═══════════════════════════════════════════════════════════════════════\n    // count() та existsById() — Допоміжні методи\n    // ═══════════════════════════════════════════════════════════════════════\n\n    @Test\n    void count_shouldReturnCorrectNumber_whenMultipleAuthorsExist() {\n        // ═══ Arrange ═══\n        repository.save(new Author(\"Автор\", \"1\"));\n        repository.save(new Author(\"Автор\", \"2\"));\n        repository.save(new Author(\"Автор\", \"3\"));\n\n        // ═══ Act ═══\n        long count = repository.count();\n\n        // ═══ Assert ═══\n        assertThat(count).isEqualTo(3);\n    }\n\n    @Test\n    void existsById_shouldReturnTrue_whenAuthorExists() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Іван\", \"Франко\");\n        repository.save(author);\n\n        // ═══ Act ═══\n        boolean exists = repository.existsById(author.getId());\n\n        // ═══ Assert ═══\n        assertThat(exists).isTrue();\n    }\n\n    @Test\n    void existsById_shouldReturnFalse_whenAuthorNotExists() {\n        // ═══ Arrange ═══\n        UUID nonExistentId = UUID.randomUUID();\n\n        // ═══ Act ═══\n        boolean exists = repository.existsById(nonExistentId);\n\n        // ═══ Assert ═══\n        assertThat(exists).isFalse();\n    }\n}\n",[2968,8247,8248,8254,8258,8265,8272,8278,8285,8289,8296,8303,8309,8313,8323,8327,8331,8336,8340,8345,8350,8355,8359,8373,8377,8390,8394,8400,8409,8414,8425,8429,8433,8437,8442,8446,8450,8456,8464,8469,8492,8508,8524,8528,8533,8544,8548,8553,8558,8577,8581,8586,8615,8637,8659,8681,8704,8708,8713,8737,8741,8745,8751,8760,8764,8788,8793,8797,8801,8811,8815,8819,8847,8866,8884,8888,8892,8898,8907,8911,8929,8933,8956,8969,8973,8996,9011,9015,9029,9033,9038,9043,9063,9102,9119,9123,9127,9131,9136,9140,9144,9150,9159,9163,9187,9197,9214,9218,9222,9247,9251,9255,9267,9294,9320,9324,9328,9334,9343,9347,9364,9368,9372,9395,9399,9403,9413,9417,9421,9425,9430,9434,9438,9444,9453,9457,9479,9501,9524,9528,9539,9550,9561,9565,9569,9592,9596,9600,9615,9622,9638,9659,9663,9667,9673,9682,9686,9691,9695,9699,9721,9725,9729,9739,9743,9747,9751,9756,9760,9764,9770,9779,9783,9805,9820,9830,9834,9840,9857,9874,9890,9906,9911,9916,9927,9932,9937,9966,9989,10012,10035,10058,10063,10068,10075,10085,10090,10115,10138,10143,10148,10166,10199,10216,10221,10226,10231,10237,10242,10247,10254,10264,10269,10294,10305,10322,10327,10332,10352,10357,10362,10375,10395,10418,10423,10428,10435,10445,10450,10467,10472,10477,10494,10499,10504,10516,10521,10526,10531,10537,10542,10547,10554,10564,10569,10594,10619,10644,10671,10676,10681,10713,10718,10723,10739,10747,10760,10777,10782,10787,10794,10804,10809,10834,10839,10844,10873,10902,10931,10936,10941,10957,10973,10989,10994,10999,11006,11016,11021,11046,11051,11056,11084,11089,11094,11105,11110,11115,11120,11126,11131,11136,11143,11153,11158,11184,11210,11236,11241,11246,11266,11271,11276,11292,11297,11302,11309,11319,11324,11347,11358,11363,11368,11396,11401,11406,11418,11423,11428,11435,11445,11450,11467,11472,11477,11494,11499,11504,11515,11520],{"__ignoreMap":2998},[3002,8249,8250,8252],{"class":3004,"line":3005},[3002,8251,6550],{"class":4021},[3002,8253,6553],{"class":3008},[3002,8255,8256],{"class":3004,"line":3016},[3002,8257,3153],{"emptyLinePlaceholder":3152},[3002,8259,8260,8262],{"class":3004,"line":3029},[3002,8261,6562],{"class":4021},[3002,8263,8264],{"class":3008}," com.example.audiobook.domain.Author;\n",[3002,8266,8267,8269],{"class":3004,"line":3036},[3002,8268,6562],{"class":4021},[3002,8270,8271],{"class":3008}," com.example.audiobook.repository.jdbc.JdbcAuthorRepository;\n",[3002,8273,8274,8276],{"class":3004,"line":3067},[3002,8275,6562],{"class":4021},[3002,8277,6579],{"class":3008},[3002,8279,8280,8282],{"class":3004,"line":3091},[3002,8281,6562],{"class":4021},[3002,8283,8284],{"class":3008}," org.junit.jupiter.api.Test;\n",[3002,8286,8287],{"class":3004,"line":3121},[3002,8288,3153],{"emptyLinePlaceholder":3152},[3002,8290,8291,8293],{"class":3004,"line":3149},[3002,8292,6562],{"class":4021},[3002,8294,8295],{"class":3008}," java.util.List;\n",[3002,8297,8298,8300],{"class":3004,"line":3156},[3002,8299,6562],{"class":4021},[3002,8301,8302],{"class":3008}," java.util.Optional;\n",[3002,8304,8305,8307],{"class":3004,"line":3180},[3002,8306,6562],{"class":4021},[3002,8308,6639],{"class":3008},[3002,8310,8311],{"class":3004,"line":3202},[3002,8312,3153],{"emptyLinePlaceholder":3152},[3002,8314,8315,8317,8320],{"class":3004,"line":3207},[3002,8316,6562],{"class":4021},[3002,8318,8319],{"class":4021}," static",[3002,8321,8322],{"class":3008}," org.assertj.core.api.Assertions.*;\n",[3002,8324,8325],{"class":3004,"line":3228},[3002,8326,3153],{"emptyLinePlaceholder":3152},[3002,8328,8329],{"class":3004,"line":3257},[3002,8330,6655],{"class":3032},[3002,8332,8333],{"class":3004,"line":3262},[3002,8334,8335],{"class":3032}," * Інтеграційні тести для {@link JdbcAuthorRepository}.\n",[3002,8337,8338],{"class":3004,"line":3268},[3002,8339,6665],{"class":3032},[3002,8341,8342],{"class":3004,"line":3282},[3002,8343,8344],{"class":3032}," * Кожен тест виконується на новій in-memory H2 БД — тести повністю ізольовані.\n",[3002,8346,8347],{"class":3004,"line":3287},[3002,8348,8349],{"class":3032}," * Використовується патерн AAA (Arrange-Act-Assert) та конвенція найменування\n",[3002,8351,8352],{"class":3004,"line":3293},[3002,8353,8354],{"class":3032}," * {@code methodName_shouldBehavior_whenCondition}.\n",[3002,8356,8357],{"class":3004,"line":3307},[3002,8358,6799],{"class":3032},[3002,8360,8361,8363,8366,8369,8371],{"class":3004,"line":4214},[3002,8362,3061],{"class":4021},[3002,8364,8365],{"class":3012}," JdbcAuthorRepositoryTest",[3002,8367,8368],{"class":4021}," extends",[3002,8370,6814],{"class":3012},[3002,8372,6817],{"class":3008},[3002,8374,8375],{"class":3004,"line":4409},[3002,8376,3153],{"emptyLinePlaceholder":3152},[3002,8378,8379,8382,8385,8388],{"class":3004,"line":4415},[3002,8380,8381],{"class":4021},"    private",[3002,8383,8384],{"class":3012}," AuthorRepository",[3002,8386,8387],{"class":3042}," repository",[3002,8389,3889],{"class":3008},[3002,8391,8392],{"class":3004,"line":4420},[3002,8393,3153],{"emptyLinePlaceholder":3152},[3002,8395,8396,8398],{"class":3004,"line":4425},[3002,8397,6905],{"class":3008},[3002,8399,4250],{"class":3012},[3002,8401,8402,8404,8407],{"class":3004,"line":4432},[3002,8403,6913],{"class":3012},[3002,8405,8406],{"class":3022}," setUpRepository",[3002,8408,3026],{"class":3008},[3002,8410,8411],{"class":3004,"line":4441},[3002,8412,8413],{"class":3032},"        // Базовий setUp() вже виконав ініціалізацію БД\n",[3002,8415,8416,8419,8421,8423],{"class":3004,"line":4464},[3002,8417,8418],{"class":3008},"        repository = ",[3002,8420,3219],{"class":3218},[3002,8422,3222],{"class":3022},[3002,8424,5811],{"class":3008},[3002,8426,8427],{"class":3004,"line":4479},[3002,8428,7076],{"class":3008},[3002,8430,8431],{"class":3004,"line":4954},[3002,8432,3153],{"emptyLinePlaceholder":3152},[3002,8434,8435],{"class":3004,"line":4972},[3002,8436,7168],{"class":3032},[3002,8438,8439],{"class":3004,"line":4990},[3002,8440,8441],{"class":3032},"    // save() — Тести вставки нових авторів\n",[3002,8443,8444],{"class":3004,"line":5007},[3002,8445,7168],{"class":3032},[3002,8447,8448],{"class":3004,"line":5016},[3002,8449,3153],{"emptyLinePlaceholder":3152},[3002,8451,8452,8454],{"class":3004,"line":5487},[3002,8453,6905],{"class":3008},[3002,8455,3013],{"class":3012},[3002,8457,8458,8460,8462],{"class":3004,"line":5492},[3002,8459,6913],{"class":3012},[3002,8461,3398],{"class":3022},[3002,8463,3026],{"class":3008},[3002,8465,8466],{"class":3004,"line":5504},[3002,8467,8468],{"class":3032},"        // ═══ Arrange ═══\n",[3002,8470,8471,8474,8476,8478,8480,8482,8484,8486,8488,8490],{"class":3004,"line":5514},[3002,8472,8473],{"class":3012},"        Author",[3002,8475,3234],{"class":3042},[3002,8477,3046],{"class":3008},[3002,8479,3219],{"class":3218},[3002,8481,3241],{"class":3022},[3002,8483,3052],{"class":3008},[3002,8485,3247],{"class":3246},[3002,8487,2975],{"class":3008},[3002,8489,3252],{"class":3246},[3002,8491,3064],{"class":3008},[3002,8493,8494,8497,8499,8501,8503,8506],{"class":3004,"line":5552},[3002,8495,8496],{"class":3042},"        author",[3002,8498,3058],{"class":3008},[3002,8500,3488],{"class":3022},[3002,8502,3052],{"class":3008},[3002,8504,8505],{"class":3246},"\"Український письменник, поет, публіцист\"",[3002,8507,3064],{"class":3008},[3002,8509,8510,8512,8514,8517,8519,8522],{"class":3004,"line":5571},[3002,8511,8496],{"class":3042},[3002,8513,3058],{"class":3008},[3002,8515,8516],{"class":3022},"setImagePath",[3002,8518,3052],{"class":3008},[3002,8520,8521],{"class":3246},"\"/images/franko.jpg\"",[3002,8523,3064],{"class":3008},[3002,8525,8526],{"class":3004,"line":5586},[3002,8527,3153],{"emptyLinePlaceholder":3152},[3002,8529,8530],{"class":3004,"line":5600},[3002,8531,8532],{"class":3032},"        // ═══ Act ═══\n",[3002,8534,8535,8538,8540,8542],{"class":3004,"line":5605},[3002,8536,8537],{"class":3042},"        repository",[3002,8539,3058],{"class":3008},[3002,8541,3276],{"class":3022},[3002,8543,3279],{"class":3008},[3002,8545,8546],{"class":3004,"line":5610},[3002,8547,3153],{"emptyLinePlaceholder":3152},[3002,8549,8550],{"class":3004,"line":6796},[3002,8551,8552],{"class":3032},"        // ═══ Assert ═══\n",[3002,8554,8555],{"class":3004,"line":6802},[3002,8556,8557],{"class":3032},"        // Перевірка 1: ID має бути згенерований\n",[3002,8559,8560,8563,8565,8567,8569,8571,8573,8575],{"class":3004,"line":6820},[3002,8561,8562],{"class":3022},"        assertThat",[3002,8564,3052],{"class":3008},[3002,8566,3545],{"class":3042},[3002,8568,3058],{"class":3008},[3002,8570,3550],{"class":3022},[3002,8572,3136],{"class":3008},[3002,8574,6055],{"class":3022},[3002,8576,3304],{"class":3008},[3002,8578,8579],{"class":3004,"line":6825},[3002,8580,3153],{"emptyLinePlaceholder":3152},[3002,8582,8583],{"class":3004,"line":6831},[3002,8584,8585],{"class":3032},"        // Перевірка 2: Автор має бути у БД\n",[3002,8587,8588,8590,8592,8594,8597,8599,8601,8603,8605,8607,8609,8611,8613],{"class":3004,"line":6837},[3002,8589,8473],{"class":3012},[3002,8591,3530],{"class":3042},[3002,8593,3046],{"class":3008},[3002,8595,8596],{"class":3042},"repository",[3002,8598,3058],{"class":3008},[3002,8600,3540],{"class":3022},[3002,8602,3052],{"class":3008},[3002,8604,3545],{"class":3042},[3002,8606,3058],{"class":3008},[3002,8608,3550],{"class":3022},[3002,8610,3136],{"class":3008},[3002,8612,3555],{"class":3022},[3002,8614,3304],{"class":3008},[3002,8616,8617,8619,8621,8623,8625,8627,8629,8631,8633,8635],{"class":3004,"line":6843},[3002,8618,8562],{"class":3022},[3002,8620,3052],{"class":3008},[3002,8622,3567],{"class":3042},[3002,8624,3058],{"class":3008},[3002,8626,3572],{"class":3022},[3002,8628,3136],{"class":3008},[3002,8630,3577],{"class":3022},[3002,8632,3052],{"class":3008},[3002,8634,3247],{"class":3246},[3002,8636,3064],{"class":3008},[3002,8638,8639,8641,8643,8645,8647,8649,8651,8653,8655,8657],{"class":3004,"line":6849},[3002,8640,8562],{"class":3022},[3002,8642,3052],{"class":3008},[3002,8644,3567],{"class":3042},[3002,8646,3058],{"class":3008},[3002,8648,3596],{"class":3022},[3002,8650,3136],{"class":3008},[3002,8652,3577],{"class":3022},[3002,8654,3052],{"class":3008},[3002,8656,3252],{"class":3246},[3002,8658,3064],{"class":3008},[3002,8660,8661,8663,8665,8667,8669,8671,8673,8675,8677,8679],{"class":3004,"line":6863},[3002,8662,8562],{"class":3022},[3002,8664,3052],{"class":3008},[3002,8666,3567],{"class":3042},[3002,8668,3058],{"class":3008},[3002,8670,3619],{"class":3022},[3002,8672,3136],{"class":3008},[3002,8674,3577],{"class":3022},[3002,8676,3052],{"class":3008},[3002,8678,8505],{"class":3246},[3002,8680,3064],{"class":3008},[3002,8682,8683,8685,8687,8689,8691,8694,8696,8698,8700,8702],{"class":3004,"line":6868},[3002,8684,8562],{"class":3022},[3002,8686,3052],{"class":3008},[3002,8688,3567],{"class":3042},[3002,8690,3058],{"class":3008},[3002,8692,8693],{"class":3022},"getImagePath",[3002,8695,3136],{"class":3008},[3002,8697,3577],{"class":3022},[3002,8699,3052],{"class":3008},[3002,8701,8521],{"class":3246},[3002,8703,3064],{"class":3008},[3002,8705,8706],{"class":3004,"line":6873},[3002,8707,3153],{"emptyLinePlaceholder":3152},[3002,8709,8710],{"class":3004,"line":6879},[3002,8711,8712],{"class":3032},"        // Перевірка 3: У таблиці має бути рівно 1 рядок\n",[3002,8714,8715,8717,8719,8721,8723,8726,8729,8731,8733,8735],{"class":3004,"line":6885},[3002,8716,8562],{"class":3022},[3002,8718,3052],{"class":3008},[3002,8720,8197],{"class":3022},[3002,8722,3052],{"class":3008},[3002,8724,8725],{"class":3246},"\"authors\"",[3002,8727,8728],{"class":3008},")).",[3002,8730,3577],{"class":3022},[3002,8732,3052],{"class":3008},[3002,8734,3144],{"class":3143},[3002,8736,3064],{"class":3008},[3002,8738,8739],{"class":3004,"line":6891},[3002,8740,7076],{"class":3008},[3002,8742,8743],{"class":3004,"line":6897},[3002,8744,3153],{"emptyLinePlaceholder":3152},[3002,8746,8747,8749],{"class":3004,"line":6902},[3002,8748,6905],{"class":3008},[3002,8750,3013],{"class":3012},[3002,8752,8753,8755,8758],{"class":3004,"line":6910},[3002,8754,6913],{"class":3012},[3002,8756,8757],{"class":3022}," save_shouldHandleNullBio_whenBioNotProvided",[3002,8759,3026],{"class":3008},[3002,8761,8762],{"class":3004,"line":6935},[3002,8763,8468],{"class":3032},[3002,8765,8766,8768,8770,8772,8774,8776,8778,8781,8783,8786],{"class":3004,"line":6941},[3002,8767,8473],{"class":3012},[3002,8769,3234],{"class":3042},[3002,8771,3046],{"class":3008},[3002,8773,3219],{"class":3218},[3002,8775,3241],{"class":3022},[3002,8777,3052],{"class":3008},[3002,8779,8780],{"class":3246},"\"Леся\"",[3002,8782,2975],{"class":3008},[3002,8784,8785],{"class":3246},"\"Українка\"",[3002,8787,3064],{"class":3008},[3002,8789,8790],{"class":3004,"line":6984},[3002,8791,8792],{"class":3032},"        // bio та imagePath залишаються null\n",[3002,8794,8795],{"class":3004,"line":6990},[3002,8796,3153],{"emptyLinePlaceholder":3152},[3002,8798,8799],{"class":3004,"line":6996},[3002,8800,8532],{"class":3032},[3002,8802,8803,8805,8807,8809],{"class":3004,"line":7002},[3002,8804,8537],{"class":3042},[3002,8806,3058],{"class":3008},[3002,8808,3276],{"class":3022},[3002,8810,3279],{"class":3008},[3002,8812,8813],{"class":3004,"line":7008},[3002,8814,3153],{"emptyLinePlaceholder":3152},[3002,8816,8817],{"class":3004,"line":7028},[3002,8818,8552],{"class":3032},[3002,8820,8821,8823,8825,8827,8829,8831,8833,8835,8837,8839,8841,8843,8845],{"class":3004,"line":7050},[3002,8822,8473],{"class":3012},[3002,8824,3530],{"class":3042},[3002,8826,3046],{"class":3008},[3002,8828,8596],{"class":3042},[3002,8830,3058],{"class":3008},[3002,8832,3540],{"class":3022},[3002,8834,3052],{"class":3008},[3002,8836,3545],{"class":3042},[3002,8838,3058],{"class":3008},[3002,8840,3550],{"class":3022},[3002,8842,3136],{"class":3008},[3002,8844,3555],{"class":3022},[3002,8846,3304],{"class":3008},[3002,8848,8849,8851,8853,8855,8857,8859,8861,8864],{"class":3004,"line":7055},[3002,8850,8562],{"class":3022},[3002,8852,3052],{"class":3008},[3002,8854,3567],{"class":3042},[3002,8856,3058],{"class":3008},[3002,8858,3619],{"class":3022},[3002,8860,3136],{"class":3008},[3002,8862,8863],{"class":3022},"isNull",[3002,8865,3304],{"class":3008},[3002,8867,8868,8870,8872,8874,8876,8878,8880,8882],{"class":3004,"line":7061},[3002,8869,8562],{"class":3022},[3002,8871,3052],{"class":3008},[3002,8873,3567],{"class":3042},[3002,8875,3058],{"class":3008},[3002,8877,8693],{"class":3022},[3002,8879,3136],{"class":3008},[3002,8881,8863],{"class":3022},[3002,8883,3304],{"class":3008},[3002,8885,8886],{"class":3004,"line":7073},[3002,8887,7076],{"class":3008},[3002,8889,8890],{"class":3004,"line":7079},[3002,8891,3153],{"emptyLinePlaceholder":3152},[3002,8893,8894,8896],{"class":3004,"line":7084},[3002,8895,6905],{"class":3008},[3002,8897,3013],{"class":3012},[3002,8899,8900,8902,8905],{"class":3004,"line":7089},[3002,8901,6913],{"class":3012},[3002,8903,8904],{"class":3022}," save_shouldThrowException_whenDuplicateId",[3002,8906,3026],{"class":3008},[3002,8908,8909],{"class":3004,"line":7095},[3002,8910,8468],{"class":3032},[3002,8912,8913,8916,8919,8921,8923,8925,8927],{"class":3004,"line":7101},[3002,8914,8915],{"class":3012},"        UUID",[3002,8917,8918],{"class":3042}," sharedId",[3002,8920,3046],{"class":3008},[3002,8922,3875],{"class":3042},[3002,8924,3058],{"class":3008},[3002,8926,3880],{"class":3022},[3002,8928,3304],{"class":3008},[3002,8930,8931],{"class":3004,"line":7106},[3002,8932,6987],{"class":3008},[3002,8934,8935,8937,8940,8942,8944,8946,8948,8950,8952,8954],{"class":3004,"line":7113},[3002,8936,8473],{"class":3012},[3002,8938,8939],{"class":3042}," author1",[3002,8941,3046],{"class":3008},[3002,8943,3219],{"class":3218},[3002,8945,3241],{"class":3022},[3002,8947,3052],{"class":3008},[3002,8949,5828],{"class":3246},[3002,8951,2975],{"class":3008},[3002,8953,5833],{"class":3246},[3002,8955,3064],{"class":3008},[3002,8957,8958,8961,8963,8966],{"class":3004,"line":7122},[3002,8959,8960],{"class":3042},"        author1",[3002,8962,3058],{"class":3008},[3002,8964,8965],{"class":3022},"setId",[3002,8967,8968],{"class":3008},"(sharedId);\n",[3002,8970,8971],{"class":3004,"line":7137},[3002,8972,6987],{"class":3008},[3002,8974,8975,8977,8980,8982,8984,8986,8988,8990,8992,8994],{"class":3004,"line":7149},[3002,8976,8473],{"class":3012},[3002,8978,8979],{"class":3042}," author2",[3002,8981,3046],{"class":3008},[3002,8983,3219],{"class":3218},[3002,8985,3241],{"class":3022},[3002,8987,3052],{"class":3008},[3002,8989,3247],{"class":3246},[3002,8991,2975],{"class":3008},[3002,8993,3252],{"class":3246},[3002,8995,3064],{"class":3008},[3002,8997,8998,9001,9003,9005,9008],{"class":3004,"line":7155},[3002,8999,9000],{"class":3042},"        author2",[3002,9002,3058],{"class":3008},[3002,9004,8965],{"class":3022},[3002,9006,9007],{"class":3008},"(sharedId); ",[3002,9009,9010],{"class":3032},"// той самий ID!\n",[3002,9012,9013],{"class":3004,"line":7160},[3002,9014,3153],{"emptyLinePlaceholder":3152},[3002,9016,9017,9019,9021,9023,9026],{"class":3004,"line":7165},[3002,9018,8537],{"class":3042},[3002,9020,3058],{"class":3008},[3002,9022,3276],{"class":3022},[3002,9024,9025],{"class":3008},"(author1); ",[3002,9027,9028],{"class":3032},"// перший збережено успішно\n",[3002,9030,9031],{"class":3004,"line":7171},[3002,9032,3153],{"emptyLinePlaceholder":3152},[3002,9034,9035],{"class":3004,"line":7177},[3002,9036,9037],{"class":3032},"        // ═══ Act & Assert ═══\n",[3002,9039,9040],{"class":3004,"line":7182},[3002,9041,9042],{"class":3032},"        // Спроба зберегти другого автора з тим самим ID має кинути виключення\n",[3002,9044,9045,9048,9051,9054,9056,9058,9060],{"class":3004,"line":7187},[3002,9046,9047],{"class":3022},"        assertThatThrownBy",[3002,9049,9050],{"class":3008},"(() ",[3002,9052,9053],{"class":4021},"->",[3002,9055,8387],{"class":3042},[3002,9057,3058],{"class":3008},[3002,9059,3276],{"class":3022},[3002,9061,9062],{"class":3008},"(author2))\n",[3002,9064,9065,9068,9071,9073,9076,9078,9081,9083,9086,9088,9091,9093,9096,9098,9100],{"class":3004,"line":7192},[3002,9066,9067],{"class":3008},"            .",[3002,9069,9070],{"class":3022},"isInstanceOf",[3002,9072,3052],{"class":3008},[3002,9074,9075],{"class":3042},"com",[3002,9077,3058],{"class":3008},[3002,9079,9080],{"class":3042},"example",[3002,9082,3058],{"class":3008},[3002,9084,9085],{"class":3042},"audiobook",[3002,9087,3058],{"class":3008},[3002,9089,9090],{"class":3042},"db",[3002,9092,3058],{"class":3008},[3002,9094,9095],{"class":3042},"DatabaseException",[3002,9097,3058],{"class":3008},[3002,9099,3061],{"class":3042},[3002,9101,5187],{"class":3008},[3002,9103,9104,9106,9109,9111,9114,9116],{"class":3004,"line":7198},[3002,9105,9067],{"class":3008},[3002,9107,9108],{"class":3022},"hasMessageContaining",[3002,9110,3052],{"class":3008},[3002,9112,9113],{"class":3246},"\"PRIMARY KEY\"",[3002,9115,4208],{"class":3008},[3002,9117,9118],{"class":3032},"// H2 повідомляє про порушення PK\n",[3002,9120,9121],{"class":3004,"line":7203},[3002,9122,7076],{"class":3008},[3002,9124,9125],{"class":3004,"line":7209},[3002,9126,3153],{"emptyLinePlaceholder":3152},[3002,9128,9129],{"class":3004,"line":7215},[3002,9130,7168],{"class":3032},[3002,9132,9133],{"class":3004,"line":7221},[3002,9134,9135],{"class":3032},"    // findById() — Тести пошуку за ID\n",[3002,9137,9138],{"class":3004,"line":7236},[3002,9139,7168],{"class":3032},[3002,9141,9142],{"class":3004,"line":7241},[3002,9143,3153],{"emptyLinePlaceholder":3152},[3002,9145,9146,9148],{"class":3004,"line":7271},[3002,9147,6905],{"class":3008},[3002,9149,3013],{"class":3012},[3002,9151,9152,9154,9157],{"class":3004,"line":7287},[3002,9153,6913],{"class":3012},[3002,9155,9156],{"class":3022}," findById_shouldReturnAuthor_whenExists",[3002,9158,3026],{"class":3008},[3002,9160,9161],{"class":3004,"line":7292},[3002,9162,8468],{"class":3032},[3002,9164,9165,9167,9169,9171,9173,9175,9177,9180,9182,9185],{"class":3004,"line":7315},[3002,9166,8473],{"class":3012},[3002,9168,3234],{"class":3042},[3002,9170,3046],{"class":3008},[3002,9172,3219],{"class":3218},[3002,9174,3241],{"class":3022},[3002,9176,3052],{"class":3008},[3002,9178,9179],{"class":3246},"\"Михайло\"",[3002,9181,2975],{"class":3008},[3002,9183,9184],{"class":3246},"\"Коцюбинський\"",[3002,9186,3064],{"class":3008},[3002,9188,9189,9191,9193,9195],{"class":3004,"line":7337},[3002,9190,8537],{"class":3042},[3002,9192,3058],{"class":3008},[3002,9194,3276],{"class":3022},[3002,9196,3279],{"class":3008},[3002,9198,9199,9201,9204,9206,9208,9210,9212],{"class":3004,"line":7343},[3002,9200,8915],{"class":3012},[3002,9202,9203],{"class":3042}," authorId",[3002,9205,3046],{"class":3008},[3002,9207,3545],{"class":3042},[3002,9209,3058],{"class":3008},[3002,9211,3550],{"class":3022},[3002,9213,3304],{"class":3008},[3002,9215,9216],{"class":3004,"line":7349},[3002,9217,3153],{"emptyLinePlaceholder":3152},[3002,9219,9220],{"class":3004,"line":7377},[3002,9221,8532],{"class":3032},[3002,9223,9224,9227,9229,9231,9233,9236,9238,9240,9242,9244],{"class":3004,"line":7396},[3002,9225,9226],{"class":3012},"        Optional",[3002,9228,4165],{"class":3008},[3002,9230,4168],{"class":3012},[3002,9232,4171],{"class":3008},[3002,9234,9235],{"class":3042},"result",[3002,9237,3046],{"class":3008},[3002,9239,8596],{"class":3042},[3002,9241,3058],{"class":3008},[3002,9243,3540],{"class":3022},[3002,9245,9246],{"class":3008},"(authorId);\n",[3002,9248,9249],{"class":3004,"line":7417},[3002,9250,3153],{"emptyLinePlaceholder":3152},[3002,9252,9253],{"class":3004,"line":7451},[3002,9254,8552],{"class":3032},[3002,9256,9257,9259,9262,9265],{"class":3004,"line":7465},[3002,9258,8562],{"class":3022},[3002,9260,9261],{"class":3008},"(result).",[3002,9263,9264],{"class":3022},"isPresent",[3002,9266,3304],{"class":3008},[3002,9268,9269,9271,9273,9275,9277,9280,9282,9284,9286,9288,9290,9292],{"class":3004,"line":7471},[3002,9270,8562],{"class":3022},[3002,9272,3052],{"class":3008},[3002,9274,9235],{"class":3042},[3002,9276,3058],{"class":3008},[3002,9278,9279],{"class":3022},"get",[3002,9281,6961],{"class":3008},[3002,9283,3572],{"class":3022},[3002,9285,3136],{"class":3008},[3002,9287,3577],{"class":3022},[3002,9289,3052],{"class":3008},[3002,9291,9179],{"class":3246},[3002,9293,3064],{"class":3008},[3002,9295,9296,9298,9300,9302,9304,9306,9308,9310,9312,9314,9316,9318],{"class":3004,"line":7477},[3002,9297,8562],{"class":3022},[3002,9299,3052],{"class":3008},[3002,9301,9235],{"class":3042},[3002,9303,3058],{"class":3008},[3002,9305,9279],{"class":3022},[3002,9307,6961],{"class":3008},[3002,9309,3596],{"class":3022},[3002,9311,3136],{"class":3008},[3002,9313,3577],{"class":3022},[3002,9315,3052],{"class":3008},[3002,9317,9184],{"class":3246},[3002,9319,3064],{"class":3008},[3002,9321,9322],{"class":3004,"line":7482},[3002,9323,7076],{"class":3008},[3002,9325,9326],{"class":3004,"line":7487},[3002,9327,3153],{"emptyLinePlaceholder":3152},[3002,9329,9330,9332],{"class":3004,"line":7492},[3002,9331,6905],{"class":3008},[3002,9333,3013],{"class":3012},[3002,9335,9336,9338,9341],{"class":3004,"line":7497},[3002,9337,6913],{"class":3012},[3002,9339,9340],{"class":3022}," findById_shouldReturnEmpty_whenNotExists",[3002,9342,3026],{"class":3008},[3002,9344,9345],{"class":3004,"line":7503},[3002,9346,8468],{"class":3032},[3002,9348,9349,9351,9354,9356,9358,9360,9362],{"class":3004,"line":7508},[3002,9350,8915],{"class":3012},[3002,9352,9353],{"class":3042}," nonExistentId",[3002,9355,3046],{"class":3008},[3002,9357,3875],{"class":3042},[3002,9359,3058],{"class":3008},[3002,9361,3880],{"class":3022},[3002,9363,3304],{"class":3008},[3002,9365,9366],{"class":3004,"line":7521},[3002,9367,3153],{"emptyLinePlaceholder":3152},[3002,9369,9370],{"class":3004,"line":7532},[3002,9371,8532],{"class":3032},[3002,9373,9374,9376,9378,9380,9382,9384,9386,9388,9390,9392],{"class":3004,"line":7537},[3002,9375,9226],{"class":3012},[3002,9377,4165],{"class":3008},[3002,9379,4168],{"class":3012},[3002,9381,4171],{"class":3008},[3002,9383,9235],{"class":3042},[3002,9385,3046],{"class":3008},[3002,9387,8596],{"class":3042},[3002,9389,3058],{"class":3008},[3002,9391,3540],{"class":3022},[3002,9393,9394],{"class":3008},"(nonExistentId);\n",[3002,9396,9397],{"class":3004,"line":7562},[3002,9398,3153],{"emptyLinePlaceholder":3152},[3002,9400,9401],{"class":3004,"line":7585},[3002,9402,8552],{"class":3032},[3002,9404,9405,9407,9409,9411],{"class":3004,"line":7596},[3002,9406,8562],{"class":3022},[3002,9408,9261],{"class":3008},[3002,9410,4471],{"class":3022},[3002,9412,3304],{"class":3008},[3002,9414,9415],{"class":3004,"line":7615},[3002,9416,7076],{"class":3008},[3002,9418,9419],{"class":3004,"line":7629},[3002,9420,3153],{"emptyLinePlaceholder":3152},[3002,9422,9423],{"class":3004,"line":7670},[3002,9424,7168],{"class":3032},[3002,9426,9427],{"class":3004,"line":7675},[3002,9428,9429],{"class":3032},"    // findAll() — Тести вибірки всіх авторів\n",[3002,9431,9432],{"class":3004,"line":7680},[3002,9433,7168],{"class":3032},[3002,9435,9436],{"class":3004,"line":7685},[3002,9437,3153],{"emptyLinePlaceholder":3152},[3002,9439,9440,9442],{"class":3004,"line":7690},[3002,9441,6905],{"class":3008},[3002,9443,3013],{"class":3012},[3002,9445,9446,9448,9451],{"class":3004,"line":7696},[3002,9447,6913],{"class":3012},[3002,9449,9450],{"class":3022}," findAll_shouldReturnAllAuthors_whenMultipleExist",[3002,9452,3026],{"class":3008},[3002,9454,9455],{"class":3004,"line":7701},[3002,9456,8468],{"class":3032},[3002,9458,9459,9461,9463,9465,9467,9469,9471,9473,9475,9477],{"class":3004,"line":7707},[3002,9460,8473],{"class":3012},[3002,9462,8939],{"class":3042},[3002,9464,3046],{"class":3008},[3002,9466,3219],{"class":3218},[3002,9468,3241],{"class":3022},[3002,9470,3052],{"class":3008},[3002,9472,5828],{"class":3246},[3002,9474,2975],{"class":3008},[3002,9476,5833],{"class":3246},[3002,9478,3064],{"class":3008},[3002,9480,9481,9483,9485,9487,9489,9491,9493,9495,9497,9499],{"class":3004,"line":7712},[3002,9482,8473],{"class":3012},[3002,9484,8979],{"class":3042},[3002,9486,3046],{"class":3008},[3002,9488,3219],{"class":3218},[3002,9490,3241],{"class":3022},[3002,9492,3052],{"class":3008},[3002,9494,3247],{"class":3246},[3002,9496,2975],{"class":3008},[3002,9498,3252],{"class":3246},[3002,9500,3064],{"class":3008},[3002,9502,9503,9505,9508,9510,9512,9514,9516,9518,9520,9522],{"class":3004,"line":7724},[3002,9504,8473],{"class":3012},[3002,9506,9507],{"class":3042}," author3",[3002,9509,3046],{"class":3008},[3002,9511,3219],{"class":3218},[3002,9513,3241],{"class":3022},[3002,9515,3052],{"class":3008},[3002,9517,8780],{"class":3246},[3002,9519,2975],{"class":3008},[3002,9521,8785],{"class":3246},[3002,9523,3064],{"class":3008},[3002,9525,9526],{"class":3004,"line":7729},[3002,9527,6987],{"class":3008},[3002,9529,9530,9532,9534,9536],{"class":3004,"line":7753},[3002,9531,8537],{"class":3042},[3002,9533,3058],{"class":3008},[3002,9535,3276],{"class":3022},[3002,9537,9538],{"class":3008},"(author1);\n",[3002,9540,9541,9543,9545,9547],{"class":3004,"line":7774},[3002,9542,8537],{"class":3042},[3002,9544,3058],{"class":3008},[3002,9546,3276],{"class":3022},[3002,9548,9549],{"class":3008},"(author2);\n",[3002,9551,9552,9554,9556,9558],{"class":3004,"line":7791},[3002,9553,8537],{"class":3042},[3002,9555,3058],{"class":3008},[3002,9557,3276],{"class":3022},[3002,9559,9560],{"class":3008},"(author3);\n",[3002,9562,9563],{"class":3004,"line":7804},[3002,9564,3153],{"emptyLinePlaceholder":3152},[3002,9566,9567],{"class":3004,"line":7809},[3002,9568,8532],{"class":3032},[3002,9570,9571,9574,9576,9578,9580,9582,9584,9586,9588,9590],{"class":3004,"line":7814},[3002,9572,9573],{"class":3012},"        List",[3002,9575,4165],{"class":3008},[3002,9577,4168],{"class":3012},[3002,9579,4171],{"class":3008},[3002,9581,4174],{"class":3042},[3002,9583,3046],{"class":3008},[3002,9585,8596],{"class":3042},[3002,9587,3058],{"class":3008},[3002,9589,4183],{"class":3022},[3002,9591,3304],{"class":3008},[3002,9593,9594],{"class":3004,"line":7819},[3002,9595,3153],{"emptyLinePlaceholder":3152},[3002,9597,9598],{"class":3004,"line":7824},[3002,9599,8552],{"class":3032},[3002,9601,9602,9604,9606,9608,9610,9613],{"class":3004,"line":7830},[3002,9603,8562],{"class":3022},[3002,9605,4197],{"class":3008},[3002,9607,4200],{"class":3022},[3002,9609,3052],{"class":3008},[3002,9611,9612],{"class":3143},"3",[3002,9614,3064],{"class":3008},[3002,9616,9617,9619],{"class":3004,"line":7836},[3002,9618,8562],{"class":3022},[3002,9620,9621],{"class":3008},"(all)\n",[3002,9623,9624,9626,9629,9632,9635],{"class":3004,"line":7841},[3002,9625,9067],{"class":3008},[3002,9627,9628],{"class":3022},"extracting",[3002,9630,9631],{"class":3008},"(Author",[3002,9633,9634],{"class":3218},"::",[3002,9636,9637],{"class":3008},"getLastName)\n",[3002,9639,9640,9642,9645,9647,9649,9651,9653,9655,9657],{"class":3004,"line":7854},[3002,9641,9067],{"class":3008},[3002,9643,9644],{"class":3022},"containsExactlyInAnyOrder",[3002,9646,3052],{"class":3008},[3002,9648,5833],{"class":3246},[3002,9650,2975],{"class":3008},[3002,9652,3252],{"class":3246},[3002,9654,2975],{"class":3008},[3002,9656,8785],{"class":3246},[3002,9658,3064],{"class":3008},[3002,9660,9661],{"class":3004,"line":7864},[3002,9662,7076],{"class":3008},[3002,9664,9665],{"class":3004,"line":7869},[3002,9666,3153],{"emptyLinePlaceholder":3152},[3002,9668,9669,9671],{"class":3004,"line":7894},[3002,9670,6905],{"class":3008},[3002,9672,3013],{"class":3012},[3002,9674,9675,9677,9680],{"class":3004,"line":7909},[3002,9676,6913],{"class":3012},[3002,9678,9679],{"class":3022}," findAll_shouldReturnEmptyList_whenTableEmpty",[3002,9681,3026],{"class":3008},[3002,9683,9684],{"class":3004,"line":7930},[3002,9685,8468],{"class":3032},[3002,9687,9688],{"class":3004,"line":7947},[3002,9689,9690],{"class":3032},"        // Таблиця порожня (нова БД для кожного тесту)\n",[3002,9692,9693],{"class":3004,"line":7969},[3002,9694,3153],{"emptyLinePlaceholder":3152},[3002,9696,9697],{"class":3004,"line":7982},[3002,9698,8532],{"class":3032},[3002,9700,9701,9703,9705,9707,9709,9711,9713,9715,9717,9719],{"class":3004,"line":8000},[3002,9702,9573],{"class":3012},[3002,9704,4165],{"class":3008},[3002,9706,4168],{"class":3012},[3002,9708,4171],{"class":3008},[3002,9710,4174],{"class":3042},[3002,9712,3046],{"class":3008},[3002,9714,8596],{"class":3042},[3002,9716,3058],{"class":3008},[3002,9718,4183],{"class":3022},[3002,9720,3304],{"class":3008},[3002,9722,9723],{"class":3004,"line":8005},[3002,9724,3153],{"emptyLinePlaceholder":3152},[3002,9726,9727],{"class":3004,"line":8010},[3002,9728,8552],{"class":3032},[3002,9730,9731,9733,9735,9737],{"class":3004,"line":8015},[3002,9732,8562],{"class":3022},[3002,9734,4197],{"class":3008},[3002,9736,4471],{"class":3022},[3002,9738,3304],{"class":3008},[3002,9740,9741],{"class":3004,"line":8020},[3002,9742,7076],{"class":3008},[3002,9744,9745],{"class":3004,"line":8026},[3002,9746,3153],{"emptyLinePlaceholder":3152},[3002,9748,9749],{"class":3004,"line":8032},[3002,9750,7168],{"class":3032},[3002,9752,9753],{"class":3004,"line":8037},[3002,9754,9755],{"class":3032},"    // update() — Тести оновлення існуючих авторів\n",[3002,9757,9758],{"class":3004,"line":8048},[3002,9759,7168],{"class":3032},[3002,9761,9762],{"class":3004,"line":8053},[3002,9763,3153],{"emptyLinePlaceholder":3152},[3002,9765,9766,9768],{"class":3004,"line":8077},[3002,9767,6905],{"class":3008},[3002,9769,3013],{"class":3012},[3002,9771,9772,9774,9777],{"class":3004,"line":8091},[3002,9773,6913],{"class":3012},[3002,9775,9776],{"class":3022}," update_shouldModifyAllFields_whenAuthorExists",[3002,9778,3026],{"class":3008},[3002,9780,9781],{"class":3004,"line":8112},[3002,9782,8468],{"class":3032},[3002,9784,9785,9787,9789,9791,9793,9795,9797,9799,9801,9803],{"class":3004,"line":8129},[3002,9786,8473],{"class":3012},[3002,9788,3234],{"class":3042},[3002,9790,3046],{"class":3008},[3002,9792,3219],{"class":3218},[3002,9794,3241],{"class":3022},[3002,9796,3052],{"class":3008},[3002,9798,3247],{"class":3246},[3002,9800,2975],{"class":3008},[3002,9802,3252],{"class":3246},[3002,9804,3064],{"class":3008},[3002,9806,9807,9809,9811,9813,9815,9818],{"class":3004,"line":8140},[3002,9808,8496],{"class":3042},[3002,9810,3058],{"class":3008},[3002,9812,3488],{"class":3022},[3002,9814,3052],{"class":3008},[3002,9816,9817],{"class":3246},"\"Стара біографія\"",[3002,9819,3064],{"class":3008},[3002,9821,9822,9824,9826,9828],{"class":3004,"line":8145},[3002,9823,8537],{"class":3042},[3002,9825,3058],{"class":3008},[3002,9827,3276],{"class":3022},[3002,9829,3279],{"class":3008},[3002,9831,9832],{"class":3004,"line":8150},[3002,9833,3153],{"emptyLinePlaceholder":3152},[3002,9835,9837],{"class":3004,"line":9836},175,[3002,9838,9839],{"class":3032},"        // Змінити всі поля\n",[3002,9841,9843,9845,9847,9850,9852,9855],{"class":3004,"line":9842},176,[3002,9844,8496],{"class":3042},[3002,9846,3058],{"class":3008},[3002,9848,9849],{"class":3022},"setFirstName",[3002,9851,3052],{"class":3008},[3002,9853,9854],{"class":3246},"\"Іван Якович\"",[3002,9856,3064],{"class":3008},[3002,9858,9860,9862,9864,9867,9869,9872],{"class":3004,"line":9859},177,[3002,9861,8496],{"class":3042},[3002,9863,3058],{"class":3008},[3002,9865,9866],{"class":3022},"setLastName",[3002,9868,3052],{"class":3008},[3002,9870,9871],{"class":3246},"\"Франко-Захарченко\"",[3002,9873,3064],{"class":3008},[3002,9875,9877,9879,9881,9883,9885,9888],{"class":3004,"line":9876},178,[3002,9878,8496],{"class":3042},[3002,9880,3058],{"class":3008},[3002,9882,3488],{"class":3022},[3002,9884,3052],{"class":3008},[3002,9886,9887],{"class":3246},"\"Нова біографія\"",[3002,9889,3064],{"class":3008},[3002,9891,9893,9895,9897,9899,9901,9904],{"class":3004,"line":9892},179,[3002,9894,8496],{"class":3042},[3002,9896,3058],{"class":3008},[3002,9898,8516],{"class":3022},[3002,9900,3052],{"class":3008},[3002,9902,9903],{"class":3246},"\"/images/new.jpg\"",[3002,9905,3064],{"class":3008},[3002,9907,9909],{"class":3004,"line":9908},180,[3002,9910,3153],{"emptyLinePlaceholder":3152},[3002,9912,9914],{"class":3004,"line":9913},181,[3002,9915,8532],{"class":3032},[3002,9917,9919,9921,9923,9925],{"class":3004,"line":9918},182,[3002,9920,8537],{"class":3042},[3002,9922,3058],{"class":3008},[3002,9924,6131],{"class":3022},[3002,9926,3279],{"class":3008},[3002,9928,9930],{"class":3004,"line":9929},183,[3002,9931,3153],{"emptyLinePlaceholder":3152},[3002,9933,9935],{"class":3004,"line":9934},184,[3002,9936,8552],{"class":3032},[3002,9938,9940,9942,9944,9946,9948,9950,9952,9954,9956,9958,9960,9962,9964],{"class":3004,"line":9939},185,[3002,9941,8473],{"class":3012},[3002,9943,3530],{"class":3042},[3002,9945,3046],{"class":3008},[3002,9947,8596],{"class":3042},[3002,9949,3058],{"class":3008},[3002,9951,3540],{"class":3022},[3002,9953,3052],{"class":3008},[3002,9955,3545],{"class":3042},[3002,9957,3058],{"class":3008},[3002,9959,3550],{"class":3022},[3002,9961,3136],{"class":3008},[3002,9963,3555],{"class":3022},[3002,9965,3304],{"class":3008},[3002,9967,9969,9971,9973,9975,9977,9979,9981,9983,9985,9987],{"class":3004,"line":9968},186,[3002,9970,8562],{"class":3022},[3002,9972,3052],{"class":3008},[3002,9974,3567],{"class":3042},[3002,9976,3058],{"class":3008},[3002,9978,3572],{"class":3022},[3002,9980,3136],{"class":3008},[3002,9982,3577],{"class":3022},[3002,9984,3052],{"class":3008},[3002,9986,9854],{"class":3246},[3002,9988,3064],{"class":3008},[3002,9990,9992,9994,9996,9998,10000,10002,10004,10006,10008,10010],{"class":3004,"line":9991},187,[3002,9993,8562],{"class":3022},[3002,9995,3052],{"class":3008},[3002,9997,3567],{"class":3042},[3002,9999,3058],{"class":3008},[3002,10001,3596],{"class":3022},[3002,10003,3136],{"class":3008},[3002,10005,3577],{"class":3022},[3002,10007,3052],{"class":3008},[3002,10009,9871],{"class":3246},[3002,10011,3064],{"class":3008},[3002,10013,10015,10017,10019,10021,10023,10025,10027,10029,10031,10033],{"class":3004,"line":10014},188,[3002,10016,8562],{"class":3022},[3002,10018,3052],{"class":3008},[3002,10020,3567],{"class":3042},[3002,10022,3058],{"class":3008},[3002,10024,3619],{"class":3022},[3002,10026,3136],{"class":3008},[3002,10028,3577],{"class":3022},[3002,10030,3052],{"class":3008},[3002,10032,9887],{"class":3246},[3002,10034,3064],{"class":3008},[3002,10036,10038,10040,10042,10044,10046,10048,10050,10052,10054,10056],{"class":3004,"line":10037},189,[3002,10039,8562],{"class":3022},[3002,10041,3052],{"class":3008},[3002,10043,3567],{"class":3042},[3002,10045,3058],{"class":3008},[3002,10047,8693],{"class":3022},[3002,10049,3136],{"class":3008},[3002,10051,3577],{"class":3022},[3002,10053,3052],{"class":3008},[3002,10055,9903],{"class":3246},[3002,10057,3064],{"class":3008},[3002,10059,10061],{"class":3004,"line":10060},190,[3002,10062,7076],{"class":3008},[3002,10064,10066],{"class":3004,"line":10065},191,[3002,10067,3153],{"emptyLinePlaceholder":3152},[3002,10069,10071,10073],{"class":3004,"line":10070},192,[3002,10072,6905],{"class":3008},[3002,10074,3013],{"class":3012},[3002,10076,10078,10080,10083],{"class":3004,"line":10077},193,[3002,10079,6913],{"class":3012},[3002,10081,10082],{"class":3022}," update_shouldThrowException_whenAuthorNotExists",[3002,10084,3026],{"class":3008},[3002,10086,10088],{"class":3004,"line":10087},194,[3002,10089,8468],{"class":3032},[3002,10091,10093,10095,10097,10099,10101,10103,10105,10108,10110,10113],{"class":3004,"line":10092},195,[3002,10094,8473],{"class":3012},[3002,10096,3234],{"class":3042},[3002,10098,3046],{"class":3008},[3002,10100,3219],{"class":3218},[3002,10102,3241],{"class":3022},[3002,10104,3052],{"class":3008},[3002,10106,10107],{"class":3246},"\"Неіснуючий\"",[3002,10109,2975],{"class":3008},[3002,10111,10112],{"class":3246},"\"Автор\"",[3002,10114,3064],{"class":3008},[3002,10116,10118,10120,10122,10124,10126,10128,10130,10132,10135],{"class":3004,"line":10117},196,[3002,10119,8496],{"class":3042},[3002,10121,3058],{"class":3008},[3002,10123,8965],{"class":3022},[3002,10125,3052],{"class":3008},[3002,10127,3875],{"class":3042},[3002,10129,3058],{"class":3008},[3002,10131,3880],{"class":3022},[3002,10133,10134],{"class":3008},"()); ",[3002,10136,10137],{"class":3032},"// ID, якого немає у БД\n",[3002,10139,10141],{"class":3004,"line":10140},197,[3002,10142,3153],{"emptyLinePlaceholder":3152},[3002,10144,10146],{"class":3004,"line":10145},198,[3002,10147,9037],{"class":3032},[3002,10149,10151,10153,10155,10157,10159,10161,10163],{"class":3004,"line":10150},199,[3002,10152,9047],{"class":3022},[3002,10154,9050],{"class":3008},[3002,10156,9053],{"class":4021},[3002,10158,8387],{"class":3042},[3002,10160,3058],{"class":3008},[3002,10162,6131],{"class":3022},[3002,10164,10165],{"class":3008},"(author))\n",[3002,10167,10169,10171,10173,10175,10177,10179,10181,10183,10185,10187,10189,10191,10193,10195,10197],{"class":3004,"line":10168},200,[3002,10170,9067],{"class":3008},[3002,10172,9070],{"class":3022},[3002,10174,3052],{"class":3008},[3002,10176,9075],{"class":3042},[3002,10178,3058],{"class":3008},[3002,10180,9080],{"class":3042},[3002,10182,3058],{"class":3008},[3002,10184,9085],{"class":3042},[3002,10186,3058],{"class":3008},[3002,10188,9090],{"class":3042},[3002,10190,3058],{"class":3008},[3002,10192,9095],{"class":3042},[3002,10194,3058],{"class":3008},[3002,10196,3061],{"class":3042},[3002,10198,5187],{"class":3008},[3002,10200,10202,10204,10206,10208,10211,10213],{"class":3004,"line":10201},201,[3002,10203,9067],{"class":3008},[3002,10205,9108],{"class":3022},[3002,10207,3052],{"class":3008},[3002,10209,10210],{"class":3246},"\"не знайдена\"",[3002,10212,4208],{"class":3008},[3002,10214,10215],{"class":3032},"// повідомлення з AbstractJdbcRepository\n",[3002,10217,10219],{"class":3004,"line":10218},202,[3002,10220,7076],{"class":3008},[3002,10222,10224],{"class":3004,"line":10223},203,[3002,10225,3153],{"emptyLinePlaceholder":3152},[3002,10227,10229],{"class":3004,"line":10228},204,[3002,10230,7168],{"class":3032},[3002,10232,10234],{"class":3004,"line":10233},205,[3002,10235,10236],{"class":3032},"    // deleteById() — Тести видалення авторів\n",[3002,10238,10240],{"class":3004,"line":10239},206,[3002,10241,7168],{"class":3032},[3002,10243,10245],{"class":3004,"line":10244},207,[3002,10246,3153],{"emptyLinePlaceholder":3152},[3002,10248,10250,10252],{"class":3004,"line":10249},208,[3002,10251,6905],{"class":3008},[3002,10253,3013],{"class":3012},[3002,10255,10257,10259,10262],{"class":3004,"line":10256},209,[3002,10258,6913],{"class":3012},[3002,10260,10261],{"class":3022}," deleteById_shouldRemoveAuthor_whenExists",[3002,10263,3026],{"class":3008},[3002,10265,10267],{"class":3004,"line":10266},210,[3002,10268,8468],{"class":3032},[3002,10270,10272,10274,10276,10278,10280,10282,10284,10287,10289,10292],{"class":3004,"line":10271},211,[3002,10273,8473],{"class":3012},[3002,10275,3234],{"class":3042},[3002,10277,3046],{"class":3008},[3002,10279,3219],{"class":3218},[3002,10281,3241],{"class":3022},[3002,10283,3052],{"class":3008},[3002,10285,10286],{"class":3246},"\"Панас\"",[3002,10288,2975],{"class":3008},[3002,10290,10291],{"class":3246},"\"Мирний\"",[3002,10293,3064],{"class":3008},[3002,10295,10297,10299,10301,10303],{"class":3004,"line":10296},212,[3002,10298,8537],{"class":3042},[3002,10300,3058],{"class":3008},[3002,10302,3276],{"class":3022},[3002,10304,3279],{"class":3008},[3002,10306,10308,10310,10312,10314,10316,10318,10320],{"class":3004,"line":10307},213,[3002,10309,8915],{"class":3012},[3002,10311,9203],{"class":3042},[3002,10313,3046],{"class":3008},[3002,10315,3545],{"class":3042},[3002,10317,3058],{"class":3008},[3002,10319,3550],{"class":3022},[3002,10321,3304],{"class":3008},[3002,10323,10325],{"class":3004,"line":10324},214,[3002,10326,3153],{"emptyLinePlaceholder":3152},[3002,10328,10330],{"class":3004,"line":10329},215,[3002,10331,8532],{"class":3032},[3002,10333,10335,10338,10341,10343,10345,10347,10350],{"class":3004,"line":10334},216,[3002,10336,10337],{"class":3012},"        boolean",[3002,10339,10340],{"class":3042}," deleted",[3002,10342,3046],{"class":3008},[3002,10344,8596],{"class":3042},[3002,10346,3058],{"class":3008},[3002,10348,10349],{"class":3022},"deleteById",[3002,10351,9246],{"class":3008},[3002,10353,10355],{"class":3004,"line":10354},217,[3002,10356,3153],{"emptyLinePlaceholder":3152},[3002,10358,10360],{"class":3004,"line":10359},218,[3002,10361,8552],{"class":3032},[3002,10363,10365,10367,10370,10373],{"class":3004,"line":10364},219,[3002,10366,8562],{"class":3022},[3002,10368,10369],{"class":3008},"(deleted).",[3002,10371,10372],{"class":3022},"isTrue",[3002,10374,3304],{"class":3008},[3002,10376,10378,10380,10382,10384,10386,10388,10391,10393],{"class":3004,"line":10377},220,[3002,10379,8562],{"class":3022},[3002,10381,3052],{"class":3008},[3002,10383,8596],{"class":3042},[3002,10385,3058],{"class":3008},[3002,10387,3540],{"class":3022},[3002,10389,10390],{"class":3008},"(authorId)).",[3002,10392,4471],{"class":3022},[3002,10394,3304],{"class":3008},[3002,10396,10398,10400,10402,10404,10406,10408,10410,10412,10414,10416],{"class":3004,"line":10397},221,[3002,10399,8562],{"class":3022},[3002,10401,3052],{"class":3008},[3002,10403,8197],{"class":3022},[3002,10405,3052],{"class":3008},[3002,10407,8725],{"class":3246},[3002,10409,8728],{"class":3008},[3002,10411,3577],{"class":3022},[3002,10413,3052],{"class":3008},[3002,10415,5328],{"class":3143},[3002,10417,3064],{"class":3008},[3002,10419,10421],{"class":3004,"line":10420},222,[3002,10422,7076],{"class":3008},[3002,10424,10426],{"class":3004,"line":10425},223,[3002,10427,3153],{"emptyLinePlaceholder":3152},[3002,10429,10431,10433],{"class":3004,"line":10430},224,[3002,10432,6905],{"class":3008},[3002,10434,3013],{"class":3012},[3002,10436,10438,10440,10443],{"class":3004,"line":10437},225,[3002,10439,6913],{"class":3012},[3002,10441,10442],{"class":3022}," deleteById_shouldReturnFalse_whenNotExists",[3002,10444,3026],{"class":3008},[3002,10446,10448],{"class":3004,"line":10447},226,[3002,10449,8468],{"class":3032},[3002,10451,10453,10455,10457,10459,10461,10463,10465],{"class":3004,"line":10452},227,[3002,10454,8915],{"class":3012},[3002,10456,9353],{"class":3042},[3002,10458,3046],{"class":3008},[3002,10460,3875],{"class":3042},[3002,10462,3058],{"class":3008},[3002,10464,3880],{"class":3022},[3002,10466,3304],{"class":3008},[3002,10468,10470],{"class":3004,"line":10469},228,[3002,10471,3153],{"emptyLinePlaceholder":3152},[3002,10473,10475],{"class":3004,"line":10474},229,[3002,10476,8532],{"class":3032},[3002,10478,10480,10482,10484,10486,10488,10490,10492],{"class":3004,"line":10479},230,[3002,10481,10337],{"class":3012},[3002,10483,10340],{"class":3042},[3002,10485,3046],{"class":3008},[3002,10487,8596],{"class":3042},[3002,10489,3058],{"class":3008},[3002,10491,10349],{"class":3022},[3002,10493,9394],{"class":3008},[3002,10495,10497],{"class":3004,"line":10496},231,[3002,10498,3153],{"emptyLinePlaceholder":3152},[3002,10500,10502],{"class":3004,"line":10501},232,[3002,10503,8552],{"class":3032},[3002,10505,10507,10509,10511,10514],{"class":3004,"line":10506},233,[3002,10508,8562],{"class":3022},[3002,10510,10369],{"class":3008},[3002,10512,10513],{"class":3022},"isFalse",[3002,10515,3304],{"class":3008},[3002,10517,10519],{"class":3004,"line":10518},234,[3002,10520,7076],{"class":3008},[3002,10522,10524],{"class":3004,"line":10523},235,[3002,10525,3153],{"emptyLinePlaceholder":3152},[3002,10527,10529],{"class":3004,"line":10528},236,[3002,10530,7168],{"class":3032},[3002,10532,10534],{"class":3004,"line":10533},237,[3002,10535,10536],{"class":3032},"    // findByLastName() — Тести пошуку за прізвищем\n",[3002,10538,10540],{"class":3004,"line":10539},238,[3002,10541,7168],{"class":3032},[3002,10543,10545],{"class":3004,"line":10544},239,[3002,10546,3153],{"emptyLinePlaceholder":3152},[3002,10548,10550,10552],{"class":3004,"line":10549},240,[3002,10551,6905],{"class":3008},[3002,10553,3013],{"class":3012},[3002,10555,10557,10559,10562],{"class":3004,"line":10556},241,[3002,10558,6913],{"class":3012},[3002,10560,10561],{"class":3022}," findByLastName_shouldReturnMatchingAuthors_whenPartialMatch",[3002,10563,3026],{"class":3008},[3002,10565,10567],{"class":3004,"line":10566},242,[3002,10568,8468],{"class":3032},[3002,10570,10572,10574,10576,10578,10580,10582,10584,10586,10588,10590,10592],{"class":3004,"line":10571},243,[3002,10573,8537],{"class":3042},[3002,10575,3058],{"class":3008},[3002,10577,3276],{"class":3022},[3002,10579,3052],{"class":3008},[3002,10581,3219],{"class":3218},[3002,10583,3241],{"class":3022},[3002,10585,3052],{"class":3008},[3002,10587,5828],{"class":3246},[3002,10589,2975],{"class":3008},[3002,10591,5833],{"class":3246},[3002,10593,4129],{"class":3008},[3002,10595,10597,10599,10601,10603,10605,10607,10609,10611,10613,10615,10617],{"class":3004,"line":10596},244,[3002,10598,8537],{"class":3042},[3002,10600,3058],{"class":3008},[3002,10602,3276],{"class":3022},[3002,10604,3052],{"class":3008},[3002,10606,3219],{"class":3218},[3002,10608,3241],{"class":3022},[3002,10610,3052],{"class":3008},[3002,10612,3247],{"class":3246},[3002,10614,2975],{"class":3008},[3002,10616,3252],{"class":3246},[3002,10618,4129],{"class":3008},[3002,10620,10622,10624,10626,10628,10630,10632,10634,10636,10638,10640,10642],{"class":3004,"line":10621},245,[3002,10623,8537],{"class":3042},[3002,10625,3058],{"class":3008},[3002,10627,3276],{"class":3022},[3002,10629,3052],{"class":3008},[3002,10631,3219],{"class":3218},[3002,10633,3241],{"class":3022},[3002,10635,3052],{"class":3008},[3002,10637,8780],{"class":3246},[3002,10639,2975],{"class":3008},[3002,10641,8785],{"class":3246},[3002,10643,4129],{"class":3008},[3002,10645,10647,10649,10651,10653,10655,10657,10659,10661,10664,10666,10669],{"class":3004,"line":10646},246,[3002,10648,8537],{"class":3042},[3002,10650,3058],{"class":3008},[3002,10652,3276],{"class":3022},[3002,10654,3052],{"class":3008},[3002,10656,3219],{"class":3218},[3002,10658,3241],{"class":3022},[3002,10660,3052],{"class":3008},[3002,10662,10663],{"class":3246},"\"Григорій\"",[3002,10665,2975],{"class":3008},[3002,10667,10668],{"class":3246},"\"Сковорода\"",[3002,10670,4129],{"class":3008},[3002,10672,10674],{"class":3004,"line":10673},247,[3002,10675,3153],{"emptyLinePlaceholder":3152},[3002,10677,10679],{"class":3004,"line":10678},248,[3002,10680,8532],{"class":3032},[3002,10682,10684,10686,10688,10690,10692,10694,10696,10698,10700,10703,10705,10708,10710],{"class":3004,"line":10683},249,[3002,10685,9573],{"class":3012},[3002,10687,4165],{"class":3008},[3002,10689,4168],{"class":3012},[3002,10691,4171],{"class":3008},[3002,10693,9235],{"class":3042},[3002,10695,3046],{"class":3008},[3002,10697,8596],{"class":3042},[3002,10699,3058],{"class":3008},[3002,10701,10702],{"class":3022},"findByLastName",[3002,10704,3052],{"class":3008},[3002,10706,10707],{"class":3246},"\"ко\"",[3002,10709,4208],{"class":3008},[3002,10711,10712],{"class":3032},"// \"Франко\", \"Сковорода\"\n",[3002,10714,10716],{"class":3004,"line":10715},250,[3002,10717,3153],{"emptyLinePlaceholder":3152},[3002,10719,10721],{"class":3004,"line":10720},251,[3002,10722,8552],{"class":3032},[3002,10724,10726,10728,10730,10732,10734,10737],{"class":3004,"line":10725},252,[3002,10727,8562],{"class":3022},[3002,10729,9261],{"class":3008},[3002,10731,4200],{"class":3022},[3002,10733,3052],{"class":3008},[3002,10735,10736],{"class":3143},"2",[3002,10738,3064],{"class":3008},[3002,10740,10742,10744],{"class":3004,"line":10741},253,[3002,10743,8562],{"class":3022},[3002,10745,10746],{"class":3008},"(result)\n",[3002,10748,10750,10752,10754,10756,10758],{"class":3004,"line":10749},254,[3002,10751,9067],{"class":3008},[3002,10753,9628],{"class":3022},[3002,10755,9631],{"class":3008},[3002,10757,9634],{"class":3218},[3002,10759,9637],{"class":3008},[3002,10761,10763,10765,10767,10769,10771,10773,10775],{"class":3004,"line":10762},255,[3002,10764,9067],{"class":3008},[3002,10766,9644],{"class":3022},[3002,10768,3052],{"class":3008},[3002,10770,3252],{"class":3246},[3002,10772,2975],{"class":3008},[3002,10774,10668],{"class":3246},[3002,10776,3064],{"class":3008},[3002,10778,10780],{"class":3004,"line":10779},256,[3002,10781,7076],{"class":3008},[3002,10783,10785],{"class":3004,"line":10784},257,[3002,10786,3153],{"emptyLinePlaceholder":3152},[3002,10788,10790,10792],{"class":3004,"line":10789},258,[3002,10791,6905],{"class":3008},[3002,10793,3013],{"class":3012},[3002,10795,10797,10799,10802],{"class":3004,"line":10796},259,[3002,10798,6913],{"class":3012},[3002,10800,10801],{"class":3022}," findByLastName_shouldBeCaseInsensitive_whenSearching",[3002,10803,3026],{"class":3008},[3002,10805,10807],{"class":3004,"line":10806},260,[3002,10808,8468],{"class":3032},[3002,10810,10812,10814,10816,10818,10820,10822,10824,10826,10828,10830,10832],{"class":3004,"line":10811},261,[3002,10813,8537],{"class":3042},[3002,10815,3058],{"class":3008},[3002,10817,3276],{"class":3022},[3002,10819,3052],{"class":3008},[3002,10821,3219],{"class":3218},[3002,10823,3241],{"class":3022},[3002,10825,3052],{"class":3008},[3002,10827,5828],{"class":3246},[3002,10829,2975],{"class":3008},[3002,10831,5833],{"class":3246},[3002,10833,4129],{"class":3008},[3002,10835,10837],{"class":3004,"line":10836},262,[3002,10838,3153],{"emptyLinePlaceholder":3152},[3002,10840,10842],{"class":3004,"line":10841},263,[3002,10843,8532],{"class":3032},[3002,10845,10847,10849,10851,10853,10855,10858,10860,10862,10864,10866,10868,10871],{"class":3004,"line":10846},264,[3002,10848,9573],{"class":3012},[3002,10850,4165],{"class":3008},[3002,10852,4168],{"class":3012},[3002,10854,4171],{"class":3008},[3002,10856,10857],{"class":3042},"result1",[3002,10859,3046],{"class":3008},[3002,10861,8596],{"class":3042},[3002,10863,3058],{"class":3008},[3002,10865,10702],{"class":3022},[3002,10867,3052],{"class":3008},[3002,10869,10870],{"class":3246},"\"шевч\"",[3002,10872,3064],{"class":3008},[3002,10874,10876,10878,10880,10882,10884,10887,10889,10891,10893,10895,10897,10900],{"class":3004,"line":10875},265,[3002,10877,9573],{"class":3012},[3002,10879,4165],{"class":3008},[3002,10881,4168],{"class":3012},[3002,10883,4171],{"class":3008},[3002,10885,10886],{"class":3042},"result2",[3002,10888,3046],{"class":3008},[3002,10890,8596],{"class":3042},[3002,10892,3058],{"class":3008},[3002,10894,10702],{"class":3022},[3002,10896,3052],{"class":3008},[3002,10898,10899],{"class":3246},"\"ШЕВЧ\"",[3002,10901,3064],{"class":3008},[3002,10903,10905,10907,10909,10911,10913,10916,10918,10920,10922,10924,10926,10929],{"class":3004,"line":10904},266,[3002,10906,9573],{"class":3012},[3002,10908,4165],{"class":3008},[3002,10910,4168],{"class":3012},[3002,10912,4171],{"class":3008},[3002,10914,10915],{"class":3042},"result3",[3002,10917,3046],{"class":3008},[3002,10919,8596],{"class":3042},[3002,10921,3058],{"class":3008},[3002,10923,10702],{"class":3022},[3002,10925,3052],{"class":3008},[3002,10927,10928],{"class":3246},"\"ШеВч\"",[3002,10930,3064],{"class":3008},[3002,10932,10934],{"class":3004,"line":10933},267,[3002,10935,3153],{"emptyLinePlaceholder":3152},[3002,10937,10939],{"class":3004,"line":10938},268,[3002,10940,8552],{"class":3032},[3002,10942,10944,10946,10949,10951,10953,10955],{"class":3004,"line":10943},269,[3002,10945,8562],{"class":3022},[3002,10947,10948],{"class":3008},"(result1).",[3002,10950,4200],{"class":3022},[3002,10952,3052],{"class":3008},[3002,10954,3144],{"class":3143},[3002,10956,3064],{"class":3008},[3002,10958,10960,10962,10965,10967,10969,10971],{"class":3004,"line":10959},270,[3002,10961,8562],{"class":3022},[3002,10963,10964],{"class":3008},"(result2).",[3002,10966,4200],{"class":3022},[3002,10968,3052],{"class":3008},[3002,10970,3144],{"class":3143},[3002,10972,3064],{"class":3008},[3002,10974,10976,10978,10981,10983,10985,10987],{"class":3004,"line":10975},271,[3002,10977,8562],{"class":3022},[3002,10979,10980],{"class":3008},"(result3).",[3002,10982,4200],{"class":3022},[3002,10984,3052],{"class":3008},[3002,10986,3144],{"class":3143},[3002,10988,3064],{"class":3008},[3002,10990,10992],{"class":3004,"line":10991},272,[3002,10993,7076],{"class":3008},[3002,10995,10997],{"class":3004,"line":10996},273,[3002,10998,3153],{"emptyLinePlaceholder":3152},[3002,11000,11002,11004],{"class":3004,"line":11001},274,[3002,11003,6905],{"class":3008},[3002,11005,3013],{"class":3012},[3002,11007,11009,11011,11014],{"class":3004,"line":11008},275,[3002,11010,6913],{"class":3012},[3002,11012,11013],{"class":3022}," findByLastName_shouldReturnEmptyList_whenNoMatches",[3002,11015,3026],{"class":3008},[3002,11017,11019],{"class":3004,"line":11018},276,[3002,11020,8468],{"class":3032},[3002,11022,11024,11026,11028,11030,11032,11034,11036,11038,11040,11042,11044],{"class":3004,"line":11023},277,[3002,11025,8537],{"class":3042},[3002,11027,3058],{"class":3008},[3002,11029,3276],{"class":3022},[3002,11031,3052],{"class":3008},[3002,11033,3219],{"class":3218},[3002,11035,3241],{"class":3022},[3002,11037,3052],{"class":3008},[3002,11039,5828],{"class":3246},[3002,11041,2975],{"class":3008},[3002,11043,5833],{"class":3246},[3002,11045,4129],{"class":3008},[3002,11047,11049],{"class":3004,"line":11048},278,[3002,11050,3153],{"emptyLinePlaceholder":3152},[3002,11052,11054],{"class":3004,"line":11053},279,[3002,11055,8532],{"class":3032},[3002,11057,11059,11061,11063,11065,11067,11069,11071,11073,11075,11077,11079,11082],{"class":3004,"line":11058},280,[3002,11060,9573],{"class":3012},[3002,11062,4165],{"class":3008},[3002,11064,4168],{"class":3012},[3002,11066,4171],{"class":3008},[3002,11068,9235],{"class":3042},[3002,11070,3046],{"class":3008},[3002,11072,8596],{"class":3042},[3002,11074,3058],{"class":3008},[3002,11076,10702],{"class":3022},[3002,11078,3052],{"class":3008},[3002,11080,11081],{"class":3246},"\"Неіснуюче\"",[3002,11083,3064],{"class":3008},[3002,11085,11087],{"class":3004,"line":11086},281,[3002,11088,3153],{"emptyLinePlaceholder":3152},[3002,11090,11092],{"class":3004,"line":11091},282,[3002,11093,8552],{"class":3032},[3002,11095,11097,11099,11101,11103],{"class":3004,"line":11096},283,[3002,11098,8562],{"class":3022},[3002,11100,9261],{"class":3008},[3002,11102,4471],{"class":3022},[3002,11104,3304],{"class":3008},[3002,11106,11108],{"class":3004,"line":11107},284,[3002,11109,7076],{"class":3008},[3002,11111,11113],{"class":3004,"line":11112},285,[3002,11114,3153],{"emptyLinePlaceholder":3152},[3002,11116,11118],{"class":3004,"line":11117},286,[3002,11119,7168],{"class":3032},[3002,11121,11123],{"class":3004,"line":11122},287,[3002,11124,11125],{"class":3032},"    // count() та existsById() — Допоміжні методи\n",[3002,11127,11129],{"class":3004,"line":11128},288,[3002,11130,7168],{"class":3032},[3002,11132,11134],{"class":3004,"line":11133},289,[3002,11135,3153],{"emptyLinePlaceholder":3152},[3002,11137,11139,11141],{"class":3004,"line":11138},290,[3002,11140,6905],{"class":3008},[3002,11142,3013],{"class":3012},[3002,11144,11146,11148,11151],{"class":3004,"line":11145},291,[3002,11147,6913],{"class":3012},[3002,11149,11150],{"class":3022}," count_shouldReturnCorrectNumber_whenMultipleAuthorsExist",[3002,11152,3026],{"class":3008},[3002,11154,11156],{"class":3004,"line":11155},292,[3002,11157,8468],{"class":3032},[3002,11159,11161,11163,11165,11167,11169,11171,11173,11175,11177,11179,11182],{"class":3004,"line":11160},293,[3002,11162,8537],{"class":3042},[3002,11164,3058],{"class":3008},[3002,11166,3276],{"class":3022},[3002,11168,3052],{"class":3008},[3002,11170,3219],{"class":3218},[3002,11172,3241],{"class":3022},[3002,11174,3052],{"class":3008},[3002,11176,10112],{"class":3246},[3002,11178,2975],{"class":3008},[3002,11180,11181],{"class":3246},"\"1\"",[3002,11183,4129],{"class":3008},[3002,11185,11187,11189,11191,11193,11195,11197,11199,11201,11203,11205,11208],{"class":3004,"line":11186},294,[3002,11188,8537],{"class":3042},[3002,11190,3058],{"class":3008},[3002,11192,3276],{"class":3022},[3002,11194,3052],{"class":3008},[3002,11196,3219],{"class":3218},[3002,11198,3241],{"class":3022},[3002,11200,3052],{"class":3008},[3002,11202,10112],{"class":3246},[3002,11204,2975],{"class":3008},[3002,11206,11207],{"class":3246},"\"2\"",[3002,11209,4129],{"class":3008},[3002,11211,11213,11215,11217,11219,11221,11223,11225,11227,11229,11231,11234],{"class":3004,"line":11212},295,[3002,11214,8537],{"class":3042},[3002,11216,3058],{"class":3008},[3002,11218,3276],{"class":3022},[3002,11220,3052],{"class":3008},[3002,11222,3219],{"class":3218},[3002,11224,3241],{"class":3022},[3002,11226,3052],{"class":3008},[3002,11228,10112],{"class":3246},[3002,11230,2975],{"class":3008},[3002,11232,11233],{"class":3246},"\"3\"",[3002,11235,4129],{"class":3008},[3002,11237,11239],{"class":3004,"line":11238},296,[3002,11240,3153],{"emptyLinePlaceholder":3152},[3002,11242,11244],{"class":3004,"line":11243},297,[3002,11245,8532],{"class":3032},[3002,11247,11249,11252,11255,11257,11259,11261,11264],{"class":3004,"line":11248},298,[3002,11250,11251],{"class":3012},"        long",[3002,11253,11254],{"class":3042}," count",[3002,11256,3046],{"class":3008},[3002,11258,8596],{"class":3042},[3002,11260,3058],{"class":3008},[3002,11262,11263],{"class":3022},"count",[3002,11265,3304],{"class":3008},[3002,11267,11269],{"class":3004,"line":11268},299,[3002,11270,3153],{"emptyLinePlaceholder":3152},[3002,11272,11274],{"class":3004,"line":11273},300,[3002,11275,8552],{"class":3032},[3002,11277,11279,11281,11284,11286,11288,11290],{"class":3004,"line":11278},301,[3002,11280,8562],{"class":3022},[3002,11282,11283],{"class":3008},"(count).",[3002,11285,3577],{"class":3022},[3002,11287,3052],{"class":3008},[3002,11289,9612],{"class":3143},[3002,11291,3064],{"class":3008},[3002,11293,11295],{"class":3004,"line":11294},302,[3002,11296,7076],{"class":3008},[3002,11298,11300],{"class":3004,"line":11299},303,[3002,11301,3153],{"emptyLinePlaceholder":3152},[3002,11303,11305,11307],{"class":3004,"line":11304},304,[3002,11306,6905],{"class":3008},[3002,11308,3013],{"class":3012},[3002,11310,11312,11314,11317],{"class":3004,"line":11311},305,[3002,11313,6913],{"class":3012},[3002,11315,11316],{"class":3022}," existsById_shouldReturnTrue_whenAuthorExists",[3002,11318,3026],{"class":3008},[3002,11320,11322],{"class":3004,"line":11321},306,[3002,11323,8468],{"class":3032},[3002,11325,11327,11329,11331,11333,11335,11337,11339,11341,11343,11345],{"class":3004,"line":11326},307,[3002,11328,8473],{"class":3012},[3002,11330,3234],{"class":3042},[3002,11332,3046],{"class":3008},[3002,11334,3219],{"class":3218},[3002,11336,3241],{"class":3022},[3002,11338,3052],{"class":3008},[3002,11340,3247],{"class":3246},[3002,11342,2975],{"class":3008},[3002,11344,3252],{"class":3246},[3002,11346,3064],{"class":3008},[3002,11348,11350,11352,11354,11356],{"class":3004,"line":11349},308,[3002,11351,8537],{"class":3042},[3002,11353,3058],{"class":3008},[3002,11355,3276],{"class":3022},[3002,11357,3279],{"class":3008},[3002,11359,11361],{"class":3004,"line":11360},309,[3002,11362,3153],{"emptyLinePlaceholder":3152},[3002,11364,11366],{"class":3004,"line":11365},310,[3002,11367,8532],{"class":3032},[3002,11369,11371,11373,11376,11378,11380,11382,11385,11387,11389,11391,11393],{"class":3004,"line":11370},311,[3002,11372,10337],{"class":3012},[3002,11374,11375],{"class":3042}," exists",[3002,11377,3046],{"class":3008},[3002,11379,8596],{"class":3042},[3002,11381,3058],{"class":3008},[3002,11383,11384],{"class":3022},"existsById",[3002,11386,3052],{"class":3008},[3002,11388,3545],{"class":3042},[3002,11390,3058],{"class":3008},[3002,11392,3550],{"class":3022},[3002,11394,11395],{"class":3008},"());\n",[3002,11397,11399],{"class":3004,"line":11398},312,[3002,11400,3153],{"emptyLinePlaceholder":3152},[3002,11402,11404],{"class":3004,"line":11403},313,[3002,11405,8552],{"class":3032},[3002,11407,11409,11411,11414,11416],{"class":3004,"line":11408},314,[3002,11410,8562],{"class":3022},[3002,11412,11413],{"class":3008},"(exists).",[3002,11415,10372],{"class":3022},[3002,11417,3304],{"class":3008},[3002,11419,11421],{"class":3004,"line":11420},315,[3002,11422,7076],{"class":3008},[3002,11424,11426],{"class":3004,"line":11425},316,[3002,11427,3153],{"emptyLinePlaceholder":3152},[3002,11429,11431,11433],{"class":3004,"line":11430},317,[3002,11432,6905],{"class":3008},[3002,11434,3013],{"class":3012},[3002,11436,11438,11440,11443],{"class":3004,"line":11437},318,[3002,11439,6913],{"class":3012},[3002,11441,11442],{"class":3022}," existsById_shouldReturnFalse_whenAuthorNotExists",[3002,11444,3026],{"class":3008},[3002,11446,11448],{"class":3004,"line":11447},319,[3002,11449,8468],{"class":3032},[3002,11451,11453,11455,11457,11459,11461,11463,11465],{"class":3004,"line":11452},320,[3002,11454,8915],{"class":3012},[3002,11456,9353],{"class":3042},[3002,11458,3046],{"class":3008},[3002,11460,3875],{"class":3042},[3002,11462,3058],{"class":3008},[3002,11464,3880],{"class":3022},[3002,11466,3304],{"class":3008},[3002,11468,11470],{"class":3004,"line":11469},321,[3002,11471,3153],{"emptyLinePlaceholder":3152},[3002,11473,11475],{"class":3004,"line":11474},322,[3002,11476,8532],{"class":3032},[3002,11478,11480,11482,11484,11486,11488,11490,11492],{"class":3004,"line":11479},323,[3002,11481,10337],{"class":3012},[3002,11483,11375],{"class":3042},[3002,11485,3046],{"class":3008},[3002,11487,8596],{"class":3042},[3002,11489,3058],{"class":3008},[3002,11491,11384],{"class":3022},[3002,11493,9394],{"class":3008},[3002,11495,11497],{"class":3004,"line":11496},324,[3002,11498,3153],{"emptyLinePlaceholder":3152},[3002,11500,11502],{"class":3004,"line":11501},325,[3002,11503,8552],{"class":3032},[3002,11505,11507,11509,11511,11513],{"class":3004,"line":11506},326,[3002,11508,8562],{"class":3022},[3002,11510,11413],{"class":3008},[3002,11512,10513],{"class":3022},[3002,11514,3304],{"class":3008},[3002,11516,11518],{"class":3004,"line":11517},327,[3002,11519,7076],{"class":3008},[3002,11521,11523],{"class":3004,"line":11522},328,[3002,11524,3310],{"class":3008},[2964,11526,11527],{},[2989,11528,11529],{},"Спостереження:",[3332,11531,11532,11540,11552,11565],{},[3335,11533,11534,5534,11537,11539],{},[2989,11535,11536],{},"Рядки 36–60",[2968,11538,6351],{},"): Найповніший тест — перевіряє ID, всі поля та кількість рядків у таблиці.",[3335,11541,11542,5534,11545,11547,11548,11551],{},[2989,11543,11544],{},"Рядки 92–102",[2968,11546,6389],{},"): Тест негативного сценарію — використовує ",[2968,11549,11550],{},"assertThatThrownBy()"," з AssertJ.",[3335,11553,11554,5534,11557,11560,11561,11564],{},[2989,11555,11556],{},"Рядки 165–177",[2968,11558,11559],{},"findAll_shouldReturnAllAuthors_whenMultipleExist","): Використовує ",[2968,11562,11563],{},"extracting()"," для перевірки списку — читабельніше за цикл.",[3335,11566,11567,5534,11570,11573],{},[2989,11568,11569],{},"Рядки 268–283",[2968,11571,11572],{},"findByLastName_shouldBeCaseInsensitive_whenSearching","): Перевіряє, що пошук не залежить від регістру.",[3768,11575,11576,11581,11667,11670],{},[2964,11577,11578],{},[2989,11579,11580],{},"AssertJ vs JUnit Assertions:",[2993,11582,11584],{"className":2995,"code":11583,"language":2997,"meta":2998,"style":2998},"// JUnit (старий стиль)\nassertEquals(\"Іван\", author.getFirstName());\nassertTrue(result.isPresent());\n\n// AssertJ (fluent, читабельніший)\nassertThat(author.getFirstName()).isEqualTo(\"Іван\");\nassertThat(result).isPresent();\n",[2968,11585,11586,11591,11610,11625,11629,11634,11657],{"__ignoreMap":2998},[3002,11587,11588],{"class":3004,"line":3005},[3002,11589,11590],{"class":3032},"// JUnit (старий стиль)\n",[3002,11592,11593,11596,11598,11600,11602,11604,11606,11608],{"class":3004,"line":3016},[3002,11594,11595],{"class":3022},"assertEquals",[3002,11597,3052],{"class":3008},[3002,11599,3247],{"class":3246},[3002,11601,2975],{"class":3008},[3002,11603,3545],{"class":3042},[3002,11605,3058],{"class":3008},[3002,11607,3572],{"class":3022},[3002,11609,11395],{"class":3008},[3002,11611,11612,11615,11617,11619,11621,11623],{"class":3004,"line":3029},[3002,11613,11614],{"class":3022},"assertTrue",[3002,11616,3052],{"class":3008},[3002,11618,9235],{"class":3042},[3002,11620,3058],{"class":3008},[3002,11622,9264],{"class":3022},[3002,11624,11395],{"class":3008},[3002,11626,11627],{"class":3004,"line":3036},[3002,11628,3153],{"emptyLinePlaceholder":3152},[3002,11630,11631],{"class":3004,"line":3067},[3002,11632,11633],{"class":3032},"// AssertJ (fluent, читабельніший)\n",[3002,11635,11636,11639,11641,11643,11645,11647,11649,11651,11653,11655],{"class":3004,"line":3091},[3002,11637,11638],{"class":3022},"assertThat",[3002,11640,3052],{"class":3008},[3002,11642,3545],{"class":3042},[3002,11644,3058],{"class":3008},[3002,11646,3572],{"class":3022},[3002,11648,3136],{"class":3008},[3002,11650,3577],{"class":3022},[3002,11652,3052],{"class":3008},[3002,11654,3247],{"class":3246},[3002,11656,3064],{"class":3008},[3002,11658,11659,11661,11663,11665],{"class":3004,"line":3121},[3002,11660,11638],{"class":3022},[3002,11662,9261],{"class":3008},[3002,11664,9264],{"class":3022},[3002,11666,3304],{"class":3008},[2964,11668,11669],{},"AssertJ надає кращі повідомлення про помилки:",[2993,11671,11674],{"className":11672,"code":11673,"language":3781},[3779],"JUnit:  expected: \u003CІван> but was: \u003CIvan>\nAssertJ: Expecting firstName to be \"Іван\" but was \"Ivan\"\n",[2968,11675,11673],{"__ignoreMap":2998},[3660,11677],{},[2959,11679,11681],{"id":11680},"тестування-складних-сценаріїв-constraints-та-каскадне-видалення","Тестування складних сценаріїв: Constraints та каскадне видалення",[2964,11683,11684,11685,11688],{},"Інтеграційні тести особливо важливі для перевірки ",[2989,11686,11687],{},"обмежень БД"," (constraints), оскільки mock-тести не можуть їх емулювати.",[3810,11690,11692],{"id":11691},"тестування-unique-constraint","Тестування UNIQUE constraint",[2993,11694,11696],{"className":2995,"code":11695,"language":2997,"meta":4616,"style":2998},"package com.example.audiobook.repository;\n\nimport com.example.audiobook.domain.Genre;\nimport com.example.audiobook.repository.jdbc.JdbcGenreRepository;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport static org.assertj.core.api.Assertions.*;\n\n/**\n * Інтеграційні тести для {@link JdbcGenreRepository}.\n */\nclass JdbcGenreRepositoryTest extends AbstractRepositoryTest {\n\n    private GenreRepository repository;\n\n    @BeforeEach\n    void setUpRepository() {\n        repository = new JdbcGenreRepository(connectionManager);\n    }\n\n    @Test\n    void save_shouldInsertNewGenre_whenValidData() {\n        // ═══ Arrange ═══\n        Genre genre = new Genre(\"Фантастика\");\n        genre.setDescription(\"Науково-фантастичні твори\");\n\n        // ═══ Act ═══\n        repository.save(genre);\n\n        // ═══ Assert ═══\n        Genre loaded = repository.findById(genre.getId()).orElseThrow();\n        assertThat(loaded.getName()).isEqualTo(\"Фантастика\");\n        assertThat(loaded.getDescription()).isEqualTo(\"Науково-фантастичні твори\");\n    }\n\n    @Test\n    void save_shouldThrowException_whenDuplicateName() {\n        // ═══ Arrange ═══\n        Genre genre1 = new Genre(\"Проза\");\n        Genre genre2 = new Genre(\"Проза\"); // та сама назва!\n\n        repository.save(genre1); // перший збережено успішно\n\n        // ═══ Act & Assert ═══\n        // UNIQUE constraint на genres.name має запобігти вставці\n        assertThatThrownBy(() -> repository.save(genre2))\n            .isInstanceOf(com.example.audiobook.db.DatabaseException.class)\n            .hasMessageContaining(\"UNIQUE\"); // H2 повідомляє про порушення UNIQUE\n    }\n\n    @Test\n    void findByName_shouldReturnGenre_whenExactMatch() {\n        // ═══ Arrange ═══\n        Genre genre = new Genre(\"Детектив\");\n        repository.save(genre);\n\n        // ═══ Act ═══\n        var result = repository.findByName(\"Детектив\");\n\n        // ═══ Assert ═══\n        assertThat(result).isPresent();\n        assertThat(result.get().getName()).isEqualTo(\"Детектив\");\n    }\n\n    @Test\n    void findByName_shouldReturnEmpty_whenNoMatch() {\n        // ═══ Arrange ═══\n        repository.save(new Genre(\"Проза\"));\n\n        // ═══ Act ═══\n        var result = repository.findByName(\"Поезія\");\n\n        // ═══ Assert ═══\n        assertThat(result).isEmpty();\n    }\n}\n",[2968,11697,11698,11704,11708,11715,11722,11728,11734,11738,11746,11750,11754,11759,11763,11776,11780,11791,11795,11801,11809,11820,11824,11828,11834,11843,11847,11869,11886,11890,11894,11905,11909,11913,11942,11965,11988,11992,11996,12002,12011,12015,12035,12057,12061,12074,12078,12082,12087,12104,12136,12152,12156,12160,12166,12175,12179,12198,12208,12212,12216,12239,12243,12247,12257,12283,12287,12291,12297,12306,12310,12330,12334,12338,12359,12363,12367,12377,12381],{"__ignoreMap":2998},[3002,11699,11700,11702],{"class":3004,"line":3005},[3002,11701,6550],{"class":4021},[3002,11703,6553],{"class":3008},[3002,11705,11706],{"class":3004,"line":3016},[3002,11707,3153],{"emptyLinePlaceholder":3152},[3002,11709,11710,11712],{"class":3004,"line":3029},[3002,11711,6562],{"class":4021},[3002,11713,11714],{"class":3008}," com.example.audiobook.domain.Genre;\n",[3002,11716,11717,11719],{"class":3004,"line":3036},[3002,11718,6562],{"class":4021},[3002,11720,11721],{"class":3008}," com.example.audiobook.repository.jdbc.JdbcGenreRepository;\n",[3002,11723,11724,11726],{"class":3004,"line":3067},[3002,11725,6562],{"class":4021},[3002,11727,6579],{"class":3008},[3002,11729,11730,11732],{"class":3004,"line":3091},[3002,11731,6562],{"class":4021},[3002,11733,8284],{"class":3008},[3002,11735,11736],{"class":3004,"line":3121},[3002,11737,3153],{"emptyLinePlaceholder":3152},[3002,11739,11740,11742,11744],{"class":3004,"line":3149},[3002,11741,6562],{"class":4021},[3002,11743,8319],{"class":4021},[3002,11745,8322],{"class":3008},[3002,11747,11748],{"class":3004,"line":3156},[3002,11749,3153],{"emptyLinePlaceholder":3152},[3002,11751,11752],{"class":3004,"line":3180},[3002,11753,6655],{"class":3032},[3002,11755,11756],{"class":3004,"line":3202},[3002,11757,11758],{"class":3032}," * Інтеграційні тести для {@link JdbcGenreRepository}.\n",[3002,11760,11761],{"class":3004,"line":3207},[3002,11762,6799],{"class":3032},[3002,11764,11765,11767,11770,11772,11774],{"class":3004,"line":3228},[3002,11766,3061],{"class":4021},[3002,11768,11769],{"class":3012}," JdbcGenreRepositoryTest",[3002,11771,8368],{"class":4021},[3002,11773,6814],{"class":3012},[3002,11775,6817],{"class":3008},[3002,11777,11778],{"class":3004,"line":3257},[3002,11779,3153],{"emptyLinePlaceholder":3152},[3002,11781,11782,11784,11787,11789],{"class":3004,"line":3262},[3002,11783,8381],{"class":4021},[3002,11785,11786],{"class":3012}," GenreRepository",[3002,11788,8387],{"class":3042},[3002,11790,3889],{"class":3008},[3002,11792,11793],{"class":3004,"line":3268},[3002,11794,3153],{"emptyLinePlaceholder":3152},[3002,11796,11797,11799],{"class":3004,"line":3282},[3002,11798,6905],{"class":3008},[3002,11800,4250],{"class":3012},[3002,11802,11803,11805,11807],{"class":3004,"line":3287},[3002,11804,6913],{"class":3012},[3002,11806,8406],{"class":3022},[3002,11808,3026],{"class":3008},[3002,11810,11811,11813,11815,11818],{"class":3004,"line":3293},[3002,11812,8418],{"class":3008},[3002,11814,3219],{"class":3218},[3002,11816,11817],{"class":3022}," JdbcGenreRepository",[3002,11819,5811],{"class":3008},[3002,11821,11822],{"class":3004,"line":3307},[3002,11823,7076],{"class":3008},[3002,11825,11826],{"class":3004,"line":4214},[3002,11827,3153],{"emptyLinePlaceholder":3152},[3002,11829,11830,11832],{"class":3004,"line":4409},[3002,11831,6905],{"class":3008},[3002,11833,3013],{"class":3012},[3002,11835,11836,11838,11841],{"class":3004,"line":4415},[3002,11837,6913],{"class":3012},[3002,11839,11840],{"class":3022}," save_shouldInsertNewGenre_whenValidData",[3002,11842,3026],{"class":3008},[3002,11844,11845],{"class":3004,"line":4420},[3002,11846,8468],{"class":3032},[3002,11848,11849,11852,11855,11857,11859,11862,11864,11867],{"class":3004,"line":4425},[3002,11850,11851],{"class":3012},"        Genre",[3002,11853,11854],{"class":3042}," genre",[3002,11856,3046],{"class":3008},[3002,11858,3219],{"class":3218},[3002,11860,11861],{"class":3022}," Genre",[3002,11863,3052],{"class":3008},[3002,11865,11866],{"class":3246},"\"Фантастика\"",[3002,11868,3064],{"class":3008},[3002,11870,11871,11874,11876,11879,11881,11884],{"class":3004,"line":4432},[3002,11872,11873],{"class":3042},"        genre",[3002,11875,3058],{"class":3008},[3002,11877,11878],{"class":3022},"setDescription",[3002,11880,3052],{"class":3008},[3002,11882,11883],{"class":3246},"\"Науково-фантастичні твори\"",[3002,11885,3064],{"class":3008},[3002,11887,11888],{"class":3004,"line":4441},[3002,11889,3153],{"emptyLinePlaceholder":3152},[3002,11891,11892],{"class":3004,"line":4464},[3002,11893,8532],{"class":3032},[3002,11895,11896,11898,11900,11902],{"class":3004,"line":4479},[3002,11897,8537],{"class":3042},[3002,11899,3058],{"class":3008},[3002,11901,3276],{"class":3022},[3002,11903,11904],{"class":3008},"(genre);\n",[3002,11906,11907],{"class":3004,"line":4954},[3002,11908,3153],{"emptyLinePlaceholder":3152},[3002,11910,11911],{"class":3004,"line":4972},[3002,11912,8552],{"class":3032},[3002,11914,11915,11917,11919,11921,11923,11925,11927,11929,11932,11934,11936,11938,11940],{"class":3004,"line":4990},[3002,11916,11851],{"class":3012},[3002,11918,3530],{"class":3042},[3002,11920,3046],{"class":3008},[3002,11922,8596],{"class":3042},[3002,11924,3058],{"class":3008},[3002,11926,3540],{"class":3022},[3002,11928,3052],{"class":3008},[3002,11930,11931],{"class":3042},"genre",[3002,11933,3058],{"class":3008},[3002,11935,3550],{"class":3022},[3002,11937,3136],{"class":3008},[3002,11939,3555],{"class":3022},[3002,11941,3304],{"class":3008},[3002,11943,11944,11946,11948,11950,11952,11955,11957,11959,11961,11963],{"class":3004,"line":5007},[3002,11945,8562],{"class":3022},[3002,11947,3052],{"class":3008},[3002,11949,3567],{"class":3042},[3002,11951,3058],{"class":3008},[3002,11953,11954],{"class":3022},"getName",[3002,11956,3136],{"class":3008},[3002,11958,3577],{"class":3022},[3002,11960,3052],{"class":3008},[3002,11962,11866],{"class":3246},[3002,11964,3064],{"class":3008},[3002,11966,11967,11969,11971,11973,11975,11978,11980,11982,11984,11986],{"class":3004,"line":5016},[3002,11968,8562],{"class":3022},[3002,11970,3052],{"class":3008},[3002,11972,3567],{"class":3042},[3002,11974,3058],{"class":3008},[3002,11976,11977],{"class":3022},"getDescription",[3002,11979,3136],{"class":3008},[3002,11981,3577],{"class":3022},[3002,11983,3052],{"class":3008},[3002,11985,11883],{"class":3246},[3002,11987,3064],{"class":3008},[3002,11989,11990],{"class":3004,"line":5487},[3002,11991,7076],{"class":3008},[3002,11993,11994],{"class":3004,"line":5492},[3002,11995,3153],{"emptyLinePlaceholder":3152},[3002,11997,11998,12000],{"class":3004,"line":5504},[3002,11999,6905],{"class":3008},[3002,12001,3013],{"class":3012},[3002,12003,12004,12006,12009],{"class":3004,"line":5514},[3002,12005,6913],{"class":3012},[3002,12007,12008],{"class":3022}," save_shouldThrowException_whenDuplicateName",[3002,12010,3026],{"class":3008},[3002,12012,12013],{"class":3004,"line":5552},[3002,12014,8468],{"class":3032},[3002,12016,12017,12019,12022,12024,12026,12028,12030,12033],{"class":3004,"line":5571},[3002,12018,11851],{"class":3012},[3002,12020,12021],{"class":3042}," genre1",[3002,12023,3046],{"class":3008},[3002,12025,3219],{"class":3218},[3002,12027,11861],{"class":3022},[3002,12029,3052],{"class":3008},[3002,12031,12032],{"class":3246},"\"Проза\"",[3002,12034,3064],{"class":3008},[3002,12036,12037,12039,12042,12044,12046,12048,12050,12052,12054],{"class":3004,"line":5586},[3002,12038,11851],{"class":3012},[3002,12040,12041],{"class":3042}," genre2",[3002,12043,3046],{"class":3008},[3002,12045,3219],{"class":3218},[3002,12047,11861],{"class":3022},[3002,12049,3052],{"class":3008},[3002,12051,12032],{"class":3246},[3002,12053,4208],{"class":3008},[3002,12055,12056],{"class":3032},"// та сама назва!\n",[3002,12058,12059],{"class":3004,"line":5600},[3002,12060,3153],{"emptyLinePlaceholder":3152},[3002,12062,12063,12065,12067,12069,12072],{"class":3004,"line":5605},[3002,12064,8537],{"class":3042},[3002,12066,3058],{"class":3008},[3002,12068,3276],{"class":3022},[3002,12070,12071],{"class":3008},"(genre1); ",[3002,12073,9028],{"class":3032},[3002,12075,12076],{"class":3004,"line":5610},[3002,12077,3153],{"emptyLinePlaceholder":3152},[3002,12079,12080],{"class":3004,"line":6796},[3002,12081,9037],{"class":3032},[3002,12083,12084],{"class":3004,"line":6802},[3002,12085,12086],{"class":3032},"        // UNIQUE constraint на genres.name має запобігти вставці\n",[3002,12088,12089,12091,12093,12095,12097,12099,12101],{"class":3004,"line":6820},[3002,12090,9047],{"class":3022},[3002,12092,9050],{"class":3008},[3002,12094,9053],{"class":4021},[3002,12096,8387],{"class":3042},[3002,12098,3058],{"class":3008},[3002,12100,3276],{"class":3022},[3002,12102,12103],{"class":3008},"(genre2))\n",[3002,12105,12106,12108,12110,12112,12114,12116,12118,12120,12122,12124,12126,12128,12130,12132,12134],{"class":3004,"line":6825},[3002,12107,9067],{"class":3008},[3002,12109,9070],{"class":3022},[3002,12111,3052],{"class":3008},[3002,12113,9075],{"class":3042},[3002,12115,3058],{"class":3008},[3002,12117,9080],{"class":3042},[3002,12119,3058],{"class":3008},[3002,12121,9085],{"class":3042},[3002,12123,3058],{"class":3008},[3002,12125,9090],{"class":3042},[3002,12127,3058],{"class":3008},[3002,12129,9095],{"class":3042},[3002,12131,3058],{"class":3008},[3002,12133,3061],{"class":3042},[3002,12135,5187],{"class":3008},[3002,12137,12138,12140,12142,12144,12147,12149],{"class":3004,"line":6831},[3002,12139,9067],{"class":3008},[3002,12141,9108],{"class":3022},[3002,12143,3052],{"class":3008},[3002,12145,12146],{"class":3246},"\"UNIQUE\"",[3002,12148,4208],{"class":3008},[3002,12150,12151],{"class":3032},"// H2 повідомляє про порушення UNIQUE\n",[3002,12153,12154],{"class":3004,"line":6837},[3002,12155,7076],{"class":3008},[3002,12157,12158],{"class":3004,"line":6843},[3002,12159,3153],{"emptyLinePlaceholder":3152},[3002,12161,12162,12164],{"class":3004,"line":6849},[3002,12163,6905],{"class":3008},[3002,12165,3013],{"class":3012},[3002,12167,12168,12170,12173],{"class":3004,"line":6863},[3002,12169,6913],{"class":3012},[3002,12171,12172],{"class":3022}," findByName_shouldReturnGenre_whenExactMatch",[3002,12174,3026],{"class":3008},[3002,12176,12177],{"class":3004,"line":6868},[3002,12178,8468],{"class":3032},[3002,12180,12181,12183,12185,12187,12189,12191,12193,12196],{"class":3004,"line":6873},[3002,12182,11851],{"class":3012},[3002,12184,11854],{"class":3042},[3002,12186,3046],{"class":3008},[3002,12188,3219],{"class":3218},[3002,12190,11861],{"class":3022},[3002,12192,3052],{"class":3008},[3002,12194,12195],{"class":3246},"\"Детектив\"",[3002,12197,3064],{"class":3008},[3002,12199,12200,12202,12204,12206],{"class":3004,"line":6879},[3002,12201,8537],{"class":3042},[3002,12203,3058],{"class":3008},[3002,12205,3276],{"class":3022},[3002,12207,11904],{"class":3008},[3002,12209,12210],{"class":3004,"line":6885},[3002,12211,3153],{"emptyLinePlaceholder":3152},[3002,12213,12214],{"class":3004,"line":6891},[3002,12215,8532],{"class":3032},[3002,12217,12218,12221,12224,12226,12228,12230,12233,12235,12237],{"class":3004,"line":6897},[3002,12219,12220],{"class":4021},"        var",[3002,12222,12223],{"class":3042}," result",[3002,12225,3046],{"class":3008},[3002,12227,8596],{"class":3042},[3002,12229,3058],{"class":3008},[3002,12231,12232],{"class":3022},"findByName",[3002,12234,3052],{"class":3008},[3002,12236,12195],{"class":3246},[3002,12238,3064],{"class":3008},[3002,12240,12241],{"class":3004,"line":6902},[3002,12242,3153],{"emptyLinePlaceholder":3152},[3002,12244,12245],{"class":3004,"line":6910},[3002,12246,8552],{"class":3032},[3002,12248,12249,12251,12253,12255],{"class":3004,"line":6935},[3002,12250,8562],{"class":3022},[3002,12252,9261],{"class":3008},[3002,12254,9264],{"class":3022},[3002,12256,3304],{"class":3008},[3002,12258,12259,12261,12263,12265,12267,12269,12271,12273,12275,12277,12279,12281],{"class":3004,"line":6941},[3002,12260,8562],{"class":3022},[3002,12262,3052],{"class":3008},[3002,12264,9235],{"class":3042},[3002,12266,3058],{"class":3008},[3002,12268,9279],{"class":3022},[3002,12270,6961],{"class":3008},[3002,12272,11954],{"class":3022},[3002,12274,3136],{"class":3008},[3002,12276,3577],{"class":3022},[3002,12278,3052],{"class":3008},[3002,12280,12195],{"class":3246},[3002,12282,3064],{"class":3008},[3002,12284,12285],{"class":3004,"line":6984},[3002,12286,7076],{"class":3008},[3002,12288,12289],{"class":3004,"line":6990},[3002,12290,3153],{"emptyLinePlaceholder":3152},[3002,12292,12293,12295],{"class":3004,"line":6996},[3002,12294,6905],{"class":3008},[3002,12296,3013],{"class":3012},[3002,12298,12299,12301,12304],{"class":3004,"line":7002},[3002,12300,6913],{"class":3012},[3002,12302,12303],{"class":3022}," findByName_shouldReturnEmpty_whenNoMatch",[3002,12305,3026],{"class":3008},[3002,12307,12308],{"class":3004,"line":7008},[3002,12309,8468],{"class":3032},[3002,12311,12312,12314,12316,12318,12320,12322,12324,12326,12328],{"class":3004,"line":7028},[3002,12313,8537],{"class":3042},[3002,12315,3058],{"class":3008},[3002,12317,3276],{"class":3022},[3002,12319,3052],{"class":3008},[3002,12321,3219],{"class":3218},[3002,12323,11861],{"class":3022},[3002,12325,3052],{"class":3008},[3002,12327,12032],{"class":3246},[3002,12329,4129],{"class":3008},[3002,12331,12332],{"class":3004,"line":7050},[3002,12333,3153],{"emptyLinePlaceholder":3152},[3002,12335,12336],{"class":3004,"line":7055},[3002,12337,8532],{"class":3032},[3002,12339,12340,12342,12344,12346,12348,12350,12352,12354,12357],{"class":3004,"line":7061},[3002,12341,12220],{"class":4021},[3002,12343,12223],{"class":3042},[3002,12345,3046],{"class":3008},[3002,12347,8596],{"class":3042},[3002,12349,3058],{"class":3008},[3002,12351,12232],{"class":3022},[3002,12353,3052],{"class":3008},[3002,12355,12356],{"class":3246},"\"Поезія\"",[3002,12358,3064],{"class":3008},[3002,12360,12361],{"class":3004,"line":7073},[3002,12362,3153],{"emptyLinePlaceholder":3152},[3002,12364,12365],{"class":3004,"line":7079},[3002,12366,8552],{"class":3032},[3002,12368,12369,12371,12373,12375],{"class":3004,"line":7084},[3002,12370,8562],{"class":3022},[3002,12372,9261],{"class":3008},[3002,12374,4471],{"class":3022},[3002,12376,3304],{"class":3008},[3002,12378,12379],{"class":3004,"line":7089},[3002,12380,7076],{"class":3008},[3002,12382,12383],{"class":3004,"line":7095},[3002,12384,3310],{"class":3008},[3660,12386],{},[3810,12388,12390],{"id":12389},"тестування-foreign-key-constraint-та-cascade","Тестування FOREIGN KEY constraint та CASCADE",[2993,12392,12394],{"className":2995,"code":12393,"language":2997,"meta":4616,"style":2998},"package com.example.audiobook.repository;\n\nimport com.example.audiobook.domain.Author;\nimport com.example.audiobook.domain.Audiobook;\nimport com.example.audiobook.domain.Genre;\nimport com.example.audiobook.repository.jdbc.*;\nimport org.junit.jupiter.api.BeforeEach;\nimport org.junit.jupiter.api.Test;\n\nimport java.util.List;\nimport java.util.UUID;\n\nimport static org.assertj.core.api.Assertions.*;\n\n/**\n * Інтеграційні тести для {@link JdbcAudiobookRepository}.\n * \u003Cp>\n * Особлива увага приділяється тестуванню FK constraints та каскадного видалення.\n */\nclass JdbcAudiobookRepositoryTest extends AbstractRepositoryTest {\n\n    private AudiobookRepository audiobookRepo;\n    private AuthorRepository authorRepo;\n    private GenreRepository genreRepo;\n\n    @BeforeEach\n    void setUpRepositories() {\n        audiobookRepo = new JdbcAudiobookRepository(connectionManager);\n        authorRepo = new JdbcAuthorRepository(connectionManager);\n        genreRepo = new JdbcGenreRepository(connectionManager);\n    }\n\n    @Test\n    void save_shouldInsertNewAudiobook_whenValidData() {\n        // ═══ Arrange ═══\n        // Спочатку створити автора та жанр (батьківські сутності)\n        Author author = new Author(\"Іван\", \"Франко\");\n        Genre genre = new Genre(\"Проза\");\n        authorRepo.save(author);\n        genreRepo.save(genre);\n\n        Audiobook book = new Audiobook(\"Захар Беркут\", author, genre);\n        book.setDuration(18000); // 5 годин у секундах\n        book.setReleaseYear(1883);\n        book.setDescription(\"Історична повість\");\n\n        // ═══ Act ═══\n        audiobookRepo.save(book);\n\n        // ═══ Assert ═══\n        Audiobook loaded = audiobookRepo.findById(book.getId()).orElseThrow();\n        assertThat(loaded.getTitle()).isEqualTo(\"Захар Беркут\");\n        assertThat(loaded.getDuration()).isEqualTo(18000);\n        assertThat(loaded.getReleaseYear()).isEqualTo(1883);\n        assertThat(loaded.getAuthor().getId()).isEqualTo(author.getId());\n        assertThat(loaded.getGenre().getId()).isEqualTo(genre.getId());\n    }\n\n    @Test\n    void save_shouldThrowException_whenAuthorNotExists() {\n        // ═══ Arrange ═══\n        Genre genre = new Genre(\"Проза\");\n        genreRepo.save(genre);\n\n        // Створити автора БЕЗ збереження у БД\n        Author nonExistentAuthor = new Author(\"Неіснуючий\", \"Автор\");\n        nonExistentAuthor.setId(UUID.randomUUID());\n\n        Audiobook book = new Audiobook(\"Книга\", nonExistentAuthor, genre);\n        book.setDuration(1000);\n        book.setReleaseYear(2020);\n\n        // ═══ Act & Assert ═══\n        // FK constraint має запобігти вставці (author_id не існує у authors)\n        assertThatThrownBy(() -> audiobookRepo.save(book))\n            .isInstanceOf(com.example.audiobook.db.DatabaseException.class)\n            .hasMessageContaining(\"FOREIGN KEY\"); // або \"REFERENTIAL INTEGRITY\"\n    }\n\n    @Test\n    void save_shouldThrowException_whenGenreNotExists() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Іван\", \"Франко\");\n        authorRepo.save(author);\n\n        Genre nonExistentGenre = new Genre(\"Неіснуючий жанр\");\n        nonExistentGenre.setId(UUID.randomUUID());\n\n        Audiobook book = new Audiobook(\"Книга\", author, nonExistentGenre);\n        book.setDuration(1000);\n        book.setReleaseYear(2020);\n\n        // ═══ Act & Assert ═══\n        assertThatThrownBy(() -> audiobookRepo.save(book))\n            .isInstanceOf(com.example.audiobook.db.DatabaseException.class)\n            .hasMessageContaining(\"FOREIGN KEY\");\n    }\n\n    @Test\n    void deleteAuthor_shouldCascadeDeleteAudiobooks_whenAuthorHasBooks() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Тарас\", \"Шевченко\");\n        Genre genre = new Genre(\"Поезія\");\n        authorRepo.save(author);\n        genreRepo.save(genre);\n\n        Audiobook book1 = new Audiobook(\"Кобзар\", author, genre);\n        book1.setDuration(10000);\n        book1.setReleaseYear(1840);\n\n        Audiobook book2 = new Audiobook(\"Гайдамаки\", author, genre);\n        book2.setDuration(8000);\n        book2.setReleaseYear(1841);\n\n        audiobookRepo.save(book1);\n        audiobookRepo.save(book2);\n\n        // Перевірка: у БД 2 книги\n        assertThat(countRowsInTable(\"audiobooks\")).isEqualTo(2);\n\n        // ═══ Act ═══\n        // Видалити автора → має каскадно видалити всі його книги (ON DELETE CASCADE)\n        authorRepo.deleteById(author.getId());\n\n        // ═══ Assert ═══\n        assertThat(authorRepo.findById(author.getId())).isEmpty();\n        assertThat(audiobookRepo.findById(book1.getId())).isEmpty();\n        assertThat(audiobookRepo.findById(book2.getId())).isEmpty();\n        assertThat(countRowsInTable(\"audiobooks\")).isEqualTo(0);\n    }\n\n    @Test\n    void deleteGenre_shouldCascadeDeleteAudiobooks_whenGenreHasBooks() {\n        // ═══ Arrange ═══\n        Author author = new Author(\"Леся\", \"Українка\");\n        Genre genre = new Genre(\"Драма\");\n        authorRepo.save(author);\n        genreRepo.save(genre);\n\n        Audiobook book = new Audiobook(\"Лісова пісня\", author, genre);\n        book.setDuration(7200);\n        book.setReleaseYear(1911);\n        audiobookRepo.save(book);\n\n        // ═══ Act ═══\n        genreRepo.deleteById(genre.getId());\n\n        // ═══ Assert ═══\n        assertThat(genreRepo.findById(genre.getId())).isEmpty();\n        assertThat(audiobookRepo.findById(book.getId())).isEmpty();\n    }\n}\n",[2968,12395,12396,12402,12406,12412,12419,12425,12432,12438,12444,12448,12454,12460,12464,12472,12476,12480,12485,12489,12494,12498,12511,12515,12527,12538,12549,12553,12559,12568,12580,12591,12602,12606,12610,12616,12625,12629,12634,12656,12674,12685,12696,12700,12723,12743,12759,12774,12778,12782,12794,12798,12802,12832,12855,12878,12901,12932,12963,12967,12971,12977,12986,12990,13008,13018,13022,13027,13050,13069,13073,13093,13108,13123,13127,13131,13136,13153,13185,13201,13205,13209,13215,13224,13228,13250,13260,13264,13284,13303,13307,13326,13340,13354,13358,13362,13378,13410,13422,13426,13430,13436,13445,13449,13471,13489,13499,13509,13513,13533,13549,13564,13568,13588,13604,13619,13623,13634,13645,13649,13654,13677,13681,13685,13690,13708,13712,13716,13743,13770,13797,13819,13823,13827,13833,13842,13846,13868,13887,13897,13907,13911,13930,13945,13960,13970,13974,13978,13996,14000,14004,14031,14057,14061],{"__ignoreMap":2998},[3002,12397,12398,12400],{"class":3004,"line":3005},[3002,12399,6550],{"class":4021},[3002,12401,6553],{"class":3008},[3002,12403,12404],{"class":3004,"line":3016},[3002,12405,3153],{"emptyLinePlaceholder":3152},[3002,12407,12408,12410],{"class":3004,"line":3029},[3002,12409,6562],{"class":4021},[3002,12411,8264],{"class":3008},[3002,12413,12414,12416],{"class":3004,"line":3036},[3002,12415,6562],{"class":4021},[3002,12417,12418],{"class":3008}," com.example.audiobook.domain.Audiobook;\n",[3002,12420,12421,12423],{"class":3004,"line":3067},[3002,12422,6562],{"class":4021},[3002,12424,11714],{"class":3008},[3002,12426,12427,12429],{"class":3004,"line":3091},[3002,12428,6562],{"class":4021},[3002,12430,12431],{"class":3008}," com.example.audiobook.repository.jdbc.*;\n",[3002,12433,12434,12436],{"class":3004,"line":3121},[3002,12435,6562],{"class":4021},[3002,12437,6579],{"class":3008},[3002,12439,12440,12442],{"class":3004,"line":3149},[3002,12441,6562],{"class":4021},[3002,12443,8284],{"class":3008},[3002,12445,12446],{"class":3004,"line":3156},[3002,12447,3153],{"emptyLinePlaceholder":3152},[3002,12449,12450,12452],{"class":3004,"line":3180},[3002,12451,6562],{"class":4021},[3002,12453,8295],{"class":3008},[3002,12455,12456,12458],{"class":3004,"line":3202},[3002,12457,6562],{"class":4021},[3002,12459,6639],{"class":3008},[3002,12461,12462],{"class":3004,"line":3207},[3002,12463,3153],{"emptyLinePlaceholder":3152},[3002,12465,12466,12468,12470],{"class":3004,"line":3228},[3002,12467,6562],{"class":4021},[3002,12469,8319],{"class":4021},[3002,12471,8322],{"class":3008},[3002,12473,12474],{"class":3004,"line":3257},[3002,12475,3153],{"emptyLinePlaceholder":3152},[3002,12477,12478],{"class":3004,"line":3262},[3002,12479,6655],{"class":3032},[3002,12481,12482],{"class":3004,"line":3268},[3002,12483,12484],{"class":3032}," * Інтеграційні тести для {@link JdbcAudiobookRepository}.\n",[3002,12486,12487],{"class":3004,"line":3282},[3002,12488,6665],{"class":3032},[3002,12490,12491],{"class":3004,"line":3287},[3002,12492,12493],{"class":3032}," * Особлива увага приділяється тестуванню FK constraints та каскадного видалення.\n",[3002,12495,12496],{"class":3004,"line":3293},[3002,12497,6799],{"class":3032},[3002,12499,12500,12502,12505,12507,12509],{"class":3004,"line":3307},[3002,12501,3061],{"class":4021},[3002,12503,12504],{"class":3012}," JdbcAudiobookRepositoryTest",[3002,12506,8368],{"class":4021},[3002,12508,6814],{"class":3012},[3002,12510,6817],{"class":3008},[3002,12512,12513],{"class":3004,"line":4214},[3002,12514,3153],{"emptyLinePlaceholder":3152},[3002,12516,12517,12519,12522,12525],{"class":3004,"line":4409},[3002,12518,8381],{"class":4021},[3002,12520,12521],{"class":3012}," AudiobookRepository",[3002,12523,12524],{"class":3042}," audiobookRepo",[3002,12526,3889],{"class":3008},[3002,12528,12529,12531,12533,12536],{"class":3004,"line":4415},[3002,12530,8381],{"class":4021},[3002,12532,8384],{"class":3012},[3002,12534,12535],{"class":3042}," authorRepo",[3002,12537,3889],{"class":3008},[3002,12539,12540,12542,12544,12547],{"class":3004,"line":4420},[3002,12541,8381],{"class":4021},[3002,12543,11786],{"class":3012},[3002,12545,12546],{"class":3042}," genreRepo",[3002,12548,3889],{"class":3008},[3002,12550,12551],{"class":3004,"line":4425},[3002,12552,3153],{"emptyLinePlaceholder":3152},[3002,12554,12555,12557],{"class":3004,"line":4432},[3002,12556,6905],{"class":3008},[3002,12558,4250],{"class":3012},[3002,12560,12561,12563,12566],{"class":3004,"line":4441},[3002,12562,6913],{"class":3012},[3002,12564,12565],{"class":3022}," setUpRepositories",[3002,12567,3026],{"class":3008},[3002,12569,12570,12573,12575,12578],{"class":3004,"line":4464},[3002,12571,12572],{"class":3008},"        audiobookRepo = ",[3002,12574,3219],{"class":3218},[3002,12576,12577],{"class":3022}," JdbcAudiobookRepository",[3002,12579,5811],{"class":3008},[3002,12581,12582,12585,12587,12589],{"class":3004,"line":4479},[3002,12583,12584],{"class":3008},"        authorRepo = ",[3002,12586,3219],{"class":3218},[3002,12588,3222],{"class":3022},[3002,12590,5811],{"class":3008},[3002,12592,12593,12596,12598,12600],{"class":3004,"line":4954},[3002,12594,12595],{"class":3008},"        genreRepo = ",[3002,12597,3219],{"class":3218},[3002,12599,11817],{"class":3022},[3002,12601,5811],{"class":3008},[3002,12603,12604],{"class":3004,"line":4972},[3002,12605,7076],{"class":3008},[3002,12607,12608],{"class":3004,"line":4990},[3002,12609,3153],{"emptyLinePlaceholder":3152},[3002,12611,12612,12614],{"class":3004,"line":5007},[3002,12613,6905],{"class":3008},[3002,12615,3013],{"class":3012},[3002,12617,12618,12620,12623],{"class":3004,"line":5016},[3002,12619,6913],{"class":3012},[3002,12621,12622],{"class":3022}," save_shouldInsertNewAudiobook_whenValidData",[3002,12624,3026],{"class":3008},[3002,12626,12627],{"class":3004,"line":5487},[3002,12628,8468],{"class":3032},[3002,12630,12631],{"class":3004,"line":5492},[3002,12632,12633],{"class":3032},"        // Спочатку створити автора та жанр (батьківські сутності)\n",[3002,12635,12636,12638,12640,12642,12644,12646,12648,12650,12652,12654],{"class":3004,"line":5504},[3002,12637,8473],{"class":3012},[3002,12639,3234],{"class":3042},[3002,12641,3046],{"class":3008},[3002,12643,3219],{"class":3218},[3002,12645,3241],{"class":3022},[3002,12647,3052],{"class":3008},[3002,12649,3247],{"class":3246},[3002,12651,2975],{"class":3008},[3002,12653,3252],{"class":3246},[3002,12655,3064],{"class":3008},[3002,12657,12658,12660,12662,12664,12666,12668,12670,12672],{"class":3004,"line":5514},[3002,12659,11851],{"class":3012},[3002,12661,11854],{"class":3042},[3002,12663,3046],{"class":3008},[3002,12665,3219],{"class":3218},[3002,12667,11861],{"class":3022},[3002,12669,3052],{"class":3008},[3002,12671,12032],{"class":3246},[3002,12673,3064],{"class":3008},[3002,12675,12676,12679,12681,12683],{"class":3004,"line":5552},[3002,12677,12678],{"class":3042},"        authorRepo",[3002,12680,3058],{"class":3008},[3002,12682,3276],{"class":3022},[3002,12684,3279],{"class":3008},[3002,12686,12687,12690,12692,12694],{"class":3004,"line":5571},[3002,12688,12689],{"class":3042},"        genreRepo",[3002,12691,3058],{"class":3008},[3002,12693,3276],{"class":3022},[3002,12695,11904],{"class":3008},[3002,12697,12698],{"class":3004,"line":5586},[3002,12699,3153],{"emptyLinePlaceholder":3152},[3002,12701,12702,12705,12708,12710,12712,12715,12717,12720],{"class":3004,"line":5600},[3002,12703,12704],{"class":3012},"        Audiobook",[3002,12706,12707],{"class":3042}," book",[3002,12709,3046],{"class":3008},[3002,12711,3219],{"class":3218},[3002,12713,12714],{"class":3022}," Audiobook",[3002,12716,3052],{"class":3008},[3002,12718,12719],{"class":3246},"\"Захар Беркут\"",[3002,12721,12722],{"class":3008},", author, genre);\n",[3002,12724,12725,12728,12730,12733,12735,12738,12740],{"class":3004,"line":5605},[3002,12726,12727],{"class":3042},"        book",[3002,12729,3058],{"class":3008},[3002,12731,12732],{"class":3022},"setDuration",[3002,12734,3052],{"class":3008},[3002,12736,12737],{"class":3143},"18000",[3002,12739,4208],{"class":3008},[3002,12741,12742],{"class":3032},"// 5 годин у секундах\n",[3002,12744,12745,12747,12749,12752,12754,12757],{"class":3004,"line":5610},[3002,12746,12727],{"class":3042},[3002,12748,3058],{"class":3008},[3002,12750,12751],{"class":3022},"setReleaseYear",[3002,12753,3052],{"class":3008},[3002,12755,12756],{"class":3143},"1883",[3002,12758,3064],{"class":3008},[3002,12760,12761,12763,12765,12767,12769,12772],{"class":3004,"line":6796},[3002,12762,12727],{"class":3042},[3002,12764,3058],{"class":3008},[3002,12766,11878],{"class":3022},[3002,12768,3052],{"class":3008},[3002,12770,12771],{"class":3246},"\"Історична повість\"",[3002,12773,3064],{"class":3008},[3002,12775,12776],{"class":3004,"line":6802},[3002,12777,3153],{"emptyLinePlaceholder":3152},[3002,12779,12780],{"class":3004,"line":6820},[3002,12781,8532],{"class":3032},[3002,12783,12784,12787,12789,12791],{"class":3004,"line":6825},[3002,12785,12786],{"class":3042},"        audiobookRepo",[3002,12788,3058],{"class":3008},[3002,12790,3276],{"class":3022},[3002,12792,12793],{"class":3008},"(book);\n",[3002,12795,12796],{"class":3004,"line":6831},[3002,12797,3153],{"emptyLinePlaceholder":3152},[3002,12799,12800],{"class":3004,"line":6837},[3002,12801,8552],{"class":3032},[3002,12803,12804,12806,12808,12810,12813,12815,12817,12819,12822,12824,12826,12828,12830],{"class":3004,"line":6843},[3002,12805,12704],{"class":3012},[3002,12807,3530],{"class":3042},[3002,12809,3046],{"class":3008},[3002,12811,12812],{"class":3042},"audiobookRepo",[3002,12814,3058],{"class":3008},[3002,12816,3540],{"class":3022},[3002,12818,3052],{"class":3008},[3002,12820,12821],{"class":3042},"book",[3002,12823,3058],{"class":3008},[3002,12825,3550],{"class":3022},[3002,12827,3136],{"class":3008},[3002,12829,3555],{"class":3022},[3002,12831,3304],{"class":3008},[3002,12833,12834,12836,12838,12840,12842,12845,12847,12849,12851,12853],{"class":3004,"line":6849},[3002,12835,8562],{"class":3022},[3002,12837,3052],{"class":3008},[3002,12839,3567],{"class":3042},[3002,12841,3058],{"class":3008},[3002,12843,12844],{"class":3022},"getTitle",[3002,12846,3136],{"class":3008},[3002,12848,3577],{"class":3022},[3002,12850,3052],{"class":3008},[3002,12852,12719],{"class":3246},[3002,12854,3064],{"class":3008},[3002,12856,12857,12859,12861,12863,12865,12868,12870,12872,12874,12876],{"class":3004,"line":6863},[3002,12858,8562],{"class":3022},[3002,12860,3052],{"class":3008},[3002,12862,3567],{"class":3042},[3002,12864,3058],{"class":3008},[3002,12866,12867],{"class":3022},"getDuration",[3002,12869,3136],{"class":3008},[3002,12871,3577],{"class":3022},[3002,12873,3052],{"class":3008},[3002,12875,12737],{"class":3143},[3002,12877,3064],{"class":3008},[3002,12879,12880,12882,12884,12886,12888,12891,12893,12895,12897,12899],{"class":3004,"line":6868},[3002,12881,8562],{"class":3022},[3002,12883,3052],{"class":3008},[3002,12885,3567],{"class":3042},[3002,12887,3058],{"class":3008},[3002,12889,12890],{"class":3022},"getReleaseYear",[3002,12892,3136],{"class":3008},[3002,12894,3577],{"class":3022},[3002,12896,3052],{"class":3008},[3002,12898,12756],{"class":3143},[3002,12900,3064],{"class":3008},[3002,12902,12903,12905,12907,12909,12911,12914,12916,12918,12920,12922,12924,12926,12928,12930],{"class":3004,"line":6873},[3002,12904,8562],{"class":3022},[3002,12906,3052],{"class":3008},[3002,12908,3567],{"class":3042},[3002,12910,3058],{"class":3008},[3002,12912,12913],{"class":3022},"getAuthor",[3002,12915,6961],{"class":3008},[3002,12917,3550],{"class":3022},[3002,12919,3136],{"class":3008},[3002,12921,3577],{"class":3022},[3002,12923,3052],{"class":3008},[3002,12925,3545],{"class":3042},[3002,12927,3058],{"class":3008},[3002,12929,3550],{"class":3022},[3002,12931,11395],{"class":3008},[3002,12933,12934,12936,12938,12940,12942,12945,12947,12949,12951,12953,12955,12957,12959,12961],{"class":3004,"line":6879},[3002,12935,8562],{"class":3022},[3002,12937,3052],{"class":3008},[3002,12939,3567],{"class":3042},[3002,12941,3058],{"class":3008},[3002,12943,12944],{"class":3022},"getGenre",[3002,12946,6961],{"class":3008},[3002,12948,3550],{"class":3022},[3002,12950,3136],{"class":3008},[3002,12952,3577],{"class":3022},[3002,12954,3052],{"class":3008},[3002,12956,11931],{"class":3042},[3002,12958,3058],{"class":3008},[3002,12960,3550],{"class":3022},[3002,12962,11395],{"class":3008},[3002,12964,12965],{"class":3004,"line":6885},[3002,12966,7076],{"class":3008},[3002,12968,12969],{"class":3004,"line":6891},[3002,12970,3153],{"emptyLinePlaceholder":3152},[3002,12972,12973,12975],{"class":3004,"line":6897},[3002,12974,6905],{"class":3008},[3002,12976,3013],{"class":3012},[3002,12978,12979,12981,12984],{"class":3004,"line":6902},[3002,12980,6913],{"class":3012},[3002,12982,12983],{"class":3022}," save_shouldThrowException_whenAuthorNotExists",[3002,12985,3026],{"class":3008},[3002,12987,12988],{"class":3004,"line":6910},[3002,12989,8468],{"class":3032},[3002,12991,12992,12994,12996,12998,13000,13002,13004,13006],{"class":3004,"line":6935},[3002,12993,11851],{"class":3012},[3002,12995,11854],{"class":3042},[3002,12997,3046],{"class":3008},[3002,12999,3219],{"class":3218},[3002,13001,11861],{"class":3022},[3002,13003,3052],{"class":3008},[3002,13005,12032],{"class":3246},[3002,13007,3064],{"class":3008},[3002,13009,13010,13012,13014,13016],{"class":3004,"line":6941},[3002,13011,12689],{"class":3042},[3002,13013,3058],{"class":3008},[3002,13015,3276],{"class":3022},[3002,13017,11904],{"class":3008},[3002,13019,13020],{"class":3004,"line":6984},[3002,13021,3153],{"emptyLinePlaceholder":3152},[3002,13023,13024],{"class":3004,"line":6990},[3002,13025,13026],{"class":3032},"        // Створити автора БЕЗ збереження у БД\n",[3002,13028,13029,13031,13034,13036,13038,13040,13042,13044,13046,13048],{"class":3004,"line":6996},[3002,13030,8473],{"class":3012},[3002,13032,13033],{"class":3042}," nonExistentAuthor",[3002,13035,3046],{"class":3008},[3002,13037,3219],{"class":3218},[3002,13039,3241],{"class":3022},[3002,13041,3052],{"class":3008},[3002,13043,10107],{"class":3246},[3002,13045,2975],{"class":3008},[3002,13047,10112],{"class":3246},[3002,13049,3064],{"class":3008},[3002,13051,13052,13055,13057,13059,13061,13063,13065,13067],{"class":3004,"line":7002},[3002,13053,13054],{"class":3042},"        nonExistentAuthor",[3002,13056,3058],{"class":3008},[3002,13058,8965],{"class":3022},[3002,13060,3052],{"class":3008},[3002,13062,3875],{"class":3042},[3002,13064,3058],{"class":3008},[3002,13066,3880],{"class":3022},[3002,13068,11395],{"class":3008},[3002,13070,13071],{"class":3004,"line":7008},[3002,13072,3153],{"emptyLinePlaceholder":3152},[3002,13074,13075,13077,13079,13081,13083,13085,13087,13090],{"class":3004,"line":7028},[3002,13076,12704],{"class":3012},[3002,13078,12707],{"class":3042},[3002,13080,3046],{"class":3008},[3002,13082,3219],{"class":3218},[3002,13084,12714],{"class":3022},[3002,13086,3052],{"class":3008},[3002,13088,13089],{"class":3246},"\"Книга\"",[3002,13091,13092],{"class":3008},", nonExistentAuthor, genre);\n",[3002,13094,13095,13097,13099,13101,13103,13106],{"class":3004,"line":7050},[3002,13096,12727],{"class":3042},[3002,13098,3058],{"class":3008},[3002,13100,12732],{"class":3022},[3002,13102,3052],{"class":3008},[3002,13104,13105],{"class":3143},"1000",[3002,13107,3064],{"class":3008},[3002,13109,13110,13112,13114,13116,13118,13121],{"class":3004,"line":7055},[3002,13111,12727],{"class":3042},[3002,13113,3058],{"class":3008},[3002,13115,12751],{"class":3022},[3002,13117,3052],{"class":3008},[3002,13119,13120],{"class":3143},"2020",[3002,13122,3064],{"class":3008},[3002,13124,13125],{"class":3004,"line":7061},[3002,13126,3153],{"emptyLinePlaceholder":3152},[3002,13128,13129],{"class":3004,"line":7073},[3002,13130,9037],{"class":3032},[3002,13132,13133],{"class":3004,"line":7079},[3002,13134,13135],{"class":3032},"        // FK constraint має запобігти вставці (author_id не існує у authors)\n",[3002,13137,13138,13140,13142,13144,13146,13148,13150],{"class":3004,"line":7084},[3002,13139,9047],{"class":3022},[3002,13141,9050],{"class":3008},[3002,13143,9053],{"class":4021},[3002,13145,12524],{"class":3042},[3002,13147,3058],{"class":3008},[3002,13149,3276],{"class":3022},[3002,13151,13152],{"class":3008},"(book))\n",[3002,13154,13155,13157,13159,13161,13163,13165,13167,13169,13171,13173,13175,13177,13179,13181,13183],{"class":3004,"line":7089},[3002,13156,9067],{"class":3008},[3002,13158,9070],{"class":3022},[3002,13160,3052],{"class":3008},[3002,13162,9075],{"class":3042},[3002,13164,3058],{"class":3008},[3002,13166,9080],{"class":3042},[3002,13168,3058],{"class":3008},[3002,13170,9085],{"class":3042},[3002,13172,3058],{"class":3008},[3002,13174,9090],{"class":3042},[3002,13176,3058],{"class":3008},[3002,13178,9095],{"class":3042},[3002,13180,3058],{"class":3008},[3002,13182,3061],{"class":3042},[3002,13184,5187],{"class":3008},[3002,13186,13187,13189,13191,13193,13196,13198],{"class":3004,"line":7095},[3002,13188,9067],{"class":3008},[3002,13190,9108],{"class":3022},[3002,13192,3052],{"class":3008},[3002,13194,13195],{"class":3246},"\"FOREIGN KEY\"",[3002,13197,4208],{"class":3008},[3002,13199,13200],{"class":3032},"// або \"REFERENTIAL INTEGRITY\"\n",[3002,13202,13203],{"class":3004,"line":7101},[3002,13204,7076],{"class":3008},[3002,13206,13207],{"class":3004,"line":7106},[3002,13208,3153],{"emptyLinePlaceholder":3152},[3002,13210,13211,13213],{"class":3004,"line":7113},[3002,13212,6905],{"class":3008},[3002,13214,3013],{"class":3012},[3002,13216,13217,13219,13222],{"class":3004,"line":7122},[3002,13218,6913],{"class":3012},[3002,13220,13221],{"class":3022}," save_shouldThrowException_whenGenreNotExists",[3002,13223,3026],{"class":3008},[3002,13225,13226],{"class":3004,"line":7137},[3002,13227,8468],{"class":3032},[3002,13229,13230,13232,13234,13236,13238,13240,13242,13244,13246,13248],{"class":3004,"line":7149},[3002,13231,8473],{"class":3012},[3002,13233,3234],{"class":3042},[3002,13235,3046],{"class":3008},[3002,13237,3219],{"class":3218},[3002,13239,3241],{"class":3022},[3002,13241,3052],{"class":3008},[3002,13243,3247],{"class":3246},[3002,13245,2975],{"class":3008},[3002,13247,3252],{"class":3246},[3002,13249,3064],{"class":3008},[3002,13251,13252,13254,13256,13258],{"class":3004,"line":7155},[3002,13253,12678],{"class":3042},[3002,13255,3058],{"class":3008},[3002,13257,3276],{"class":3022},[3002,13259,3279],{"class":3008},[3002,13261,13262],{"class":3004,"line":7160},[3002,13263,3153],{"emptyLinePlaceholder":3152},[3002,13265,13266,13268,13271,13273,13275,13277,13279,13282],{"class":3004,"line":7165},[3002,13267,11851],{"class":3012},[3002,13269,13270],{"class":3042}," nonExistentGenre",[3002,13272,3046],{"class":3008},[3002,13274,3219],{"class":3218},[3002,13276,11861],{"class":3022},[3002,13278,3052],{"class":3008},[3002,13280,13281],{"class":3246},"\"Неіснуючий жанр\"",[3002,13283,3064],{"class":3008},[3002,13285,13286,13289,13291,13293,13295,13297,13299,13301],{"class":3004,"line":7171},[3002,13287,13288],{"class":3042},"        nonExistentGenre",[3002,13290,3058],{"class":3008},[3002,13292,8965],{"class":3022},[3002,13294,3052],{"class":3008},[3002,13296,3875],{"class":3042},[3002,13298,3058],{"class":3008},[3002,13300,3880],{"class":3022},[3002,13302,11395],{"class":3008},[3002,13304,13305],{"class":3004,"line":7177},[3002,13306,3153],{"emptyLinePlaceholder":3152},[3002,13308,13309,13311,13313,13315,13317,13319,13321,13323],{"class":3004,"line":7182},[3002,13310,12704],{"class":3012},[3002,13312,12707],{"class":3042},[3002,13314,3046],{"class":3008},[3002,13316,3219],{"class":3218},[3002,13318,12714],{"class":3022},[3002,13320,3052],{"class":3008},[3002,13322,13089],{"class":3246},[3002,13324,13325],{"class":3008},", author, nonExistentGenre);\n",[3002,13327,13328,13330,13332,13334,13336,13338],{"class":3004,"line":7187},[3002,13329,12727],{"class":3042},[3002,13331,3058],{"class":3008},[3002,13333,12732],{"class":3022},[3002,13335,3052],{"class":3008},[3002,13337,13105],{"class":3143},[3002,13339,3064],{"class":3008},[3002,13341,13342,13344,13346,13348,13350,13352],{"class":3004,"line":7192},[3002,13343,12727],{"class":3042},[3002,13345,3058],{"class":3008},[3002,13347,12751],{"class":3022},[3002,13349,3052],{"class":3008},[3002,13351,13120],{"class":3143},[3002,13353,3064],{"class":3008},[3002,13355,13356],{"class":3004,"line":7198},[3002,13357,3153],{"emptyLinePlaceholder":3152},[3002,13359,13360],{"class":3004,"line":7203},[3002,13361,9037],{"class":3032},[3002,13363,13364,13366,13368,13370,13372,13374,13376],{"class":3004,"line":7209},[3002,13365,9047],{"class":3022},[3002,13367,9050],{"class":3008},[3002,13369,9053],{"class":4021},[3002,13371,12524],{"class":3042},[3002,13373,3058],{"class":3008},[3002,13375,3276],{"class":3022},[3002,13377,13152],{"class":3008},[3002,13379,13380,13382,13384,13386,13388,13390,13392,13394,13396,13398,13400,13402,13404,13406,13408],{"class":3004,"line":7215},[3002,13381,9067],{"class":3008},[3002,13383,9070],{"class":3022},[3002,13385,3052],{"class":3008},[3002,13387,9075],{"class":3042},[3002,13389,3058],{"class":3008},[3002,13391,9080],{"class":3042},[3002,13393,3058],{"class":3008},[3002,13395,9085],{"class":3042},[3002,13397,3058],{"class":3008},[3002,13399,9090],{"class":3042},[3002,13401,3058],{"class":3008},[3002,13403,9095],{"class":3042},[3002,13405,3058],{"class":3008},[3002,13407,3061],{"class":3042},[3002,13409,5187],{"class":3008},[3002,13411,13412,13414,13416,13418,13420],{"class":3004,"line":7221},[3002,13413,9067],{"class":3008},[3002,13415,9108],{"class":3022},[3002,13417,3052],{"class":3008},[3002,13419,13195],{"class":3246},[3002,13421,3064],{"class":3008},[3002,13423,13424],{"class":3004,"line":7236},[3002,13425,7076],{"class":3008},[3002,13427,13428],{"class":3004,"line":7241},[3002,13429,3153],{"emptyLinePlaceholder":3152},[3002,13431,13432,13434],{"class":3004,"line":7271},[3002,13433,6905],{"class":3008},[3002,13435,3013],{"class":3012},[3002,13437,13438,13440,13443],{"class":3004,"line":7287},[3002,13439,6913],{"class":3012},[3002,13441,13442],{"class":3022}," deleteAuthor_shouldCascadeDeleteAudiobooks_whenAuthorHasBooks",[3002,13444,3026],{"class":3008},[3002,13446,13447],{"class":3004,"line":7292},[3002,13448,8468],{"class":3032},[3002,13450,13451,13453,13455,13457,13459,13461,13463,13465,13467,13469],{"class":3004,"line":7315},[3002,13452,8473],{"class":3012},[3002,13454,3234],{"class":3042},[3002,13456,3046],{"class":3008},[3002,13458,3219],{"class":3218},[3002,13460,3241],{"class":3022},[3002,13462,3052],{"class":3008},[3002,13464,5828],{"class":3246},[3002,13466,2975],{"class":3008},[3002,13468,5833],{"class":3246},[3002,13470,3064],{"class":3008},[3002,13472,13473,13475,13477,13479,13481,13483,13485,13487],{"class":3004,"line":7337},[3002,13474,11851],{"class":3012},[3002,13476,11854],{"class":3042},[3002,13478,3046],{"class":3008},[3002,13480,3219],{"class":3218},[3002,13482,11861],{"class":3022},[3002,13484,3052],{"class":3008},[3002,13486,12356],{"class":3246},[3002,13488,3064],{"class":3008},[3002,13490,13491,13493,13495,13497],{"class":3004,"line":7343},[3002,13492,12678],{"class":3042},[3002,13494,3058],{"class":3008},[3002,13496,3276],{"class":3022},[3002,13498,3279],{"class":3008},[3002,13500,13501,13503,13505,13507],{"class":3004,"line":7349},[3002,13502,12689],{"class":3042},[3002,13504,3058],{"class":3008},[3002,13506,3276],{"class":3022},[3002,13508,11904],{"class":3008},[3002,13510,13511],{"class":3004,"line":7377},[3002,13512,3153],{"emptyLinePlaceholder":3152},[3002,13514,13515,13517,13520,13522,13524,13526,13528,13531],{"class":3004,"line":7396},[3002,13516,12704],{"class":3012},[3002,13518,13519],{"class":3042}," book1",[3002,13521,3046],{"class":3008},[3002,13523,3219],{"class":3218},[3002,13525,12714],{"class":3022},[3002,13527,3052],{"class":3008},[3002,13529,13530],{"class":3246},"\"Кобзар\"",[3002,13532,12722],{"class":3008},[3002,13534,13535,13538,13540,13542,13544,13547],{"class":3004,"line":7417},[3002,13536,13537],{"class":3042},"        book1",[3002,13539,3058],{"class":3008},[3002,13541,12732],{"class":3022},[3002,13543,3052],{"class":3008},[3002,13545,13546],{"class":3143},"10000",[3002,13548,3064],{"class":3008},[3002,13550,13551,13553,13555,13557,13559,13562],{"class":3004,"line":7451},[3002,13552,13537],{"class":3042},[3002,13554,3058],{"class":3008},[3002,13556,12751],{"class":3022},[3002,13558,3052],{"class":3008},[3002,13560,13561],{"class":3143},"1840",[3002,13563,3064],{"class":3008},[3002,13565,13566],{"class":3004,"line":7465},[3002,13567,3153],{"emptyLinePlaceholder":3152},[3002,13569,13570,13572,13575,13577,13579,13581,13583,13586],{"class":3004,"line":7471},[3002,13571,12704],{"class":3012},[3002,13573,13574],{"class":3042}," book2",[3002,13576,3046],{"class":3008},[3002,13578,3219],{"class":3218},[3002,13580,12714],{"class":3022},[3002,13582,3052],{"class":3008},[3002,13584,13585],{"class":3246},"\"Гайдамаки\"",[3002,13587,12722],{"class":3008},[3002,13589,13590,13593,13595,13597,13599,13602],{"class":3004,"line":7477},[3002,13591,13592],{"class":3042},"        book2",[3002,13594,3058],{"class":3008},[3002,13596,12732],{"class":3022},[3002,13598,3052],{"class":3008},[3002,13600,13601],{"class":3143},"8000",[3002,13603,3064],{"class":3008},[3002,13605,13606,13608,13610,13612,13614,13617],{"class":3004,"line":7482},[3002,13607,13592],{"class":3042},[3002,13609,3058],{"class":3008},[3002,13611,12751],{"class":3022},[3002,13613,3052],{"class":3008},[3002,13615,13616],{"class":3143},"1841",[3002,13618,3064],{"class":3008},[3002,13620,13621],{"class":3004,"line":7487},[3002,13622,3153],{"emptyLinePlaceholder":3152},[3002,13624,13625,13627,13629,13631],{"class":3004,"line":7492},[3002,13626,12786],{"class":3042},[3002,13628,3058],{"class":3008},[3002,13630,3276],{"class":3022},[3002,13632,13633],{"class":3008},"(book1);\n",[3002,13635,13636,13638,13640,13642],{"class":3004,"line":7497},[3002,13637,12786],{"class":3042},[3002,13639,3058],{"class":3008},[3002,13641,3276],{"class":3022},[3002,13643,13644],{"class":3008},"(book2);\n",[3002,13646,13647],{"class":3004,"line":7503},[3002,13648,3153],{"emptyLinePlaceholder":3152},[3002,13650,13651],{"class":3004,"line":7508},[3002,13652,13653],{"class":3032},"        // Перевірка: у БД 2 книги\n",[3002,13655,13656,13658,13660,13662,13664,13667,13669,13671,13673,13675],{"class":3004,"line":7521},[3002,13657,8562],{"class":3022},[3002,13659,3052],{"class":3008},[3002,13661,8197],{"class":3022},[3002,13663,3052],{"class":3008},[3002,13665,13666],{"class":3246},"\"audiobooks\"",[3002,13668,8728],{"class":3008},[3002,13670,3577],{"class":3022},[3002,13672,3052],{"class":3008},[3002,13674,10736],{"class":3143},[3002,13676,3064],{"class":3008},[3002,13678,13679],{"class":3004,"line":7532},[3002,13680,3153],{"emptyLinePlaceholder":3152},[3002,13682,13683],{"class":3004,"line":7537},[3002,13684,8532],{"class":3032},[3002,13686,13687],{"class":3004,"line":7562},[3002,13688,13689],{"class":3032},"        // Видалити автора → має каскадно видалити всі його книги (ON DELETE CASCADE)\n",[3002,13691,13692,13694,13696,13698,13700,13702,13704,13706],{"class":3004,"line":7585},[3002,13693,12678],{"class":3042},[3002,13695,3058],{"class":3008},[3002,13697,10349],{"class":3022},[3002,13699,3052],{"class":3008},[3002,13701,3545],{"class":3042},[3002,13703,3058],{"class":3008},[3002,13705,3550],{"class":3022},[3002,13707,11395],{"class":3008},[3002,13709,13710],{"class":3004,"line":7596},[3002,13711,3153],{"emptyLinePlaceholder":3152},[3002,13713,13714],{"class":3004,"line":7615},[3002,13715,8552],{"class":3032},[3002,13717,13718,13720,13722,13725,13727,13729,13731,13733,13735,13737,13739,13741],{"class":3004,"line":7629},[3002,13719,8562],{"class":3022},[3002,13721,3052],{"class":3008},[3002,13723,13724],{"class":3042},"authorRepo",[3002,13726,3058],{"class":3008},[3002,13728,3540],{"class":3022},[3002,13730,3052],{"class":3008},[3002,13732,3545],{"class":3042},[3002,13734,3058],{"class":3008},[3002,13736,3550],{"class":3022},[3002,13738,3112],{"class":3008},[3002,13740,4471],{"class":3022},[3002,13742,3304],{"class":3008},[3002,13744,13745,13747,13749,13751,13753,13755,13757,13760,13762,13764,13766,13768],{"class":3004,"line":7670},[3002,13746,8562],{"class":3022},[3002,13748,3052],{"class":3008},[3002,13750,12812],{"class":3042},[3002,13752,3058],{"class":3008},[3002,13754,3540],{"class":3022},[3002,13756,3052],{"class":3008},[3002,13758,13759],{"class":3042},"book1",[3002,13761,3058],{"class":3008},[3002,13763,3550],{"class":3022},[3002,13765,3112],{"class":3008},[3002,13767,4471],{"class":3022},[3002,13769,3304],{"class":3008},[3002,13771,13772,13774,13776,13778,13780,13782,13784,13787,13789,13791,13793,13795],{"class":3004,"line":7675},[3002,13773,8562],{"class":3022},[3002,13775,3052],{"class":3008},[3002,13777,12812],{"class":3042},[3002,13779,3058],{"class":3008},[3002,13781,3540],{"class":3022},[3002,13783,3052],{"class":3008},[3002,13785,13786],{"class":3042},"book2",[3002,13788,3058],{"class":3008},[3002,13790,3550],{"class":3022},[3002,13792,3112],{"class":3008},[3002,13794,4471],{"class":3022},[3002,13796,3304],{"class":3008},[3002,13798,13799,13801,13803,13805,13807,13809,13811,13813,13815,13817],{"class":3004,"line":7680},[3002,13800,8562],{"class":3022},[3002,13802,3052],{"class":3008},[3002,13804,8197],{"class":3022},[3002,13806,3052],{"class":3008},[3002,13808,13666],{"class":3246},[3002,13810,8728],{"class":3008},[3002,13812,3577],{"class":3022},[3002,13814,3052],{"class":3008},[3002,13816,5328],{"class":3143},[3002,13818,3064],{"class":3008},[3002,13820,13821],{"class":3004,"line":7685},[3002,13822,7076],{"class":3008},[3002,13824,13825],{"class":3004,"line":7690},[3002,13826,3153],{"emptyLinePlaceholder":3152},[3002,13828,13829,13831],{"class":3004,"line":7696},[3002,13830,6905],{"class":3008},[3002,13832,3013],{"class":3012},[3002,13834,13835,13837,13840],{"class":3004,"line":7701},[3002,13836,6913],{"class":3012},[3002,13838,13839],{"class":3022}," deleteGenre_shouldCascadeDeleteAudiobooks_whenGenreHasBooks",[3002,13841,3026],{"class":3008},[3002,13843,13844],{"class":3004,"line":7707},[3002,13845,8468],{"class":3032},[3002,13847,13848,13850,13852,13854,13856,13858,13860,13862,13864,13866],{"class":3004,"line":7712},[3002,13849,8473],{"class":3012},[3002,13851,3234],{"class":3042},[3002,13853,3046],{"class":3008},[3002,13855,3219],{"class":3218},[3002,13857,3241],{"class":3022},[3002,13859,3052],{"class":3008},[3002,13861,8780],{"class":3246},[3002,13863,2975],{"class":3008},[3002,13865,8785],{"class":3246},[3002,13867,3064],{"class":3008},[3002,13869,13870,13872,13874,13876,13878,13880,13882,13885],{"class":3004,"line":7724},[3002,13871,11851],{"class":3012},[3002,13873,11854],{"class":3042},[3002,13875,3046],{"class":3008},[3002,13877,3219],{"class":3218},[3002,13879,11861],{"class":3022},[3002,13881,3052],{"class":3008},[3002,13883,13884],{"class":3246},"\"Драма\"",[3002,13886,3064],{"class":3008},[3002,13888,13889,13891,13893,13895],{"class":3004,"line":7729},[3002,13890,12678],{"class":3042},[3002,13892,3058],{"class":3008},[3002,13894,3276],{"class":3022},[3002,13896,3279],{"class":3008},[3002,13898,13899,13901,13903,13905],{"class":3004,"line":7753},[3002,13900,12689],{"class":3042},[3002,13902,3058],{"class":3008},[3002,13904,3276],{"class":3022},[3002,13906,11904],{"class":3008},[3002,13908,13909],{"class":3004,"line":7774},[3002,13910,3153],{"emptyLinePlaceholder":3152},[3002,13912,13913,13915,13917,13919,13921,13923,13925,13928],{"class":3004,"line":7791},[3002,13914,12704],{"class":3012},[3002,13916,12707],{"class":3042},[3002,13918,3046],{"class":3008},[3002,13920,3219],{"class":3218},[3002,13922,12714],{"class":3022},[3002,13924,3052],{"class":3008},[3002,13926,13927],{"class":3246},"\"Лісова пісня\"",[3002,13929,12722],{"class":3008},[3002,13931,13932,13934,13936,13938,13940,13943],{"class":3004,"line":7804},[3002,13933,12727],{"class":3042},[3002,13935,3058],{"class":3008},[3002,13937,12732],{"class":3022},[3002,13939,3052],{"class":3008},[3002,13941,13942],{"class":3143},"7200",[3002,13944,3064],{"class":3008},[3002,13946,13947,13949,13951,13953,13955,13958],{"class":3004,"line":7809},[3002,13948,12727],{"class":3042},[3002,13950,3058],{"class":3008},[3002,13952,12751],{"class":3022},[3002,13954,3052],{"class":3008},[3002,13956,13957],{"class":3143},"1911",[3002,13959,3064],{"class":3008},[3002,13961,13962,13964,13966,13968],{"class":3004,"line":7814},[3002,13963,12786],{"class":3042},[3002,13965,3058],{"class":3008},[3002,13967,3276],{"class":3022},[3002,13969,12793],{"class":3008},[3002,13971,13972],{"class":3004,"line":7819},[3002,13973,3153],{"emptyLinePlaceholder":3152},[3002,13975,13976],{"class":3004,"line":7824},[3002,13977,8532],{"class":3032},[3002,13979,13980,13982,13984,13986,13988,13990,13992,13994],{"class":3004,"line":7830},[3002,13981,12689],{"class":3042},[3002,13983,3058],{"class":3008},[3002,13985,10349],{"class":3022},[3002,13987,3052],{"class":3008},[3002,13989,11931],{"class":3042},[3002,13991,3058],{"class":3008},[3002,13993,3550],{"class":3022},[3002,13995,11395],{"class":3008},[3002,13997,13998],{"class":3004,"line":7836},[3002,13999,3153],{"emptyLinePlaceholder":3152},[3002,14001,14002],{"class":3004,"line":7841},[3002,14003,8552],{"class":3032},[3002,14005,14006,14008,14010,14013,14015,14017,14019,14021,14023,14025,14027,14029],{"class":3004,"line":7854},[3002,14007,8562],{"class":3022},[3002,14009,3052],{"class":3008},[3002,14011,14012],{"class":3042},"genreRepo",[3002,14014,3058],{"class":3008},[3002,14016,3540],{"class":3022},[3002,14018,3052],{"class":3008},[3002,14020,11931],{"class":3042},[3002,14022,3058],{"class":3008},[3002,14024,3550],{"class":3022},[3002,14026,3112],{"class":3008},[3002,14028,4471],{"class":3022},[3002,14030,3304],{"class":3008},[3002,14032,14033,14035,14037,14039,14041,14043,14045,14047,14049,14051,14053,14055],{"class":3004,"line":7864},[3002,14034,8562],{"class":3022},[3002,14036,3052],{"class":3008},[3002,14038,12812],{"class":3042},[3002,14040,3058],{"class":3008},[3002,14042,3540],{"class":3022},[3002,14044,3052],{"class":3008},[3002,14046,12821],{"class":3042},[3002,14048,3058],{"class":3008},[3002,14050,3550],{"class":3022},[3002,14052,3112],{"class":3008},[3002,14054,4471],{"class":3022},[3002,14056,3304],{"class":3008},[3002,14058,14059],{"class":3004,"line":7869},[3002,14060,7076],{"class":3008},[3002,14062,14063],{"class":3004,"line":7894},[3002,14064,3310],{"class":3008},[3660,14066],{},[3810,14068,14070],{"id":14069},"тестування-check-constraint","Тестування CHECK constraint",[2993,14072,14074],{"className":2995,"code":14073,"language":2997,"meta":4616,"style":2998},"@Test\nvoid save_shouldThrowException_whenDurationNegative() {\n    // ═══ Arrange ═══\n    Author author = new Author(\"Автор\", \"Тест\");\n    Genre genre = new Genre(\"Жанр\");\n    authorRepo.save(author);\n    genreRepo.save(genre);\n\n    Audiobook book = new Audiobook(\"Книга\", author, genre);\n    book.setDuration(-100); // негативна тривалість!\n    book.setReleaseYear(2020);\n\n    // ═══ Act & Assert ═══\n    // CHECK constraint (duration > 0) має запобігти вставці\n    assertThatThrownBy(() -> audiobookRepo.save(book))\n        .isInstanceOf(com.example.audiobook.db.DatabaseException.class)\n        .hasMessageContaining(\"CHECK\"); // або \"constraint\"\n}\n\n@Test\nvoid save_shouldThrowException_whenReleaseYearTooOld() {\n    // ═══ Arrange ═══\n    Author author = new Author(\"Автор\", \"Тест\");\n    Genre genre = new Genre(\"Жанр\");\n    authorRepo.save(author);\n    genreRepo.save(genre);\n\n    Audiobook book = new Audiobook(\"Книга\", author, genre);\n    book.setDuration(1000);\n    book.setReleaseYear(1800); // раніше 1900 — порушення CHECK constraint\n\n    // ═══ Act & Assert ═══\n    assertThatThrownBy(() -> audiobookRepo.save(book))\n        .isInstanceOf(com.example.audiobook.db.DatabaseException.class)\n        .hasMessageContaining(\"CHECK\");\n}\n\n@Test\nvoid save_shouldThrowException_whenReleaseYearInFuture() {\n    // ═══ Arrange ═══\n    Author author = new Author(\"Автор\", \"Тест\");\n    Genre genre = new Genre(\"Жанр\");\n    authorRepo.save(author);\n    genreRepo.save(genre);\n\n    Audiobook book = new Audiobook(\"Книга\", author, genre);\n    book.setDuration(1000);\n    book.setReleaseYear(2100); // занадто далеко у майбутньому\n\n    // ═══ Act & Assert ═══\n    assertThatThrownBy(() -> audiobookRepo.save(book))\n        .isInstanceOf(com.example.audiobook.db.DatabaseException.class)\n        .hasMessageContaining(\"CHECK\");\n}\n\n@Test\nvoid save_shouldAcceptCurrentYearPlusOne_whenReleaseYearValid() {\n    // ═══ Arrange ═══\n    Author author = new Author(\"Автор\", \"Тест\");\n    Genre genre = new Genre(\"Жанр\");\n    authorRepo.save(author);\n    genreRepo.save(genre);\n\n    int nextYear = java.time.Year.now().getValue() + 1;\n\n    Audiobook book = new Audiobook(\"Майбутня книга\", author, genre);\n    book.setDuration(1000);\n    book.setReleaseYear(nextYear); // наступний рік — допустимо\n\n    // ═══ Act ═══\n    audiobookRepo.save(book);\n\n    // ═══ Assert ═══\n    Audiobook loaded = audiobookRepo.findById(book.getId()).orElseThrow();\n    assertThat(loaded.getReleaseYear()).isEqualTo(nextYear);\n}\n",[2968,14075,14076,14082,14091,14095,14118,14138,14149,14160,14164,14183,14203,14217,14221,14226,14231,14248,14281,14297,14301,14305,14311,14320,14324,14346,14364,14374,14384,14388,14406,14420,14438,14442,14446,14462,14494,14506,14510,14514,14520,14529,14533,14555,14573,14583,14593,14597,14615,14629,14647,14651,14655,14671,14703,14715,14719,14723,14729,14738,14742,14764,14782,14792,14802,14806,14844,14848,14867,14881,14895,14899,14903,14914,14918,14922,14950,14969],{"__ignoreMap":2998},[3002,14077,14078,14080],{"class":3004,"line":3005},[3002,14079,3009],{"class":3008},[3002,14081,3013],{"class":3012},[3002,14083,14084,14086,14089],{"class":3004,"line":3016},[3002,14085,3019],{"class":3012},[3002,14087,14088],{"class":3022}," save_shouldThrowException_whenDurationNegative",[3002,14090,3026],{"class":3008},[3002,14092,14093],{"class":3004,"line":3029},[3002,14094,5796],{"class":3032},[3002,14096,14097,14099,14101,14103,14105,14107,14109,14111,14113,14116],{"class":3004,"line":3036},[3002,14098,3231],{"class":3012},[3002,14100,3234],{"class":3042},[3002,14102,3046],{"class":3008},[3002,14104,3219],{"class":3218},[3002,14106,3241],{"class":3022},[3002,14108,3052],{"class":3008},[3002,14110,10112],{"class":3246},[3002,14112,2975],{"class":3008},[3002,14114,14115],{"class":3246},"\"Тест\"",[3002,14117,3064],{"class":3008},[3002,14119,14120,14123,14125,14127,14129,14131,14133,14136],{"class":3004,"line":3067},[3002,14121,14122],{"class":3012},"    Genre",[3002,14124,11854],{"class":3042},[3002,14126,3046],{"class":3008},[3002,14128,3219],{"class":3218},[3002,14130,11861],{"class":3022},[3002,14132,3052],{"class":3008},[3002,14134,14135],{"class":3246},"\"Жанр\"",[3002,14137,3064],{"class":3008},[3002,14139,14140,14143,14145,14147],{"class":3004,"line":3091},[3002,14141,14142],{"class":3042},"    authorRepo",[3002,14144,3058],{"class":3008},[3002,14146,3276],{"class":3022},[3002,14148,3279],{"class":3008},[3002,14150,14151,14154,14156,14158],{"class":3004,"line":3121},[3002,14152,14153],{"class":3042},"    genreRepo",[3002,14155,3058],{"class":3008},[3002,14157,3276],{"class":3022},[3002,14159,11904],{"class":3008},[3002,14161,14162],{"class":3004,"line":3149},[3002,14163,3153],{"emptyLinePlaceholder":3152},[3002,14165,14166,14169,14171,14173,14175,14177,14179,14181],{"class":3004,"line":3156},[3002,14167,14168],{"class":3012},"    Audiobook",[3002,14170,12707],{"class":3042},[3002,14172,3046],{"class":3008},[3002,14174,3219],{"class":3218},[3002,14176,12714],{"class":3022},[3002,14178,3052],{"class":3008},[3002,14180,13089],{"class":3246},[3002,14182,12722],{"class":3008},[3002,14184,14185,14188,14190,14192,14195,14198,14200],{"class":3004,"line":3180},[3002,14186,14187],{"class":3042},"    book",[3002,14189,3058],{"class":3008},[3002,14191,12732],{"class":3022},[3002,14193,14194],{"class":3008},"(-",[3002,14196,14197],{"class":3143},"100",[3002,14199,4208],{"class":3008},[3002,14201,14202],{"class":3032},"// негативна тривалість!\n",[3002,14204,14205,14207,14209,14211,14213,14215],{"class":3004,"line":3202},[3002,14206,14187],{"class":3042},[3002,14208,3058],{"class":3008},[3002,14210,12751],{"class":3022},[3002,14212,3052],{"class":3008},[3002,14214,13120],{"class":3143},[3002,14216,3064],{"class":3008},[3002,14218,14219],{"class":3004,"line":3207},[3002,14220,3153],{"emptyLinePlaceholder":3152},[3002,14222,14223],{"class":3004,"line":3228},[3002,14224,14225],{"class":3032},"    // ═══ Act & Assert ═══\n",[3002,14227,14228],{"class":3004,"line":3257},[3002,14229,14230],{"class":3032},"    // CHECK constraint (duration > 0) має запобігти вставці\n",[3002,14232,14233,14236,14238,14240,14242,14244,14246],{"class":3004,"line":3262},[3002,14234,14235],{"class":3022},"    assertThatThrownBy",[3002,14237,9050],{"class":3008},[3002,14239,9053],{"class":4021},[3002,14241,12524],{"class":3042},[3002,14243,3058],{"class":3008},[3002,14245,3276],{"class":3022},[3002,14247,13152],{"class":3008},[3002,14249,14250,14253,14255,14257,14259,14261,14263,14265,14267,14269,14271,14273,14275,14277,14279],{"class":3004,"line":3268},[3002,14251,14252],{"class":3008},"        .",[3002,14254,9070],{"class":3022},[3002,14256,3052],{"class":3008},[3002,14258,9075],{"class":3042},[3002,14260,3058],{"class":3008},[3002,14262,9080],{"class":3042},[3002,14264,3058],{"class":3008},[3002,14266,9085],{"class":3042},[3002,14268,3058],{"class":3008},[3002,14270,9090],{"class":3042},[3002,14272,3058],{"class":3008},[3002,14274,9095],{"class":3042},[3002,14276,3058],{"class":3008},[3002,14278,3061],{"class":3042},[3002,14280,5187],{"class":3008},[3002,14282,14283,14285,14287,14289,14292,14294],{"class":3004,"line":3282},[3002,14284,14252],{"class":3008},[3002,14286,9108],{"class":3022},[3002,14288,3052],{"class":3008},[3002,14290,14291],{"class":3246},"\"CHECK\"",[3002,14293,4208],{"class":3008},[3002,14295,14296],{"class":3032},"// або \"constraint\"\n",[3002,14298,14299],{"class":3004,"line":3287},[3002,14300,3310],{"class":3008},[3002,14302,14303],{"class":3004,"line":3293},[3002,14304,3153],{"emptyLinePlaceholder":3152},[3002,14306,14307,14309],{"class":3004,"line":3307},[3002,14308,3009],{"class":3008},[3002,14310,3013],{"class":3012},[3002,14312,14313,14315,14318],{"class":3004,"line":4214},[3002,14314,3019],{"class":3012},[3002,14316,14317],{"class":3022}," save_shouldThrowException_whenReleaseYearTooOld",[3002,14319,3026],{"class":3008},[3002,14321,14322],{"class":3004,"line":4409},[3002,14323,5796],{"class":3032},[3002,14325,14326,14328,14330,14332,14334,14336,14338,14340,14342,14344],{"class":3004,"line":4415},[3002,14327,3231],{"class":3012},[3002,14329,3234],{"class":3042},[3002,14331,3046],{"class":3008},[3002,14333,3219],{"class":3218},[3002,14335,3241],{"class":3022},[3002,14337,3052],{"class":3008},[3002,14339,10112],{"class":3246},[3002,14341,2975],{"class":3008},[3002,14343,14115],{"class":3246},[3002,14345,3064],{"class":3008},[3002,14347,14348,14350,14352,14354,14356,14358,14360,14362],{"class":3004,"line":4420},[3002,14349,14122],{"class":3012},[3002,14351,11854],{"class":3042},[3002,14353,3046],{"class":3008},[3002,14355,3219],{"class":3218},[3002,14357,11861],{"class":3022},[3002,14359,3052],{"class":3008},[3002,14361,14135],{"class":3246},[3002,14363,3064],{"class":3008},[3002,14365,14366,14368,14370,14372],{"class":3004,"line":4425},[3002,14367,14142],{"class":3042},[3002,14369,3058],{"class":3008},[3002,14371,3276],{"class":3022},[3002,14373,3279],{"class":3008},[3002,14375,14376,14378,14380,14382],{"class":3004,"line":4432},[3002,14377,14153],{"class":3042},[3002,14379,3058],{"class":3008},[3002,14381,3276],{"class":3022},[3002,14383,11904],{"class":3008},[3002,14385,14386],{"class":3004,"line":4441},[3002,14387,3153],{"emptyLinePlaceholder":3152},[3002,14389,14390,14392,14394,14396,14398,14400,14402,14404],{"class":3004,"line":4464},[3002,14391,14168],{"class":3012},[3002,14393,12707],{"class":3042},[3002,14395,3046],{"class":3008},[3002,14397,3219],{"class":3218},[3002,14399,12714],{"class":3022},[3002,14401,3052],{"class":3008},[3002,14403,13089],{"class":3246},[3002,14405,12722],{"class":3008},[3002,14407,14408,14410,14412,14414,14416,14418],{"class":3004,"line":4479},[3002,14409,14187],{"class":3042},[3002,14411,3058],{"class":3008},[3002,14413,12732],{"class":3022},[3002,14415,3052],{"class":3008},[3002,14417,13105],{"class":3143},[3002,14419,3064],{"class":3008},[3002,14421,14422,14424,14426,14428,14430,14433,14435],{"class":3004,"line":4954},[3002,14423,14187],{"class":3042},[3002,14425,3058],{"class":3008},[3002,14427,12751],{"class":3022},[3002,14429,3052],{"class":3008},[3002,14431,14432],{"class":3143},"1800",[3002,14434,4208],{"class":3008},[3002,14436,14437],{"class":3032},"// раніше 1900 — порушення CHECK constraint\n",[3002,14439,14440],{"class":3004,"line":4972},[3002,14441,3153],{"emptyLinePlaceholder":3152},[3002,14443,14444],{"class":3004,"line":4990},[3002,14445,14225],{"class":3032},[3002,14447,14448,14450,14452,14454,14456,14458,14460],{"class":3004,"line":5007},[3002,14449,14235],{"class":3022},[3002,14451,9050],{"class":3008},[3002,14453,9053],{"class":4021},[3002,14455,12524],{"class":3042},[3002,14457,3058],{"class":3008},[3002,14459,3276],{"class":3022},[3002,14461,13152],{"class":3008},[3002,14463,14464,14466,14468,14470,14472,14474,14476,14478,14480,14482,14484,14486,14488,14490,14492],{"class":3004,"line":5016},[3002,14465,14252],{"class":3008},[3002,14467,9070],{"class":3022},[3002,14469,3052],{"class":3008},[3002,14471,9075],{"class":3042},[3002,14473,3058],{"class":3008},[3002,14475,9080],{"class":3042},[3002,14477,3058],{"class":3008},[3002,14479,9085],{"class":3042},[3002,14481,3058],{"class":3008},[3002,14483,9090],{"class":3042},[3002,14485,3058],{"class":3008},[3002,14487,9095],{"class":3042},[3002,14489,3058],{"class":3008},[3002,14491,3061],{"class":3042},[3002,14493,5187],{"class":3008},[3002,14495,14496,14498,14500,14502,14504],{"class":3004,"line":5487},[3002,14497,14252],{"class":3008},[3002,14499,9108],{"class":3022},[3002,14501,3052],{"class":3008},[3002,14503,14291],{"class":3246},[3002,14505,3064],{"class":3008},[3002,14507,14508],{"class":3004,"line":5492},[3002,14509,3310],{"class":3008},[3002,14511,14512],{"class":3004,"line":5504},[3002,14513,3153],{"emptyLinePlaceholder":3152},[3002,14515,14516,14518],{"class":3004,"line":5514},[3002,14517,3009],{"class":3008},[3002,14519,3013],{"class":3012},[3002,14521,14522,14524,14527],{"class":3004,"line":5552},[3002,14523,3019],{"class":3012},[3002,14525,14526],{"class":3022}," save_shouldThrowException_whenReleaseYearInFuture",[3002,14528,3026],{"class":3008},[3002,14530,14531],{"class":3004,"line":5571},[3002,14532,5796],{"class":3032},[3002,14534,14535,14537,14539,14541,14543,14545,14547,14549,14551,14553],{"class":3004,"line":5586},[3002,14536,3231],{"class":3012},[3002,14538,3234],{"class":3042},[3002,14540,3046],{"class":3008},[3002,14542,3219],{"class":3218},[3002,14544,3241],{"class":3022},[3002,14546,3052],{"class":3008},[3002,14548,10112],{"class":3246},[3002,14550,2975],{"class":3008},[3002,14552,14115],{"class":3246},[3002,14554,3064],{"class":3008},[3002,14556,14557,14559,14561,14563,14565,14567,14569,14571],{"class":3004,"line":5600},[3002,14558,14122],{"class":3012},[3002,14560,11854],{"class":3042},[3002,14562,3046],{"class":3008},[3002,14564,3219],{"class":3218},[3002,14566,11861],{"class":3022},[3002,14568,3052],{"class":3008},[3002,14570,14135],{"class":3246},[3002,14572,3064],{"class":3008},[3002,14574,14575,14577,14579,14581],{"class":3004,"line":5605},[3002,14576,14142],{"class":3042},[3002,14578,3058],{"class":3008},[3002,14580,3276],{"class":3022},[3002,14582,3279],{"class":3008},[3002,14584,14585,14587,14589,14591],{"class":3004,"line":5610},[3002,14586,14153],{"class":3042},[3002,14588,3058],{"class":3008},[3002,14590,3276],{"class":3022},[3002,14592,11904],{"class":3008},[3002,14594,14595],{"class":3004,"line":6796},[3002,14596,3153],{"emptyLinePlaceholder":3152},[3002,14598,14599,14601,14603,14605,14607,14609,14611,14613],{"class":3004,"line":6802},[3002,14600,14168],{"class":3012},[3002,14602,12707],{"class":3042},[3002,14604,3046],{"class":3008},[3002,14606,3219],{"class":3218},[3002,14608,12714],{"class":3022},[3002,14610,3052],{"class":3008},[3002,14612,13089],{"class":3246},[3002,14614,12722],{"class":3008},[3002,14616,14617,14619,14621,14623,14625,14627],{"class":3004,"line":6820},[3002,14618,14187],{"class":3042},[3002,14620,3058],{"class":3008},[3002,14622,12732],{"class":3022},[3002,14624,3052],{"class":3008},[3002,14626,13105],{"class":3143},[3002,14628,3064],{"class":3008},[3002,14630,14631,14633,14635,14637,14639,14642,14644],{"class":3004,"line":6825},[3002,14632,14187],{"class":3042},[3002,14634,3058],{"class":3008},[3002,14636,12751],{"class":3022},[3002,14638,3052],{"class":3008},[3002,14640,14641],{"class":3143},"2100",[3002,14643,4208],{"class":3008},[3002,14645,14646],{"class":3032},"// занадто далеко у майбутньому\n",[3002,14648,14649],{"class":3004,"line":6831},[3002,14650,3153],{"emptyLinePlaceholder":3152},[3002,14652,14653],{"class":3004,"line":6837},[3002,14654,14225],{"class":3032},[3002,14656,14657,14659,14661,14663,14665,14667,14669],{"class":3004,"line":6843},[3002,14658,14235],{"class":3022},[3002,14660,9050],{"class":3008},[3002,14662,9053],{"class":4021},[3002,14664,12524],{"class":3042},[3002,14666,3058],{"class":3008},[3002,14668,3276],{"class":3022},[3002,14670,13152],{"class":3008},[3002,14672,14673,14675,14677,14679,14681,14683,14685,14687,14689,14691,14693,14695,14697,14699,14701],{"class":3004,"line":6849},[3002,14674,14252],{"class":3008},[3002,14676,9070],{"class":3022},[3002,14678,3052],{"class":3008},[3002,14680,9075],{"class":3042},[3002,14682,3058],{"class":3008},[3002,14684,9080],{"class":3042},[3002,14686,3058],{"class":3008},[3002,14688,9085],{"class":3042},[3002,14690,3058],{"class":3008},[3002,14692,9090],{"class":3042},[3002,14694,3058],{"class":3008},[3002,14696,9095],{"class":3042},[3002,14698,3058],{"class":3008},[3002,14700,3061],{"class":3042},[3002,14702,5187],{"class":3008},[3002,14704,14705,14707,14709,14711,14713],{"class":3004,"line":6863},[3002,14706,14252],{"class":3008},[3002,14708,9108],{"class":3022},[3002,14710,3052],{"class":3008},[3002,14712,14291],{"class":3246},[3002,14714,3064],{"class":3008},[3002,14716,14717],{"class":3004,"line":6868},[3002,14718,3310],{"class":3008},[3002,14720,14721],{"class":3004,"line":6873},[3002,14722,3153],{"emptyLinePlaceholder":3152},[3002,14724,14725,14727],{"class":3004,"line":6879},[3002,14726,3009],{"class":3008},[3002,14728,3013],{"class":3012},[3002,14730,14731,14733,14736],{"class":3004,"line":6885},[3002,14732,3019],{"class":3012},[3002,14734,14735],{"class":3022}," save_shouldAcceptCurrentYearPlusOne_whenReleaseYearValid",[3002,14737,3026],{"class":3008},[3002,14739,14740],{"class":3004,"line":6891},[3002,14741,5796],{"class":3032},[3002,14743,14744,14746,14748,14750,14752,14754,14756,14758,14760,14762],{"class":3004,"line":6897},[3002,14745,3231],{"class":3012},[3002,14747,3234],{"class":3042},[3002,14749,3046],{"class":3008},[3002,14751,3219],{"class":3218},[3002,14753,3241],{"class":3022},[3002,14755,3052],{"class":3008},[3002,14757,10112],{"class":3246},[3002,14759,2975],{"class":3008},[3002,14761,14115],{"class":3246},[3002,14763,3064],{"class":3008},[3002,14765,14766,14768,14770,14772,14774,14776,14778,14780],{"class":3004,"line":6902},[3002,14767,14122],{"class":3012},[3002,14769,11854],{"class":3042},[3002,14771,3046],{"class":3008},[3002,14773,3219],{"class":3218},[3002,14775,11861],{"class":3022},[3002,14777,3052],{"class":3008},[3002,14779,14135],{"class":3246},[3002,14781,3064],{"class":3008},[3002,14783,14784,14786,14788,14790],{"class":3004,"line":6910},[3002,14785,14142],{"class":3042},[3002,14787,3058],{"class":3008},[3002,14789,3276],{"class":3022},[3002,14791,3279],{"class":3008},[3002,14793,14794,14796,14798,14800],{"class":3004,"line":6935},[3002,14795,14153],{"class":3042},[3002,14797,3058],{"class":3008},[3002,14799,3276],{"class":3022},[3002,14801,11904],{"class":3008},[3002,14803,14804],{"class":3004,"line":6941},[3002,14805,3153],{"emptyLinePlaceholder":3152},[3002,14807,14808,14811,14814,14816,14818,14820,14823,14825,14828,14830,14833,14835,14838,14840,14842],{"class":3004,"line":6984},[3002,14809,14810],{"class":3012},"    int",[3002,14812,14813],{"class":3042}," nextYear",[3002,14815,3046],{"class":3008},[3002,14817,2997],{"class":3042},[3002,14819,3058],{"class":3008},[3002,14821,14822],{"class":3042},"time",[3002,14824,3058],{"class":3008},[3002,14826,14827],{"class":3042},"Year",[3002,14829,3058],{"class":3008},[3002,14831,14832],{"class":3022},"now",[3002,14834,6961],{"class":3008},[3002,14836,14837],{"class":3022},"getValue",[3002,14839,3883],{"class":3008},[3002,14841,3144],{"class":3143},[3002,14843,3889],{"class":3008},[3002,14845,14846],{"class":3004,"line":6990},[3002,14847,3153],{"emptyLinePlaceholder":3152},[3002,14849,14850,14852,14854,14856,14858,14860,14862,14865],{"class":3004,"line":6996},[3002,14851,14168],{"class":3012},[3002,14853,12707],{"class":3042},[3002,14855,3046],{"class":3008},[3002,14857,3219],{"class":3218},[3002,14859,12714],{"class":3022},[3002,14861,3052],{"class":3008},[3002,14863,14864],{"class":3246},"\"Майбутня книга\"",[3002,14866,12722],{"class":3008},[3002,14868,14869,14871,14873,14875,14877,14879],{"class":3004,"line":7002},[3002,14870,14187],{"class":3042},[3002,14872,3058],{"class":3008},[3002,14874,12732],{"class":3022},[3002,14876,3052],{"class":3008},[3002,14878,13105],{"class":3143},[3002,14880,3064],{"class":3008},[3002,14882,14883,14885,14887,14889,14892],{"class":3004,"line":7008},[3002,14884,14187],{"class":3042},[3002,14886,3058],{"class":3008},[3002,14888,12751],{"class":3022},[3002,14890,14891],{"class":3008},"(nextYear); ",[3002,14893,14894],{"class":3032},"// наступний рік — допустимо\n",[3002,14896,14897],{"class":3004,"line":7028},[3002,14898,3153],{"emptyLinePlaceholder":3152},[3002,14900,14901],{"class":3004,"line":7050},[3002,14902,5859],{"class":3032},[3002,14904,14905,14908,14910,14912],{"class":3004,"line":7055},[3002,14906,14907],{"class":3042},"    audiobookRepo",[3002,14909,3058],{"class":3008},[3002,14911,3276],{"class":3022},[3002,14913,12793],{"class":3008},[3002,14915,14916],{"class":3004,"line":7061},[3002,14917,3153],{"emptyLinePlaceholder":3152},[3002,14919,14920],{"class":3004,"line":7073},[3002,14921,5878],{"class":3032},[3002,14923,14924,14926,14928,14930,14932,14934,14936,14938,14940,14942,14944,14946,14948],{"class":3004,"line":7079},[3002,14925,14168],{"class":3012},[3002,14927,3530],{"class":3042},[3002,14929,3046],{"class":3008},[3002,14931,12812],{"class":3042},[3002,14933,3058],{"class":3008},[3002,14935,3540],{"class":3022},[3002,14937,3052],{"class":3008},[3002,14939,12821],{"class":3042},[3002,14941,3058],{"class":3008},[3002,14943,3550],{"class":3022},[3002,14945,3136],{"class":3008},[3002,14947,3555],{"class":3022},[3002,14949,3304],{"class":3008},[3002,14951,14952,14954,14956,14958,14960,14962,14964,14966],{"class":3004,"line":7084},[3002,14953,3562],{"class":3022},[3002,14955,3052],{"class":3008},[3002,14957,3567],{"class":3042},[3002,14959,3058],{"class":3008},[3002,14961,12890],{"class":3022},[3002,14963,3136],{"class":3008},[3002,14965,3577],{"class":3022},[3002,14967,14968],{"class":3008},"(nextYear);\n",[3002,14970,14971],{"class":3004,"line":7089},[3002,14972,3310],{"class":3008},[2964,14974,14975],{},[2989,14976,14977],{},"Ключові спостереження:",[3332,14979,14980,14992,15001],{},[3335,14981,14982,5534,14985,14988,14989,14991],{},[2989,14983,14984],{},"Рядки 110–140",[2968,14986,14987],{},"deleteAuthor_shouldCascadeDeleteAudiobooks","): Перевіряє, що ",[2968,14990,5421],{}," працює коректно — видалення автора автоматично видаляє всі його книги.",[3335,14993,14994,5534,14997,15000],{},[2989,14995,14996],{},"Рядки 168–182",[2968,14998,14999],{},"save_shouldThrowException_whenDurationNegative","): Тест CHECK constraint — БД має відхилити негативну тривалість.",[3335,15002,15003,5534,15006,15009],{},[2989,15004,15005],{},"Рядки 218–234",[2968,15007,15008],{},"save_shouldAcceptCurrentYearPlusOne","): Позитивний тест граничного випадку — перевіряє, що наступний рік допустимий.",[3768,15011,15012,15017,15024,15047],{},[2964,15013,15014],{},[2989,15015,15016],{},"Чому тестувати constraints?",[2964,15018,15019,15020,15023],{},"Constraints — це ",[2989,15021,15022],{},"бізнес-правила на рівні БД",". Якщо вони не працюють, дані можуть стати некоректними:",[3332,15025,15026,15033,15040],{},[3335,15027,15028,15029,15032],{},"Без UNIQUE на ",[2968,15030,15031],{},"genres.name"," → дублікати жанрів",[3335,15034,15035,15036,15039],{},"Без FK на ",[2968,15037,15038],{},"audiobooks.author_id"," → «сироти» книги без автора",[3335,15041,15042,15043,15046],{},"Без CHECK на ",[2968,15044,15045],{},"duration > 0"," → книги з негативною тривалістю",[2964,15048,15049],{},"Інтеграційні тести гарантують, що ці правила дійсно працюють.",[3660,15051],{},[2959,15053,15055],{"id":15054},"test-data-builders-зменшення-дублювання","Test Data Builders: Зменшення дублювання",[2964,15057,15058],{},"У тестах вище ми багато разів повторювали код створення тестових об'єктів:",[2993,15060,15062],{"className":2995,"code":15061,"language":2997,"meta":2998,"style":2998},"Author author = new Author(\"Іван\", \"Франко\");\nauthor.setBio(\"Біографія\");\nauthor.setImagePath(\"/images/franko.jpg\");\n",[2968,15063,15064,15086,15101],{"__ignoreMap":2998},[3002,15065,15066,15068,15070,15072,15074,15076,15078,15080,15082,15084],{"class":3004,"line":3005},[3002,15067,4168],{"class":3012},[3002,15069,3234],{"class":3042},[3002,15071,3046],{"class":3008},[3002,15073,3219],{"class":3218},[3002,15075,3241],{"class":3022},[3002,15077,3052],{"class":3008},[3002,15079,3247],{"class":3246},[3002,15081,2975],{"class":3008},[3002,15083,3252],{"class":3246},[3002,15085,3064],{"class":3008},[3002,15087,15088,15090,15092,15094,15096,15099],{"class":3004,"line":3016},[3002,15089,3545],{"class":3042},[3002,15091,3058],{"class":3008},[3002,15093,3488],{"class":3022},[3002,15095,3052],{"class":3008},[3002,15097,15098],{"class":3246},"\"Біографія\"",[3002,15100,3064],{"class":3008},[3002,15102,15103,15105,15107,15109,15111,15113],{"class":3004,"line":3029},[3002,15104,3545],{"class":3042},[3002,15106,3058],{"class":3008},[3002,15108,8516],{"class":3022},[3002,15110,3052],{"class":3008},[3002,15112,8521],{"class":3246},[3002,15114,3064],{"class":3008},[2964,15116,15117,15118,15121],{},"Для складних об'єктів це призводить до значного дублювання. ",[2989,15119,15120],{},"Test Data Builder"," — це патерн, що спрощує створення тестових даних.",[3810,15123,15125],{"id":15124},"реалізація-authortestbuilder","Реалізація AuthorTestBuilder",[2993,15127,15129],{"className":2995,"code":15128,"language":2997,"meta":4616,"style":2998},"package com.example.audiobook.testutil;\n\nimport com.example.audiobook.domain.Author;\n\nimport java.util.UUID;\n\n/**\n * Builder для створення тестових об'єктів {@link Author}.\n * \u003Cp>\n * Надає значення за замовчуванням для всіх полів, що можна перевизначити\n * через fluent API.\n * \u003Cp>\n * \u003Cb>Використання:\u003C/b>\n * \u003Cpre>{@code\n * // Автор зі значеннями за замовчуванням\n * Author author = AuthorTestBuilder.anAuthor().build();\n *\n * // Автор з кастомними полями\n * Author author = AuthorTestBuilder.anAuthor()\n *     .withFirstName(\"Тарас\")\n *     .withLastName(\"Шевченко\")\n *     .withBio(\"Український поет\")\n *     .build();\n * }\u003C/pre>\n */\npublic class AuthorTestBuilder {\n\n    private UUID id = UUID.randomUUID();\n    private String firstName = \"Іван\";\n    private String lastName = \"Франко\";\n    private String bio = \"Тестова біографія автора\";\n    private String imagePath = \"/images/test-author.jpg\";\n\n    /**\n     * Статичний фабричний метод для початку побудови.\n     * Назва {@code anAuthor()} читається природно: \"an author with...\"\n     */\n    public static AuthorTestBuilder anAuthor() {\n        return new AuthorTestBuilder();\n    }\n\n    public AuthorTestBuilder withId(UUID id) {\n        this.id = id;\n        return this;\n    }\n\n    public AuthorTestBuilder withFirstName(String firstName) {\n        this.firstName = firstName;\n        return this;\n    }\n\n    public AuthorTestBuilder withLastName(String lastName) {\n        this.lastName = lastName;\n        return this;\n    }\n\n    public AuthorTestBuilder withBio(String bio) {\n        this.bio = bio;\n        return this;\n    }\n\n    public AuthorTestBuilder withImagePath(String imagePath) {\n        this.imagePath = imagePath;\n        return this;\n    }\n\n    /**\n     * Автор без біографії та зображення (null).\n     */\n    public AuthorTestBuilder withoutBio() {\n        this.bio = null;\n        return this;\n    }\n\n    public AuthorTestBuilder withoutImagePath() {\n        this.imagePath = null;\n        return this;\n    }\n\n    /**\n     * Будує об'єкт {@link Author} з налаштованими полями.\n     */\n    public Author build() {\n        Author author = new Author(firstName, lastName);\n        author.setId(id);\n        author.setBio(bio);\n        author.setImagePath(imagePath);\n        return author;\n    }\n}\n",[2968,15130,15131,15138,15142,15148,15152,15158,15162,15166,15171,15175,15180,15185,15189,15193,15197,15204,15209,15213,15220,15225,15230,15235,15240,15245,15249,15253,15264,15268,15288,15303,15318,15334,15350,15354,15358,15363,15368,15372,15386,15398,15402,15406,15423,15436,15445,15449,15453,15470,15482,15490,15494,15498,15515,15527,15535,15539,15543,15560,15572,15580,15584,15588,15605,15617,15625,15629,15633,15637,15642,15646,15657,15671,15679,15683,15687,15698,15712,15720,15724,15728,15732,15737,15741,15752,15767,15778,15789,15800,15807,15811],{"__ignoreMap":2998},[3002,15132,15133,15135],{"class":3004,"line":3005},[3002,15134,6550],{"class":4021},[3002,15136,15137],{"class":3008}," com.example.audiobook.testutil;\n",[3002,15139,15140],{"class":3004,"line":3016},[3002,15141,3153],{"emptyLinePlaceholder":3152},[3002,15143,15144,15146],{"class":3004,"line":3029},[3002,15145,6562],{"class":4021},[3002,15147,8264],{"class":3008},[3002,15149,15150],{"class":3004,"line":3036},[3002,15151,3153],{"emptyLinePlaceholder":3152},[3002,15153,15154,15156],{"class":3004,"line":3067},[3002,15155,6562],{"class":4021},[3002,15157,6639],{"class":3008},[3002,15159,15160],{"class":3004,"line":3091},[3002,15161,3153],{"emptyLinePlaceholder":3152},[3002,15163,15164],{"class":3004,"line":3121},[3002,15165,6655],{"class":3032},[3002,15167,15168],{"class":3004,"line":3149},[3002,15169,15170],{"class":3032}," * Builder для створення тестових об'єктів {@link Author}.\n",[3002,15172,15173],{"class":3004,"line":3156},[3002,15174,6665],{"class":3032},[3002,15176,15177],{"class":3004,"line":3180},[3002,15178,15179],{"class":3032}," * Надає значення за замовчуванням для всіх полів, що можна перевизначити\n",[3002,15181,15182],{"class":3004,"line":3202},[3002,15183,15184],{"class":3032}," * через fluent API.\n",[3002,15186,15187],{"class":3004,"line":3207},[3002,15188,6665],{"class":3032},[3002,15190,15191],{"class":3004,"line":3228},[3002,15192,6709],{"class":3032},[3002,15194,15195],{"class":3004,"line":3257},[3002,15196,6714],{"class":3032},[3002,15198,15199,15201],{"class":3004,"line":3262},[3002,15200,6776],{"class":3032},[3002,15202,15203],{"class":3032}," // Автор зі значеннями за замовчуванням\n",[3002,15205,15206],{"class":3004,"line":3268},[3002,15207,15208],{"class":3032}," * Author author = AuthorTestBuilder.anAuthor().build();\n",[3002,15210,15211],{"class":3004,"line":3282},[3002,15212,6729],{"class":3032},[3002,15214,15215,15217],{"class":3004,"line":3287},[3002,15216,6776],{"class":3032},[3002,15218,15219],{"class":3032}," // Автор з кастомними полями\n",[3002,15221,15222],{"class":3004,"line":3293},[3002,15223,15224],{"class":3032}," * Author author = AuthorTestBuilder.anAuthor()\n",[3002,15226,15227],{"class":3004,"line":3307},[3002,15228,15229],{"class":3032}," *     .withFirstName(\"Тарас\")\n",[3002,15231,15232],{"class":3004,"line":4214},[3002,15233,15234],{"class":3032}," *     .withLastName(\"Шевченко\")\n",[3002,15236,15237],{"class":3004,"line":4409},[3002,15238,15239],{"class":3032}," *     .withBio(\"Український поет\")\n",[3002,15241,15242],{"class":3004,"line":4415},[3002,15243,15244],{"class":3032}," *     .build();\n",[3002,15246,15247],{"class":3004,"line":4420},[3002,15248,6793],{"class":3032},[3002,15250,15251],{"class":3004,"line":4425},[3002,15252,6799],{"class":3032},[3002,15254,15255,15257,15259,15262],{"class":3004,"line":4432},[3002,15256,6805],{"class":4021},[3002,15258,6811],{"class":4021},[3002,15260,15261],{"class":3012}," AuthorTestBuilder",[3002,15263,6817],{"class":3008},[3002,15265,15266],{"class":3004,"line":4441},[3002,15267,3153],{"emptyLinePlaceholder":3152},[3002,15269,15270,15272,15275,15278,15280,15282,15284,15286],{"class":3004,"line":4464},[3002,15271,8381],{"class":4021},[3002,15273,15274],{"class":3012}," UUID",[3002,15276,15277],{"class":3042}," id",[3002,15279,3046],{"class":3008},[3002,15281,3875],{"class":3042},[3002,15283,3058],{"class":3008},[3002,15285,3880],{"class":3022},[3002,15287,3304],{"class":3008},[3002,15289,15290,15292,15294,15297,15299,15301],{"class":3004,"line":4479},[3002,15291,8381],{"class":4021},[3002,15293,7542],{"class":3012},[3002,15295,15296],{"class":3042}," firstName",[3002,15298,3046],{"class":3008},[3002,15300,3247],{"class":3246},[3002,15302,3889],{"class":3008},[3002,15304,15305,15307,15309,15312,15314,15316],{"class":3004,"line":4954},[3002,15306,8381],{"class":4021},[3002,15308,7542],{"class":3012},[3002,15310,15311],{"class":3042}," lastName",[3002,15313,3046],{"class":3008},[3002,15315,3252],{"class":3246},[3002,15317,3889],{"class":3008},[3002,15319,15320,15322,15324,15327,15329,15332],{"class":3004,"line":4972},[3002,15321,8381],{"class":4021},[3002,15323,7542],{"class":3012},[3002,15325,15326],{"class":3042}," bio",[3002,15328,3046],{"class":3008},[3002,15330,15331],{"class":3246},"\"Тестова біографія автора\"",[3002,15333,3889],{"class":3008},[3002,15335,15336,15338,15340,15343,15345,15348],{"class":3004,"line":4990},[3002,15337,8381],{"class":4021},[3002,15339,7542],{"class":3012},[3002,15341,15342],{"class":3042}," imagePath",[3002,15344,3046],{"class":3008},[3002,15346,15347],{"class":3246},"\"/images/test-author.jpg\"",[3002,15349,3889],{"class":3008},[3002,15351,15352],{"class":3004,"line":5007},[3002,15353,3153],{"emptyLinePlaceholder":3152},[3002,15355,15356],{"class":3004,"line":5016},[3002,15357,6828],{"class":3032},[3002,15359,15360],{"class":3004,"line":5487},[3002,15361,15362],{"class":3032},"     * Статичний фабричний метод для початку побудови.\n",[3002,15364,15365],{"class":3004,"line":5492},[3002,15366,15367],{"class":3032},"     * Назва {@code anAuthor()} читається природно: \"an author with...\"\n",[3002,15369,15370],{"class":3004,"line":5504},[3002,15371,6846],{"class":3032},[3002,15373,15374,15377,15379,15381,15384],{"class":3004,"line":5514},[3002,15375,15376],{"class":4021},"    public",[3002,15378,8319],{"class":4021},[3002,15380,15261],{"class":3012},[3002,15382,15383],{"class":3022}," anAuthor",[3002,15385,3026],{"class":3008},[3002,15387,15388,15391,15394,15396],{"class":3004,"line":5552},[3002,15389,15390],{"class":3218},"        return",[3002,15392,15393],{"class":3218}," new",[3002,15395,15261],{"class":3022},[3002,15397,3304],{"class":3008},[3002,15399,15400],{"class":3004,"line":5571},[3002,15401,7076],{"class":3008},[3002,15403,15404],{"class":3004,"line":5586},[3002,15405,3153],{"emptyLinePlaceholder":3152},[3002,15407,15408,15410,15412,15415,15417,15419,15421],{"class":3004,"line":5600},[3002,15409,15376],{"class":4021},[3002,15411,15261],{"class":3012},[3002,15413,15414],{"class":3022}," withId",[3002,15416,3052],{"class":3008},[3002,15418,3875],{"class":3012},[3002,15420,15277],{"class":3042},[3002,15422,7134],{"class":3008},[3002,15424,15425,15428,15430,15433],{"class":3004,"line":5605},[3002,15426,15427],{"class":4021},"        this",[3002,15429,3058],{"class":3008},[3002,15431,15432],{"class":3042},"id",[3002,15434,15435],{"class":3008}," = id;\n",[3002,15437,15438,15440,15443],{"class":3004,"line":5610},[3002,15439,15390],{"class":3218},[3002,15441,15442],{"class":4021}," this",[3002,15444,3889],{"class":3008},[3002,15446,15447],{"class":3004,"line":6796},[3002,15448,7076],{"class":3008},[3002,15450,15451],{"class":3004,"line":6802},[3002,15452,3153],{"emptyLinePlaceholder":3152},[3002,15454,15455,15457,15459,15462,15464,15466,15468],{"class":3004,"line":6820},[3002,15456,15376],{"class":4021},[3002,15458,15261],{"class":3012},[3002,15460,15461],{"class":3022}," withFirstName",[3002,15463,3052],{"class":3008},[3002,15465,3861],{"class":3012},[3002,15467,15296],{"class":3042},[3002,15469,7134],{"class":3008},[3002,15471,15472,15474,15476,15479],{"class":3004,"line":6825},[3002,15473,15427],{"class":4021},[3002,15475,3058],{"class":3008},[3002,15477,15478],{"class":3042},"firstName",[3002,15480,15481],{"class":3008}," = firstName;\n",[3002,15483,15484,15486,15488],{"class":3004,"line":6831},[3002,15485,15390],{"class":3218},[3002,15487,15442],{"class":4021},[3002,15489,3889],{"class":3008},[3002,15491,15492],{"class":3004,"line":6837},[3002,15493,7076],{"class":3008},[3002,15495,15496],{"class":3004,"line":6843},[3002,15497,3153],{"emptyLinePlaceholder":3152},[3002,15499,15500,15502,15504,15507,15509,15511,15513],{"class":3004,"line":6849},[3002,15501,15376],{"class":4021},[3002,15503,15261],{"class":3012},[3002,15505,15506],{"class":3022}," withLastName",[3002,15508,3052],{"class":3008},[3002,15510,3861],{"class":3012},[3002,15512,15311],{"class":3042},[3002,15514,7134],{"class":3008},[3002,15516,15517,15519,15521,15524],{"class":3004,"line":6863},[3002,15518,15427],{"class":4021},[3002,15520,3058],{"class":3008},[3002,15522,15523],{"class":3042},"lastName",[3002,15525,15526],{"class":3008}," = lastName;\n",[3002,15528,15529,15531,15533],{"class":3004,"line":6868},[3002,15530,15390],{"class":3218},[3002,15532,15442],{"class":4021},[3002,15534,3889],{"class":3008},[3002,15536,15537],{"class":3004,"line":6873},[3002,15538,7076],{"class":3008},[3002,15540,15541],{"class":3004,"line":6879},[3002,15542,3153],{"emptyLinePlaceholder":3152},[3002,15544,15545,15547,15549,15552,15554,15556,15558],{"class":3004,"line":6885},[3002,15546,15376],{"class":4021},[3002,15548,15261],{"class":3012},[3002,15550,15551],{"class":3022}," withBio",[3002,15553,3052],{"class":3008},[3002,15555,3861],{"class":3012},[3002,15557,15326],{"class":3042},[3002,15559,7134],{"class":3008},[3002,15561,15562,15564,15566,15569],{"class":3004,"line":6891},[3002,15563,15427],{"class":4021},[3002,15565,3058],{"class":3008},[3002,15567,15568],{"class":3042},"bio",[3002,15570,15571],{"class":3008}," = bio;\n",[3002,15573,15574,15576,15578],{"class":3004,"line":6897},[3002,15575,15390],{"class":3218},[3002,15577,15442],{"class":4021},[3002,15579,3889],{"class":3008},[3002,15581,15582],{"class":3004,"line":6902},[3002,15583,7076],{"class":3008},[3002,15585,15586],{"class":3004,"line":6910},[3002,15587,3153],{"emptyLinePlaceholder":3152},[3002,15589,15590,15592,15594,15597,15599,15601,15603],{"class":3004,"line":6935},[3002,15591,15376],{"class":4021},[3002,15593,15261],{"class":3012},[3002,15595,15596],{"class":3022}," withImagePath",[3002,15598,3052],{"class":3008},[3002,15600,3861],{"class":3012},[3002,15602,15342],{"class":3042},[3002,15604,7134],{"class":3008},[3002,15606,15607,15609,15611,15614],{"class":3004,"line":6941},[3002,15608,15427],{"class":4021},[3002,15610,3058],{"class":3008},[3002,15612,15613],{"class":3042},"imagePath",[3002,15615,15616],{"class":3008}," = imagePath;\n",[3002,15618,15619,15621,15623],{"class":3004,"line":6984},[3002,15620,15390],{"class":3218},[3002,15622,15442],{"class":4021},[3002,15624,3889],{"class":3008},[3002,15626,15627],{"class":3004,"line":6990},[3002,15628,7076],{"class":3008},[3002,15630,15631],{"class":3004,"line":6996},[3002,15632,3153],{"emptyLinePlaceholder":3152},[3002,15634,15635],{"class":3004,"line":7002},[3002,15636,6828],{"class":3032},[3002,15638,15639],{"class":3004,"line":7008},[3002,15640,15641],{"class":3032},"     * Автор без біографії та зображення (null).\n",[3002,15643,15644],{"class":3004,"line":7028},[3002,15645,6846],{"class":3032},[3002,15647,15648,15650,15652,15655],{"class":3004,"line":7050},[3002,15649,15376],{"class":4021},[3002,15651,15261],{"class":3012},[3002,15653,15654],{"class":3022}," withoutBio",[3002,15656,3026],{"class":3008},[3002,15658,15659,15661,15663,15665,15667,15669],{"class":3004,"line":7055},[3002,15660,15427],{"class":4021},[3002,15662,3058],{"class":3008},[3002,15664,15568],{"class":3042},[3002,15666,3046],{"class":3008},[3002,15668,7131],{"class":4021},[3002,15670,3889],{"class":3008},[3002,15672,15673,15675,15677],{"class":3004,"line":7061},[3002,15674,15390],{"class":3218},[3002,15676,15442],{"class":4021},[3002,15678,3889],{"class":3008},[3002,15680,15681],{"class":3004,"line":7073},[3002,15682,7076],{"class":3008},[3002,15684,15685],{"class":3004,"line":7079},[3002,15686,3153],{"emptyLinePlaceholder":3152},[3002,15688,15689,15691,15693,15696],{"class":3004,"line":7084},[3002,15690,15376],{"class":4021},[3002,15692,15261],{"class":3012},[3002,15694,15695],{"class":3022}," withoutImagePath",[3002,15697,3026],{"class":3008},[3002,15699,15700,15702,15704,15706,15708,15710],{"class":3004,"line":7089},[3002,15701,15427],{"class":4021},[3002,15703,3058],{"class":3008},[3002,15705,15613],{"class":3042},[3002,15707,3046],{"class":3008},[3002,15709,7131],{"class":4021},[3002,15711,3889],{"class":3008},[3002,15713,15714,15716,15718],{"class":3004,"line":7095},[3002,15715,15390],{"class":3218},[3002,15717,15442],{"class":4021},[3002,15719,3889],{"class":3008},[3002,15721,15722],{"class":3004,"line":7101},[3002,15723,7076],{"class":3008},[3002,15725,15726],{"class":3004,"line":7106},[3002,15727,3153],{"emptyLinePlaceholder":3152},[3002,15729,15730],{"class":3004,"line":7113},[3002,15731,6828],{"class":3032},[3002,15733,15734],{"class":3004,"line":7122},[3002,15735,15736],{"class":3032},"     * Будує об'єкт {@link Author} з налаштованими полями.\n",[3002,15738,15739],{"class":3004,"line":7137},[3002,15740,6846],{"class":3032},[3002,15742,15743,15745,15747,15750],{"class":3004,"line":7149},[3002,15744,15376],{"class":4021},[3002,15746,3241],{"class":3012},[3002,15748,15749],{"class":3022}," build",[3002,15751,3026],{"class":3008},[3002,15753,15754,15756,15758,15760,15762,15764],{"class":3004,"line":7155},[3002,15755,8473],{"class":3012},[3002,15757,3234],{"class":3042},[3002,15759,3046],{"class":3008},[3002,15761,3219],{"class":3218},[3002,15763,3241],{"class":3022},[3002,15765,15766],{"class":3008},"(firstName, lastName);\n",[3002,15768,15769,15771,15773,15775],{"class":3004,"line":7160},[3002,15770,8496],{"class":3042},[3002,15772,3058],{"class":3008},[3002,15774,8965],{"class":3022},[3002,15776,15777],{"class":3008},"(id);\n",[3002,15779,15780,15782,15784,15786],{"class":3004,"line":7165},[3002,15781,8496],{"class":3042},[3002,15783,3058],{"class":3008},[3002,15785,3488],{"class":3022},[3002,15787,15788],{"class":3008},"(bio);\n",[3002,15790,15791,15793,15795,15797],{"class":3004,"line":7171},[3002,15792,8496],{"class":3042},[3002,15794,3058],{"class":3008},[3002,15796,8516],{"class":3022},[3002,15798,15799],{"class":3008},"(imagePath);\n",[3002,15801,15802,15804],{"class":3004,"line":7177},[3002,15803,15390],{"class":3218},[3002,15805,15806],{"class":3008}," author;\n",[3002,15808,15809],{"class":3004,"line":7182},[3002,15810,7076],{"class":3008},[3002,15812,15813],{"class":3004,"line":7187},[3002,15814,3310],{"class":3008},[3810,15816,15818],{"id":15817},"використання-у-тестах","Використання у тестах",[2993,15820,15822],{"className":2995,"code":15821,"language":2997,"meta":4616,"style":2998},"import static com.example.audiobook.testutil.AuthorTestBuilder.anAuthor;\n\n@Test\nvoid save_shouldInsertNewAuthor_whenValidData() {\n    // ═══ Arrange ═══\n    // Замість 5 рядків коду — один рядок з builder\n    Author author = anAuthor()\n        .withFirstName(\"Тарас\")\n        .withLastName(\"Шевченко\")\n        .build();\n\n    // ═══ Act ═══\n    repository.save(author);\n\n    // ═══ Assert ═══\n    Author loaded = repository.findById(author.getId()).orElseThrow();\n    assertThat(loaded.getFirstName()).isEqualTo(\"Тарас\");\n    assertThat(loaded.getLastName()).isEqualTo(\"Шевченко\");\n}\n\n@Test\nvoid save_shouldHandleNullBio_whenBioNotProvided() {\n    // ═══ Arrange ═══\n    Author author = anAuthor()\n        .withoutBio()\n        .withoutImagePath()\n        .build();\n\n    // ═══ Act ═══\n    repository.save(author);\n\n    // ═══ Assert ═══\n    Author loaded = repository.findById(author.getId()).orElseThrow();\n    assertThat(loaded.getBio()).isNull();\n}\n\n@Test\nvoid findAll_shouldReturnAllAuthors_whenMultipleExist() {\n    // ═══ Arrange ═══\n    // Створити 3 авторів одним рядком кожен\n    repository.save(anAuthor().withLastName(\"Шевченко\").build());\n    repository.save(anAuthor().withLastName(\"Франко\").build());\n    repository.save(anAuthor().withLastName(\"Українка\").build());\n\n    // ═══ Act ═══\n    List\u003CAuthor> all = repository.findAll();\n\n    // ═══ Assert ═══\n    assertThat(all).hasSize(3);\n}\n",[2968,15823,15824,15833,15837,15843,15851,15855,15860,15873,15886,15899,15908,15912,15916,15927,15931,15935,15963,15985,16007,16011,16015,16021,16029,16033,16045,16054,16063,16071,16075,16079,16089,16093,16097,16125,16143,16147,16151,16157,16165,16169,16174,16201,16227,16253,16257,16261,16283,16287,16291,16305],{"__ignoreMap":2998},[3002,15825,15826,15828,15830],{"class":3004,"line":3005},[3002,15827,6562],{"class":4021},[3002,15829,8319],{"class":4021},[3002,15831,15832],{"class":3008}," com.example.audiobook.testutil.AuthorTestBuilder.anAuthor;\n",[3002,15834,15835],{"class":3004,"line":3016},[3002,15836,3153],{"emptyLinePlaceholder":3152},[3002,15838,15839,15841],{"class":3004,"line":3029},[3002,15840,3009],{"class":3008},[3002,15842,3013],{"class":3012},[3002,15844,15845,15847,15849],{"class":3004,"line":3036},[3002,15846,3019],{"class":3012},[3002,15848,3398],{"class":3022},[3002,15850,3026],{"class":3008},[3002,15852,15853],{"class":3004,"line":3067},[3002,15854,5796],{"class":3032},[3002,15856,15857],{"class":3004,"line":3091},[3002,15858,15859],{"class":3032},"    // Замість 5 рядків коду — один рядок з builder\n",[3002,15861,15862,15864,15866,15868,15871],{"class":3004,"line":3121},[3002,15863,3231],{"class":3012},[3002,15865,3234],{"class":3042},[3002,15867,3046],{"class":3008},[3002,15869,15870],{"class":3022},"anAuthor",[3002,15872,6354],{"class":3008},[3002,15874,15875,15877,15880,15882,15884],{"class":3004,"line":3149},[3002,15876,14252],{"class":3008},[3002,15878,15879],{"class":3022},"withFirstName",[3002,15881,3052],{"class":3008},[3002,15883,5828],{"class":3246},[3002,15885,5187],{"class":3008},[3002,15887,15888,15890,15893,15895,15897],{"class":3004,"line":3156},[3002,15889,14252],{"class":3008},[3002,15891,15892],{"class":3022},"withLastName",[3002,15894,3052],{"class":3008},[3002,15896,5833],{"class":3246},[3002,15898,5187],{"class":3008},[3002,15900,15901,15903,15906],{"class":3004,"line":3180},[3002,15902,14252],{"class":3008},[3002,15904,15905],{"class":3022},"build",[3002,15907,3304],{"class":3008},[3002,15909,15910],{"class":3004,"line":3202},[3002,15911,3153],{"emptyLinePlaceholder":3152},[3002,15913,15914],{"class":3004,"line":3207},[3002,15915,5859],{"class":3032},[3002,15917,15918,15921,15923,15925],{"class":3004,"line":3228},[3002,15919,15920],{"class":3042},"    repository",[3002,15922,3058],{"class":3008},[3002,15924,3276],{"class":3022},[3002,15926,3279],{"class":3008},[3002,15928,15929],{"class":3004,"line":3257},[3002,15930,3153],{"emptyLinePlaceholder":3152},[3002,15932,15933],{"class":3004,"line":3262},[3002,15934,5878],{"class":3032},[3002,15936,15937,15939,15941,15943,15945,15947,15949,15951,15953,15955,15957,15959,15961],{"class":3004,"line":3268},[3002,15938,3231],{"class":3012},[3002,15940,3530],{"class":3042},[3002,15942,3046],{"class":3008},[3002,15944,8596],{"class":3042},[3002,15946,3058],{"class":3008},[3002,15948,3540],{"class":3022},[3002,15950,3052],{"class":3008},[3002,15952,3545],{"class":3042},[3002,15954,3058],{"class":3008},[3002,15956,3550],{"class":3022},[3002,15958,3136],{"class":3008},[3002,15960,3555],{"class":3022},[3002,15962,3304],{"class":3008},[3002,15964,15965,15967,15969,15971,15973,15975,15977,15979,15981,15983],{"class":3004,"line":3282},[3002,15966,3562],{"class":3022},[3002,15968,3052],{"class":3008},[3002,15970,3567],{"class":3042},[3002,15972,3058],{"class":3008},[3002,15974,3572],{"class":3022},[3002,15976,3136],{"class":3008},[3002,15978,3577],{"class":3022},[3002,15980,3052],{"class":3008},[3002,15982,5828],{"class":3246},[3002,15984,3064],{"class":3008},[3002,15986,15987,15989,15991,15993,15995,15997,15999,16001,16003,16005],{"class":3004,"line":3287},[3002,15988,3562],{"class":3022},[3002,15990,3052],{"class":3008},[3002,15992,3567],{"class":3042},[3002,15994,3058],{"class":3008},[3002,15996,3596],{"class":3022},[3002,15998,3136],{"class":3008},[3002,16000,3577],{"class":3022},[3002,16002,3052],{"class":3008},[3002,16004,5833],{"class":3246},[3002,16006,3064],{"class":3008},[3002,16008,16009],{"class":3004,"line":3293},[3002,16010,3310],{"class":3008},[3002,16012,16013],{"class":3004,"line":3307},[3002,16014,3153],{"emptyLinePlaceholder":3152},[3002,16016,16017,16019],{"class":3004,"line":4214},[3002,16018,3009],{"class":3008},[3002,16020,3013],{"class":3012},[3002,16022,16023,16025,16027],{"class":3004,"line":4409},[3002,16024,3019],{"class":3012},[3002,16026,8757],{"class":3022},[3002,16028,3026],{"class":3008},[3002,16030,16031],{"class":3004,"line":4415},[3002,16032,5796],{"class":3032},[3002,16034,16035,16037,16039,16041,16043],{"class":3004,"line":4420},[3002,16036,3231],{"class":3012},[3002,16038,3234],{"class":3042},[3002,16040,3046],{"class":3008},[3002,16042,15870],{"class":3022},[3002,16044,6354],{"class":3008},[3002,16046,16047,16049,16052],{"class":3004,"line":4425},[3002,16048,14252],{"class":3008},[3002,16050,16051],{"class":3022},"withoutBio",[3002,16053,6354],{"class":3008},[3002,16055,16056,16058,16061],{"class":3004,"line":4432},[3002,16057,14252],{"class":3008},[3002,16059,16060],{"class":3022},"withoutImagePath",[3002,16062,6354],{"class":3008},[3002,16064,16065,16067,16069],{"class":3004,"line":4441},[3002,16066,14252],{"class":3008},[3002,16068,15905],{"class":3022},[3002,16070,3304],{"class":3008},[3002,16072,16073],{"class":3004,"line":4464},[3002,16074,3153],{"emptyLinePlaceholder":3152},[3002,16076,16077],{"class":3004,"line":4479},[3002,16078,5859],{"class":3032},[3002,16080,16081,16083,16085,16087],{"class":3004,"line":4954},[3002,16082,15920],{"class":3042},[3002,16084,3058],{"class":3008},[3002,16086,3276],{"class":3022},[3002,16088,3279],{"class":3008},[3002,16090,16091],{"class":3004,"line":4972},[3002,16092,3153],{"emptyLinePlaceholder":3152},[3002,16094,16095],{"class":3004,"line":4990},[3002,16096,5878],{"class":3032},[3002,16098,16099,16101,16103,16105,16107,16109,16111,16113,16115,16117,16119,16121,16123],{"class":3004,"line":5007},[3002,16100,3231],{"class":3012},[3002,16102,3530],{"class":3042},[3002,16104,3046],{"class":3008},[3002,16106,8596],{"class":3042},[3002,16108,3058],{"class":3008},[3002,16110,3540],{"class":3022},[3002,16112,3052],{"class":3008},[3002,16114,3545],{"class":3042},[3002,16116,3058],{"class":3008},[3002,16118,3550],{"class":3022},[3002,16120,3136],{"class":3008},[3002,16122,3555],{"class":3022},[3002,16124,3304],{"class":3008},[3002,16126,16127,16129,16131,16133,16135,16137,16139,16141],{"class":3004,"line":5016},[3002,16128,3562],{"class":3022},[3002,16130,3052],{"class":3008},[3002,16132,3567],{"class":3042},[3002,16134,3058],{"class":3008},[3002,16136,3619],{"class":3022},[3002,16138,3136],{"class":3008},[3002,16140,8863],{"class":3022},[3002,16142,3304],{"class":3008},[3002,16144,16145],{"class":3004,"line":5487},[3002,16146,3310],{"class":3008},[3002,16148,16149],{"class":3004,"line":5492},[3002,16150,3153],{"emptyLinePlaceholder":3152},[3002,16152,16153,16155],{"class":3004,"line":5504},[3002,16154,3009],{"class":3008},[3002,16156,3013],{"class":3012},[3002,16158,16159,16161,16163],{"class":3004,"line":5514},[3002,16160,3019],{"class":3012},[3002,16162,9450],{"class":3022},[3002,16164,3026],{"class":3008},[3002,16166,16167],{"class":3004,"line":5552},[3002,16168,5796],{"class":3032},[3002,16170,16171],{"class":3004,"line":5571},[3002,16172,16173],{"class":3032},"    // Створити 3 авторів одним рядком кожен\n",[3002,16175,16176,16178,16180,16182,16184,16186,16188,16190,16192,16194,16197,16199],{"class":3004,"line":5586},[3002,16177,15920],{"class":3042},[3002,16179,3058],{"class":3008},[3002,16181,3276],{"class":3022},[3002,16183,3052],{"class":3008},[3002,16185,15870],{"class":3022},[3002,16187,6961],{"class":3008},[3002,16189,15892],{"class":3022},[3002,16191,3052],{"class":3008},[3002,16193,5833],{"class":3246},[3002,16195,16196],{"class":3008},").",[3002,16198,15905],{"class":3022},[3002,16200,11395],{"class":3008},[3002,16202,16203,16205,16207,16209,16211,16213,16215,16217,16219,16221,16223,16225],{"class":3004,"line":5600},[3002,16204,15920],{"class":3042},[3002,16206,3058],{"class":3008},[3002,16208,3276],{"class":3022},[3002,16210,3052],{"class":3008},[3002,16212,15870],{"class":3022},[3002,16214,6961],{"class":3008},[3002,16216,15892],{"class":3022},[3002,16218,3052],{"class":3008},[3002,16220,3252],{"class":3246},[3002,16222,16196],{"class":3008},[3002,16224,15905],{"class":3022},[3002,16226,11395],{"class":3008},[3002,16228,16229,16231,16233,16235,16237,16239,16241,16243,16245,16247,16249,16251],{"class":3004,"line":5605},[3002,16230,15920],{"class":3042},[3002,16232,3058],{"class":3008},[3002,16234,3276],{"class":3022},[3002,16236,3052],{"class":3008},[3002,16238,15870],{"class":3022},[3002,16240,6961],{"class":3008},[3002,16242,15892],{"class":3022},[3002,16244,3052],{"class":3008},[3002,16246,8785],{"class":3246},[3002,16248,16196],{"class":3008},[3002,16250,15905],{"class":3022},[3002,16252,11395],{"class":3008},[3002,16254,16255],{"class":3004,"line":5610},[3002,16256,3153],{"emptyLinePlaceholder":3152},[3002,16258,16259],{"class":3004,"line":6796},[3002,16260,5859],{"class":3032},[3002,16262,16263,16265,16267,16269,16271,16273,16275,16277,16279,16281],{"class":3004,"line":6802},[3002,16264,4162],{"class":3012},[3002,16266,4165],{"class":3008},[3002,16268,4168],{"class":3012},[3002,16270,4171],{"class":3008},[3002,16272,4174],{"class":3042},[3002,16274,3046],{"class":3008},[3002,16276,8596],{"class":3042},[3002,16278,3058],{"class":3008},[3002,16280,4183],{"class":3022},[3002,16282,3304],{"class":3008},[3002,16284,16285],{"class":3004,"line":6820},[3002,16286,3153],{"emptyLinePlaceholder":3152},[3002,16288,16289],{"class":3004,"line":6825},[3002,16290,5878],{"class":3032},[3002,16292,16293,16295,16297,16299,16301,16303],{"class":3004,"line":6831},[3002,16294,3562],{"class":3022},[3002,16296,4197],{"class":3008},[3002,16298,4200],{"class":3022},[3002,16300,3052],{"class":3008},[3002,16302,9612],{"class":3143},[3002,16304,3064],{"class":3008},[3002,16306,16307],{"class":3004,"line":6837},[3002,16308,3310],{"class":3008},[2964,16310,16311],{},[2989,16312,16313],{},"Переваги Test Data Builders:",[3332,16315,16316,16327,16333,16339],{},[3335,16317,16318,16319,16322,16323,16326],{},"✅ ",[2989,16320,16321],{},"Читабельність:"," ",[2968,16324,16325],{},"anAuthor().withFirstName(\"Тарас\")"," читається як англійське речення",[3335,16328,16318,16329,16332],{},[2989,16330,16331],{},"Значення за замовчуванням:"," Не потрібно встановлювати всі поля — лише ті, що важливі для тесту",[3335,16334,16318,16335,16338],{},[2989,16336,16337],{},"Зменшення дублювання:"," Спільна логіка створення об'єктів в одному місці",[3335,16340,16318,16341,16344,16345,16347],{},[2989,16342,16343],{},"Легкість змін:"," Якщо додається нове поле до ",[2968,16346,4168],{}," — змінюється лише builder",[3810,16349,16351],{"id":16350},"builders-для-інших-сутностей","Builders для інших сутностей",[2993,16353,16355],{"className":2995,"code":16354,"language":2997,"meta":4616,"style":2998},"package com.example.audiobook.testutil;\n\nimport com.example.audiobook.domain.Genre;\n\nimport java.util.UUID;\n\npublic class GenreTestBuilder {\n\n    private UUID id = UUID.randomUUID();\n    private String name = \"Тестовий жанр\";\n    private String description = \"Опис тестового жанру\";\n\n    public static GenreTestBuilder aGenre() {\n        return new GenreTestBuilder();\n    }\n\n    public GenreTestBuilder withId(UUID id) {\n        this.id = id;\n        return this;\n    }\n\n    public GenreTestBuilder withName(String name) {\n        this.name = name;\n        return this;\n    }\n\n    public GenreTestBuilder withDescription(String description) {\n        this.description = description;\n        return this;\n    }\n\n    public Genre build() {\n        Genre genre = new Genre(name);\n        genre.setId(id);\n        genre.setDescription(description);\n        return genre;\n    }\n}\n",[2968,16356,16357,16363,16367,16373,16377,16383,16387,16398,16402,16420,16436,16452,16456,16469,16479,16483,16487,16503,16513,16521,16525,16529,16546,16558,16566,16570,16574,16591,16603,16611,16615,16619,16629,16644,16654,16665,16672,16676],{"__ignoreMap":2998},[3002,16358,16359,16361],{"class":3004,"line":3005},[3002,16360,6550],{"class":4021},[3002,16362,15137],{"class":3008},[3002,16364,16365],{"class":3004,"line":3016},[3002,16366,3153],{"emptyLinePlaceholder":3152},[3002,16368,16369,16371],{"class":3004,"line":3029},[3002,16370,6562],{"class":4021},[3002,16372,11714],{"class":3008},[3002,16374,16375],{"class":3004,"line":3036},[3002,16376,3153],{"emptyLinePlaceholder":3152},[3002,16378,16379,16381],{"class":3004,"line":3067},[3002,16380,6562],{"class":4021},[3002,16382,6639],{"class":3008},[3002,16384,16385],{"class":3004,"line":3091},[3002,16386,3153],{"emptyLinePlaceholder":3152},[3002,16388,16389,16391,16393,16396],{"class":3004,"line":3121},[3002,16390,6805],{"class":4021},[3002,16392,6811],{"class":4021},[3002,16394,16395],{"class":3012}," GenreTestBuilder",[3002,16397,6817],{"class":3008},[3002,16399,16400],{"class":3004,"line":3149},[3002,16401,3153],{"emptyLinePlaceholder":3152},[3002,16403,16404,16406,16408,16410,16412,16414,16416,16418],{"class":3004,"line":3156},[3002,16405,8381],{"class":4021},[3002,16407,15274],{"class":3012},[3002,16409,15277],{"class":3042},[3002,16411,3046],{"class":3008},[3002,16413,3875],{"class":3042},[3002,16415,3058],{"class":3008},[3002,16417,3880],{"class":3022},[3002,16419,3304],{"class":3008},[3002,16421,16422,16424,16426,16429,16431,16434],{"class":3004,"line":3180},[3002,16423,8381],{"class":4021},[3002,16425,7542],{"class":3012},[3002,16427,16428],{"class":3042}," name",[3002,16430,3046],{"class":3008},[3002,16432,16433],{"class":3246},"\"Тестовий жанр\"",[3002,16435,3889],{"class":3008},[3002,16437,16438,16440,16442,16445,16447,16450],{"class":3004,"line":3202},[3002,16439,8381],{"class":4021},[3002,16441,7542],{"class":3012},[3002,16443,16444],{"class":3042}," description",[3002,16446,3046],{"class":3008},[3002,16448,16449],{"class":3246},"\"Опис тестового жанру\"",[3002,16451,3889],{"class":3008},[3002,16453,16454],{"class":3004,"line":3207},[3002,16455,3153],{"emptyLinePlaceholder":3152},[3002,16457,16458,16460,16462,16464,16467],{"class":3004,"line":3228},[3002,16459,15376],{"class":4021},[3002,16461,8319],{"class":4021},[3002,16463,16395],{"class":3012},[3002,16465,16466],{"class":3022}," aGenre",[3002,16468,3026],{"class":3008},[3002,16470,16471,16473,16475,16477],{"class":3004,"line":3257},[3002,16472,15390],{"class":3218},[3002,16474,15393],{"class":3218},[3002,16476,16395],{"class":3022},[3002,16478,3304],{"class":3008},[3002,16480,16481],{"class":3004,"line":3262},[3002,16482,7076],{"class":3008},[3002,16484,16485],{"class":3004,"line":3268},[3002,16486,3153],{"emptyLinePlaceholder":3152},[3002,16488,16489,16491,16493,16495,16497,16499,16501],{"class":3004,"line":3282},[3002,16490,15376],{"class":4021},[3002,16492,16395],{"class":3012},[3002,16494,15414],{"class":3022},[3002,16496,3052],{"class":3008},[3002,16498,3875],{"class":3012},[3002,16500,15277],{"class":3042},[3002,16502,7134],{"class":3008},[3002,16504,16505,16507,16509,16511],{"class":3004,"line":3287},[3002,16506,15427],{"class":4021},[3002,16508,3058],{"class":3008},[3002,16510,15432],{"class":3042},[3002,16512,15435],{"class":3008},[3002,16514,16515,16517,16519],{"class":3004,"line":3293},[3002,16516,15390],{"class":3218},[3002,16518,15442],{"class":4021},[3002,16520,3889],{"class":3008},[3002,16522,16523],{"class":3004,"line":3307},[3002,16524,7076],{"class":3008},[3002,16526,16527],{"class":3004,"line":4214},[3002,16528,3153],{"emptyLinePlaceholder":3152},[3002,16530,16531,16533,16535,16538,16540,16542,16544],{"class":3004,"line":4409},[3002,16532,15376],{"class":4021},[3002,16534,16395],{"class":3012},[3002,16536,16537],{"class":3022}," withName",[3002,16539,3052],{"class":3008},[3002,16541,3861],{"class":3012},[3002,16543,16428],{"class":3042},[3002,16545,7134],{"class":3008},[3002,16547,16548,16550,16552,16555],{"class":3004,"line":4415},[3002,16549,15427],{"class":4021},[3002,16551,3058],{"class":3008},[3002,16553,16554],{"class":3042},"name",[3002,16556,16557],{"class":3008}," = name;\n",[3002,16559,16560,16562,16564],{"class":3004,"line":4420},[3002,16561,15390],{"class":3218},[3002,16563,15442],{"class":4021},[3002,16565,3889],{"class":3008},[3002,16567,16568],{"class":3004,"line":4425},[3002,16569,7076],{"class":3008},[3002,16571,16572],{"class":3004,"line":4432},[3002,16573,3153],{"emptyLinePlaceholder":3152},[3002,16575,16576,16578,16580,16583,16585,16587,16589],{"class":3004,"line":4441},[3002,16577,15376],{"class":4021},[3002,16579,16395],{"class":3012},[3002,16581,16582],{"class":3022}," withDescription",[3002,16584,3052],{"class":3008},[3002,16586,3861],{"class":3012},[3002,16588,16444],{"class":3042},[3002,16590,7134],{"class":3008},[3002,16592,16593,16595,16597,16600],{"class":3004,"line":4464},[3002,16594,15427],{"class":4021},[3002,16596,3058],{"class":3008},[3002,16598,16599],{"class":3042},"description",[3002,16601,16602],{"class":3008}," = description;\n",[3002,16604,16605,16607,16609],{"class":3004,"line":4479},[3002,16606,15390],{"class":3218},[3002,16608,15442],{"class":4021},[3002,16610,3889],{"class":3008},[3002,16612,16613],{"class":3004,"line":4954},[3002,16614,7076],{"class":3008},[3002,16616,16617],{"class":3004,"line":4972},[3002,16618,3153],{"emptyLinePlaceholder":3152},[3002,16620,16621,16623,16625,16627],{"class":3004,"line":4990},[3002,16622,15376],{"class":4021},[3002,16624,11861],{"class":3012},[3002,16626,15749],{"class":3022},[3002,16628,3026],{"class":3008},[3002,16630,16631,16633,16635,16637,16639,16641],{"class":3004,"line":5007},[3002,16632,11851],{"class":3012},[3002,16634,11854],{"class":3042},[3002,16636,3046],{"class":3008},[3002,16638,3219],{"class":3218},[3002,16640,11861],{"class":3022},[3002,16642,16643],{"class":3008},"(name);\n",[3002,16645,16646,16648,16650,16652],{"class":3004,"line":5016},[3002,16647,11873],{"class":3042},[3002,16649,3058],{"class":3008},[3002,16651,8965],{"class":3022},[3002,16653,15777],{"class":3008},[3002,16655,16656,16658,16660,16662],{"class":3004,"line":5487},[3002,16657,11873],{"class":3042},[3002,16659,3058],{"class":3008},[3002,16661,11878],{"class":3022},[3002,16663,16664],{"class":3008},"(description);\n",[3002,16666,16667,16669],{"class":3004,"line":5492},[3002,16668,15390],{"class":3218},[3002,16670,16671],{"class":3008}," genre;\n",[3002,16673,16674],{"class":3004,"line":5504},[3002,16675,7076],{"class":3008},[3002,16677,16678],{"class":3004,"line":5514},[3002,16679,3310],{"class":3008},[2993,16681,16683],{"className":2995,"code":16682,"language":2997,"meta":4616,"style":2998},"package com.example.audiobook.testutil;\n\nimport com.example.audiobook.domain.Author;\nimport com.example.audiobook.domain.Audiobook;\nimport com.example.audiobook.domain.Genre;\n\nimport java.util.UUID;\n\nimport static com.example.audiobook.testutil.AuthorTestBuilder.anAuthor;\nimport static com.example.audiobook.testutil.GenreTestBuilder.aGenre;\n\npublic class AudiobookTestBuilder {\n\n    private UUID id = UUID.randomUUID();\n    private String title = \"Тестова аудіокнига\";\n    private Author author = anAuthor().build(); // за замовчуванням — тестовий автор\n    private Genre genre = aGenre().build();     // за замовчуванням — тестовий жанр\n    private int duration = 3600; // 1 година\n    private int releaseYear = 2020;\n    private String description = \"Опис тестової аудіокниги\";\n    private String coverImagePath = \"/images/test-cover.jpg\";\n\n    public static AudiobookTestBuilder anAudiobook() {\n        return new AudiobookTestBuilder();\n    }\n\n    public AudiobookTestBuilder withId(UUID id) {\n        this.id = id;\n        return this;\n    }\n\n    public AudiobookTestBuilder withTitle(String title) {\n        this.title = title;\n        return this;\n    }\n\n    public AudiobookTestBuilder withAuthor(Author author) {\n        this.author = author;\n        return this;\n    }\n\n    public AudiobookTestBuilder withGenre(Genre genre) {\n        this.genre = genre;\n        return this;\n    }\n\n    public AudiobookTestBuilder withDuration(int duration) {\n        this.duration = duration;\n        return this;\n    }\n\n    public AudiobookTestBuilder withReleaseYear(int releaseYear) {\n        this.releaseYear = releaseYear;\n        return this;\n    }\n\n    public AudiobookTestBuilder withDescription(String description) {\n        this.description = description;\n        return this;\n    }\n\n    public AudiobookTestBuilder withCoverImagePath(String coverImagePath) {\n        this.coverImagePath = coverImagePath;\n        return this;\n    }\n\n    public Audiobook build() {\n        Audiobook book = new Audiobook(title, author, genre);\n        book.setId(id);\n        book.setDuration(duration);\n        book.setReleaseYear(releaseYear);\n        book.setDescription(description);\n        book.setCoverImagePath(coverImagePath);\n        return book;\n    }\n}\n",[2968,16684,16685,16691,16695,16701,16707,16713,16717,16723,16727,16735,16744,16748,16759,16763,16781,16797,16818,16841,16862,16877,16892,16908,16912,16925,16935,16939,16943,16959,16969,16977,16981,16985,17002,17014,17022,17026,17030,17047,17058,17066,17070,17074,17092,17103,17111,17115,17119,17137,17149,17157,17161,17165,17182,17194,17202,17206,17210,17226,17236,17244,17248,17252,17269,17281,17289,17293,17297,17307,17322,17332,17343,17354,17364,17376,17383,17387],{"__ignoreMap":2998},[3002,16686,16687,16689],{"class":3004,"line":3005},[3002,16688,6550],{"class":4021},[3002,16690,15137],{"class":3008},[3002,16692,16693],{"class":3004,"line":3016},[3002,16694,3153],{"emptyLinePlaceholder":3152},[3002,16696,16697,16699],{"class":3004,"line":3029},[3002,16698,6562],{"class":4021},[3002,16700,8264],{"class":3008},[3002,16702,16703,16705],{"class":3004,"line":3036},[3002,16704,6562],{"class":4021},[3002,16706,12418],{"class":3008},[3002,16708,16709,16711],{"class":3004,"line":3067},[3002,16710,6562],{"class":4021},[3002,16712,11714],{"class":3008},[3002,16714,16715],{"class":3004,"line":3091},[3002,16716,3153],{"emptyLinePlaceholder":3152},[3002,16718,16719,16721],{"class":3004,"line":3121},[3002,16720,6562],{"class":4021},[3002,16722,6639],{"class":3008},[3002,16724,16725],{"class":3004,"line":3149},[3002,16726,3153],{"emptyLinePlaceholder":3152},[3002,16728,16729,16731,16733],{"class":3004,"line":3156},[3002,16730,6562],{"class":4021},[3002,16732,8319],{"class":4021},[3002,16734,15832],{"class":3008},[3002,16736,16737,16739,16741],{"class":3004,"line":3180},[3002,16738,6562],{"class":4021},[3002,16740,8319],{"class":4021},[3002,16742,16743],{"class":3008}," com.example.audiobook.testutil.GenreTestBuilder.aGenre;\n",[3002,16745,16746],{"class":3004,"line":3202},[3002,16747,3153],{"emptyLinePlaceholder":3152},[3002,16749,16750,16752,16754,16757],{"class":3004,"line":3207},[3002,16751,6805],{"class":4021},[3002,16753,6811],{"class":4021},[3002,16755,16756],{"class":3012}," AudiobookTestBuilder",[3002,16758,6817],{"class":3008},[3002,16760,16761],{"class":3004,"line":3228},[3002,16762,3153],{"emptyLinePlaceholder":3152},[3002,16764,16765,16767,16769,16771,16773,16775,16777,16779],{"class":3004,"line":3257},[3002,16766,8381],{"class":4021},[3002,16768,15274],{"class":3012},[3002,16770,15277],{"class":3042},[3002,16772,3046],{"class":3008},[3002,16774,3875],{"class":3042},[3002,16776,3058],{"class":3008},[3002,16778,3880],{"class":3022},[3002,16780,3304],{"class":3008},[3002,16782,16783,16785,16787,16790,16792,16795],{"class":3004,"line":3262},[3002,16784,8381],{"class":4021},[3002,16786,7542],{"class":3012},[3002,16788,16789],{"class":3042}," title",[3002,16791,3046],{"class":3008},[3002,16793,16794],{"class":3246},"\"Тестова аудіокнига\"",[3002,16796,3889],{"class":3008},[3002,16798,16799,16801,16803,16805,16807,16809,16811,16813,16815],{"class":3004,"line":3268},[3002,16800,8381],{"class":4021},[3002,16802,3241],{"class":3012},[3002,16804,3234],{"class":3042},[3002,16806,3046],{"class":3008},[3002,16808,15870],{"class":3022},[3002,16810,6961],{"class":3008},[3002,16812,15905],{"class":3022},[3002,16814,4357],{"class":3008},[3002,16816,16817],{"class":3032},"// за замовчуванням — тестовий автор\n",[3002,16819,16820,16822,16824,16826,16828,16831,16833,16835,16838],{"class":3004,"line":3282},[3002,16821,8381],{"class":4021},[3002,16823,11861],{"class":3012},[3002,16825,11854],{"class":3042},[3002,16827,3046],{"class":3008},[3002,16829,16830],{"class":3022},"aGenre",[3002,16832,6961],{"class":3008},[3002,16834,15905],{"class":3022},[3002,16836,16837],{"class":3008},"();     ",[3002,16839,16840],{"class":3032},"// за замовчуванням — тестовий жанр\n",[3002,16842,16843,16845,16848,16851,16853,16856,16859],{"class":3004,"line":3287},[3002,16844,8381],{"class":4021},[3002,16846,16847],{"class":3012}," int",[3002,16849,16850],{"class":3042}," duration",[3002,16852,3046],{"class":3008},[3002,16854,16855],{"class":3143},"3600",[3002,16857,16858],{"class":3008},"; ",[3002,16860,16861],{"class":3032},"// 1 година\n",[3002,16863,16864,16866,16868,16871,16873,16875],{"class":3004,"line":3293},[3002,16865,8381],{"class":4021},[3002,16867,16847],{"class":3012},[3002,16869,16870],{"class":3042}," releaseYear",[3002,16872,3046],{"class":3008},[3002,16874,13120],{"class":3143},[3002,16876,3889],{"class":3008},[3002,16878,16879,16881,16883,16885,16887,16890],{"class":3004,"line":3307},[3002,16880,8381],{"class":4021},[3002,16882,7542],{"class":3012},[3002,16884,16444],{"class":3042},[3002,16886,3046],{"class":3008},[3002,16888,16889],{"class":3246},"\"Опис тестової аудіокниги\"",[3002,16891,3889],{"class":3008},[3002,16893,16894,16896,16898,16901,16903,16906],{"class":3004,"line":4214},[3002,16895,8381],{"class":4021},[3002,16897,7542],{"class":3012},[3002,16899,16900],{"class":3042}," coverImagePath",[3002,16902,3046],{"class":3008},[3002,16904,16905],{"class":3246},"\"/images/test-cover.jpg\"",[3002,16907,3889],{"class":3008},[3002,16909,16910],{"class":3004,"line":4409},[3002,16911,3153],{"emptyLinePlaceholder":3152},[3002,16913,16914,16916,16918,16920,16923],{"class":3004,"line":4415},[3002,16915,15376],{"class":4021},[3002,16917,8319],{"class":4021},[3002,16919,16756],{"class":3012},[3002,16921,16922],{"class":3022}," anAudiobook",[3002,16924,3026],{"class":3008},[3002,16926,16927,16929,16931,16933],{"class":3004,"line":4420},[3002,16928,15390],{"class":3218},[3002,16930,15393],{"class":3218},[3002,16932,16756],{"class":3022},[3002,16934,3304],{"class":3008},[3002,16936,16937],{"class":3004,"line":4425},[3002,16938,7076],{"class":3008},[3002,16940,16941],{"class":3004,"line":4432},[3002,16942,3153],{"emptyLinePlaceholder":3152},[3002,16944,16945,16947,16949,16951,16953,16955,16957],{"class":3004,"line":4441},[3002,16946,15376],{"class":4021},[3002,16948,16756],{"class":3012},[3002,16950,15414],{"class":3022},[3002,16952,3052],{"class":3008},[3002,16954,3875],{"class":3012},[3002,16956,15277],{"class":3042},[3002,16958,7134],{"class":3008},[3002,16960,16961,16963,16965,16967],{"class":3004,"line":4464},[3002,16962,15427],{"class":4021},[3002,16964,3058],{"class":3008},[3002,16966,15432],{"class":3042},[3002,16968,15435],{"class":3008},[3002,16970,16971,16973,16975],{"class":3004,"line":4479},[3002,16972,15390],{"class":3218},[3002,16974,15442],{"class":4021},[3002,16976,3889],{"class":3008},[3002,16978,16979],{"class":3004,"line":4954},[3002,16980,7076],{"class":3008},[3002,16982,16983],{"class":3004,"line":4972},[3002,16984,3153],{"emptyLinePlaceholder":3152},[3002,16986,16987,16989,16991,16994,16996,16998,17000],{"class":3004,"line":4990},[3002,16988,15376],{"class":4021},[3002,16990,16756],{"class":3012},[3002,16992,16993],{"class":3022}," withTitle",[3002,16995,3052],{"class":3008},[3002,16997,3861],{"class":3012},[3002,16999,16789],{"class":3042},[3002,17001,7134],{"class":3008},[3002,17003,17004,17006,17008,17011],{"class":3004,"line":5007},[3002,17005,15427],{"class":4021},[3002,17007,3058],{"class":3008},[3002,17009,17010],{"class":3042},"title",[3002,17012,17013],{"class":3008}," = title;\n",[3002,17015,17016,17018,17020],{"class":3004,"line":5016},[3002,17017,15390],{"class":3218},[3002,17019,15442],{"class":4021},[3002,17021,3889],{"class":3008},[3002,17023,17024],{"class":3004,"line":5487},[3002,17025,7076],{"class":3008},[3002,17027,17028],{"class":3004,"line":5492},[3002,17029,3153],{"emptyLinePlaceholder":3152},[3002,17031,17032,17034,17036,17039,17041,17043,17045],{"class":3004,"line":5504},[3002,17033,15376],{"class":4021},[3002,17035,16756],{"class":3012},[3002,17037,17038],{"class":3022}," withAuthor",[3002,17040,3052],{"class":3008},[3002,17042,4168],{"class":3012},[3002,17044,3234],{"class":3042},[3002,17046,7134],{"class":3008},[3002,17048,17049,17051,17053,17055],{"class":3004,"line":5514},[3002,17050,15427],{"class":4021},[3002,17052,3058],{"class":3008},[3002,17054,3545],{"class":3042},[3002,17056,17057],{"class":3008}," = author;\n",[3002,17059,17060,17062,17064],{"class":3004,"line":5552},[3002,17061,15390],{"class":3218},[3002,17063,15442],{"class":4021},[3002,17065,3889],{"class":3008},[3002,17067,17068],{"class":3004,"line":5571},[3002,17069,7076],{"class":3008},[3002,17071,17072],{"class":3004,"line":5586},[3002,17073,3153],{"emptyLinePlaceholder":3152},[3002,17075,17076,17078,17080,17083,17085,17088,17090],{"class":3004,"line":5600},[3002,17077,15376],{"class":4021},[3002,17079,16756],{"class":3012},[3002,17081,17082],{"class":3022}," withGenre",[3002,17084,3052],{"class":3008},[3002,17086,17087],{"class":3012},"Genre",[3002,17089,11854],{"class":3042},[3002,17091,7134],{"class":3008},[3002,17093,17094,17096,17098,17100],{"class":3004,"line":5605},[3002,17095,15427],{"class":4021},[3002,17097,3058],{"class":3008},[3002,17099,11931],{"class":3042},[3002,17101,17102],{"class":3008}," = genre;\n",[3002,17104,17105,17107,17109],{"class":3004,"line":5610},[3002,17106,15390],{"class":3218},[3002,17108,15442],{"class":4021},[3002,17110,3889],{"class":3008},[3002,17112,17113],{"class":3004,"line":6796},[3002,17114,7076],{"class":3008},[3002,17116,17117],{"class":3004,"line":6802},[3002,17118,3153],{"emptyLinePlaceholder":3152},[3002,17120,17121,17123,17125,17128,17130,17133,17135],{"class":3004,"line":6820},[3002,17122,15376],{"class":4021},[3002,17124,16756],{"class":3012},[3002,17126,17127],{"class":3022}," withDuration",[3002,17129,3052],{"class":3008},[3002,17131,17132],{"class":3012},"int",[3002,17134,16850],{"class":3042},[3002,17136,7134],{"class":3008},[3002,17138,17139,17141,17143,17146],{"class":3004,"line":6825},[3002,17140,15427],{"class":4021},[3002,17142,3058],{"class":3008},[3002,17144,17145],{"class":3042},"duration",[3002,17147,17148],{"class":3008}," = duration;\n",[3002,17150,17151,17153,17155],{"class":3004,"line":6831},[3002,17152,15390],{"class":3218},[3002,17154,15442],{"class":4021},[3002,17156,3889],{"class":3008},[3002,17158,17159],{"class":3004,"line":6837},[3002,17160,7076],{"class":3008},[3002,17162,17163],{"class":3004,"line":6843},[3002,17164,3153],{"emptyLinePlaceholder":3152},[3002,17166,17167,17169,17171,17174,17176,17178,17180],{"class":3004,"line":6849},[3002,17168,15376],{"class":4021},[3002,17170,16756],{"class":3012},[3002,17172,17173],{"class":3022}," withReleaseYear",[3002,17175,3052],{"class":3008},[3002,17177,17132],{"class":3012},[3002,17179,16870],{"class":3042},[3002,17181,7134],{"class":3008},[3002,17183,17184,17186,17188,17191],{"class":3004,"line":6863},[3002,17185,15427],{"class":4021},[3002,17187,3058],{"class":3008},[3002,17189,17190],{"class":3042},"releaseYear",[3002,17192,17193],{"class":3008}," = releaseYear;\n",[3002,17195,17196,17198,17200],{"class":3004,"line":6868},[3002,17197,15390],{"class":3218},[3002,17199,15442],{"class":4021},[3002,17201,3889],{"class":3008},[3002,17203,17204],{"class":3004,"line":6873},[3002,17205,7076],{"class":3008},[3002,17207,17208],{"class":3004,"line":6879},[3002,17209,3153],{"emptyLinePlaceholder":3152},[3002,17211,17212,17214,17216,17218,17220,17222,17224],{"class":3004,"line":6885},[3002,17213,15376],{"class":4021},[3002,17215,16756],{"class":3012},[3002,17217,16582],{"class":3022},[3002,17219,3052],{"class":3008},[3002,17221,3861],{"class":3012},[3002,17223,16444],{"class":3042},[3002,17225,7134],{"class":3008},[3002,17227,17228,17230,17232,17234],{"class":3004,"line":6891},[3002,17229,15427],{"class":4021},[3002,17231,3058],{"class":3008},[3002,17233,16599],{"class":3042},[3002,17235,16602],{"class":3008},[3002,17237,17238,17240,17242],{"class":3004,"line":6897},[3002,17239,15390],{"class":3218},[3002,17241,15442],{"class":4021},[3002,17243,3889],{"class":3008},[3002,17245,17246],{"class":3004,"line":6902},[3002,17247,7076],{"class":3008},[3002,17249,17250],{"class":3004,"line":6910},[3002,17251,3153],{"emptyLinePlaceholder":3152},[3002,17253,17254,17256,17258,17261,17263,17265,17267],{"class":3004,"line":6935},[3002,17255,15376],{"class":4021},[3002,17257,16756],{"class":3012},[3002,17259,17260],{"class":3022}," withCoverImagePath",[3002,17262,3052],{"class":3008},[3002,17264,3861],{"class":3012},[3002,17266,16900],{"class":3042},[3002,17268,7134],{"class":3008},[3002,17270,17271,17273,17275,17278],{"class":3004,"line":6941},[3002,17272,15427],{"class":4021},[3002,17274,3058],{"class":3008},[3002,17276,17277],{"class":3042},"coverImagePath",[3002,17279,17280],{"class":3008}," = coverImagePath;\n",[3002,17282,17283,17285,17287],{"class":3004,"line":6984},[3002,17284,15390],{"class":3218},[3002,17286,15442],{"class":4021},[3002,17288,3889],{"class":3008},[3002,17290,17291],{"class":3004,"line":6990},[3002,17292,7076],{"class":3008},[3002,17294,17295],{"class":3004,"line":6996},[3002,17296,3153],{"emptyLinePlaceholder":3152},[3002,17298,17299,17301,17303,17305],{"class":3004,"line":7002},[3002,17300,15376],{"class":4021},[3002,17302,12714],{"class":3012},[3002,17304,15749],{"class":3022},[3002,17306,3026],{"class":3008},[3002,17308,17309,17311,17313,17315,17317,17319],{"class":3004,"line":7008},[3002,17310,12704],{"class":3012},[3002,17312,12707],{"class":3042},[3002,17314,3046],{"class":3008},[3002,17316,3219],{"class":3218},[3002,17318,12714],{"class":3022},[3002,17320,17321],{"class":3008},"(title, author, genre);\n",[3002,17323,17324,17326,17328,17330],{"class":3004,"line":7028},[3002,17325,12727],{"class":3042},[3002,17327,3058],{"class":3008},[3002,17329,8965],{"class":3022},[3002,17331,15777],{"class":3008},[3002,17333,17334,17336,17338,17340],{"class":3004,"line":7050},[3002,17335,12727],{"class":3042},[3002,17337,3058],{"class":3008},[3002,17339,12732],{"class":3022},[3002,17341,17342],{"class":3008},"(duration);\n",[3002,17344,17345,17347,17349,17351],{"class":3004,"line":7055},[3002,17346,12727],{"class":3042},[3002,17348,3058],{"class":3008},[3002,17350,12751],{"class":3022},[3002,17352,17353],{"class":3008},"(releaseYear);\n",[3002,17355,17356,17358,17360,17362],{"class":3004,"line":7061},[3002,17357,12727],{"class":3042},[3002,17359,3058],{"class":3008},[3002,17361,11878],{"class":3022},[3002,17363,16664],{"class":3008},[3002,17365,17366,17368,17370,17373],{"class":3004,"line":7073},[3002,17367,12727],{"class":3042},[3002,17369,3058],{"class":3008},[3002,17371,17372],{"class":3022},"setCoverImagePath",[3002,17374,17375],{"class":3008},"(coverImagePath);\n",[3002,17377,17378,17380],{"class":3004,"line":7079},[3002,17379,15390],{"class":3218},[3002,17381,17382],{"class":3008}," book;\n",[3002,17384,17385],{"class":3004,"line":7084},[3002,17386,7076],{"class":3008},[3002,17388,17389],{"class":3004,"line":7089},[3002,17390,3310],{"class":3008},[2964,17392,17393],{},[2989,17394,17395],{},"Використання у складних тестах:",[2993,17397,17399],{"className":2995,"code":17398,"language":2997,"meta":2998,"style":2998},"import static com.example.audiobook.testutil.AuthorTestBuilder.anAuthor;\nimport static com.example.audiobook.testutil.GenreTestBuilder.aGenre;\nimport static com.example.audiobook.testutil.AudiobookTestBuilder.anAudiobook;\n\n@Test\nvoid deleteAuthor_shouldCascadeDeleteAudiobooks_whenAuthorHasBooks() {\n    // ═══ Arrange ═══\n    Author author = anAuthor().withLastName(\"Шевченко\").build();\n    Genre genre = aGenre().withName(\"Поезія\").build();\n    \n    authorRepo.save(author);\n    genreRepo.save(genre);\n\n    Audiobook book1 = anAudiobook()\n        .withTitle(\"Кобзар\")\n        .withAuthor(author)\n        .withGenre(genre)\n        .withReleaseYear(1840)\n        .build();\n\n    Audiobook book2 = anAudiobook()\n        .withTitle(\"Гайдамаки\")\n        .withAuthor(author)\n        .withGenre(genre)\n        .withReleaseYear(1841)\n        .build();\n\n    audiobookRepo.save(book1);\n    audiobookRepo.save(book2);\n\n    // ═══ Act ═══\n    authorRepo.deleteById(author.getId());\n\n    // ═══ Assert ═══\n    assertThat(audiobookRepo.findById(book1.getId())).isEmpty();\n    assertThat(audiobookRepo.findById(book2.getId())).isEmpty();\n}\n",[2968,17400,17401,17409,17417,17426,17430,17436,17444,17448,17472,17497,17501,17511,17521,17525,17538,17551,17561,17571,17584,17592,17596,17608,17620,17628,17636,17648,17656,17660,17670,17680,17684,17688,17706,17710,17714,17740,17766],{"__ignoreMap":2998},[3002,17402,17403,17405,17407],{"class":3004,"line":3005},[3002,17404,6562],{"class":4021},[3002,17406,8319],{"class":4021},[3002,17408,15832],{"class":3008},[3002,17410,17411,17413,17415],{"class":3004,"line":3016},[3002,17412,6562],{"class":4021},[3002,17414,8319],{"class":4021},[3002,17416,16743],{"class":3008},[3002,17418,17419,17421,17423],{"class":3004,"line":3029},[3002,17420,6562],{"class":4021},[3002,17422,8319],{"class":4021},[3002,17424,17425],{"class":3008}," com.example.audiobook.testutil.AudiobookTestBuilder.anAudiobook;\n",[3002,17427,17428],{"class":3004,"line":3036},[3002,17429,3153],{"emptyLinePlaceholder":3152},[3002,17431,17432,17434],{"class":3004,"line":3067},[3002,17433,3009],{"class":3008},[3002,17435,3013],{"class":3012},[3002,17437,17438,17440,17442],{"class":3004,"line":3091},[3002,17439,3019],{"class":3012},[3002,17441,13442],{"class":3022},[3002,17443,3026],{"class":3008},[3002,17445,17446],{"class":3004,"line":3121},[3002,17447,5796],{"class":3032},[3002,17449,17450,17452,17454,17456,17458,17460,17462,17464,17466,17468,17470],{"class":3004,"line":3149},[3002,17451,3231],{"class":3012},[3002,17453,3234],{"class":3042},[3002,17455,3046],{"class":3008},[3002,17457,15870],{"class":3022},[3002,17459,6961],{"class":3008},[3002,17461,15892],{"class":3022},[3002,17463,3052],{"class":3008},[3002,17465,5833],{"class":3246},[3002,17467,16196],{"class":3008},[3002,17469,15905],{"class":3022},[3002,17471,3304],{"class":3008},[3002,17473,17474,17476,17478,17480,17482,17484,17487,17489,17491,17493,17495],{"class":3004,"line":3156},[3002,17475,14122],{"class":3012},[3002,17477,11854],{"class":3042},[3002,17479,3046],{"class":3008},[3002,17481,16830],{"class":3022},[3002,17483,6961],{"class":3008},[3002,17485,17486],{"class":3022},"withName",[3002,17488,3052],{"class":3008},[3002,17490,12356],{"class":3246},[3002,17492,16196],{"class":3008},[3002,17494,15905],{"class":3022},[3002,17496,3304],{"class":3008},[3002,17498,17499],{"class":3004,"line":3180},[3002,17500,3456],{"class":3008},[3002,17502,17503,17505,17507,17509],{"class":3004,"line":3202},[3002,17504,14142],{"class":3042},[3002,17506,3058],{"class":3008},[3002,17508,3276],{"class":3022},[3002,17510,3279],{"class":3008},[3002,17512,17513,17515,17517,17519],{"class":3004,"line":3207},[3002,17514,14153],{"class":3042},[3002,17516,3058],{"class":3008},[3002,17518,3276],{"class":3022},[3002,17520,11904],{"class":3008},[3002,17522,17523],{"class":3004,"line":3228},[3002,17524,3153],{"emptyLinePlaceholder":3152},[3002,17526,17527,17529,17531,17533,17536],{"class":3004,"line":3257},[3002,17528,14168],{"class":3012},[3002,17530,13519],{"class":3042},[3002,17532,3046],{"class":3008},[3002,17534,17535],{"class":3022},"anAudiobook",[3002,17537,6354],{"class":3008},[3002,17539,17540,17542,17545,17547,17549],{"class":3004,"line":3262},[3002,17541,14252],{"class":3008},[3002,17543,17544],{"class":3022},"withTitle",[3002,17546,3052],{"class":3008},[3002,17548,13530],{"class":3246},[3002,17550,5187],{"class":3008},[3002,17552,17553,17555,17558],{"class":3004,"line":3268},[3002,17554,14252],{"class":3008},[3002,17556,17557],{"class":3022},"withAuthor",[3002,17559,17560],{"class":3008},"(author)\n",[3002,17562,17563,17565,17568],{"class":3004,"line":3282},[3002,17564,14252],{"class":3008},[3002,17566,17567],{"class":3022},"withGenre",[3002,17569,17570],{"class":3008},"(genre)\n",[3002,17572,17573,17575,17578,17580,17582],{"class":3004,"line":3287},[3002,17574,14252],{"class":3008},[3002,17576,17577],{"class":3022},"withReleaseYear",[3002,17579,3052],{"class":3008},[3002,17581,13561],{"class":3143},[3002,17583,5187],{"class":3008},[3002,17585,17586,17588,17590],{"class":3004,"line":3293},[3002,17587,14252],{"class":3008},[3002,17589,15905],{"class":3022},[3002,17591,3304],{"class":3008},[3002,17593,17594],{"class":3004,"line":3307},[3002,17595,3153],{"emptyLinePlaceholder":3152},[3002,17597,17598,17600,17602,17604,17606],{"class":3004,"line":4214},[3002,17599,14168],{"class":3012},[3002,17601,13574],{"class":3042},[3002,17603,3046],{"class":3008},[3002,17605,17535],{"class":3022},[3002,17607,6354],{"class":3008},[3002,17609,17610,17612,17614,17616,17618],{"class":3004,"line":4409},[3002,17611,14252],{"class":3008},[3002,17613,17544],{"class":3022},[3002,17615,3052],{"class":3008},[3002,17617,13585],{"class":3246},[3002,17619,5187],{"class":3008},[3002,17621,17622,17624,17626],{"class":3004,"line":4415},[3002,17623,14252],{"class":3008},[3002,17625,17557],{"class":3022},[3002,17627,17560],{"class":3008},[3002,17629,17630,17632,17634],{"class":3004,"line":4420},[3002,17631,14252],{"class":3008},[3002,17633,17567],{"class":3022},[3002,17635,17570],{"class":3008},[3002,17637,17638,17640,17642,17644,17646],{"class":3004,"line":4425},[3002,17639,14252],{"class":3008},[3002,17641,17577],{"class":3022},[3002,17643,3052],{"class":3008},[3002,17645,13616],{"class":3143},[3002,17647,5187],{"class":3008},[3002,17649,17650,17652,17654],{"class":3004,"line":4432},[3002,17651,14252],{"class":3008},[3002,17653,15905],{"class":3022},[3002,17655,3304],{"class":3008},[3002,17657,17658],{"class":3004,"line":4441},[3002,17659,3153],{"emptyLinePlaceholder":3152},[3002,17661,17662,17664,17666,17668],{"class":3004,"line":4464},[3002,17663,14907],{"class":3042},[3002,17665,3058],{"class":3008},[3002,17667,3276],{"class":3022},[3002,17669,13633],{"class":3008},[3002,17671,17672,17674,17676,17678],{"class":3004,"line":4479},[3002,17673,14907],{"class":3042},[3002,17675,3058],{"class":3008},[3002,17677,3276],{"class":3022},[3002,17679,13644],{"class":3008},[3002,17681,17682],{"class":3004,"line":4954},[3002,17683,3153],{"emptyLinePlaceholder":3152},[3002,17685,17686],{"class":3004,"line":4972},[3002,17687,5859],{"class":3032},[3002,17689,17690,17692,17694,17696,17698,17700,17702,17704],{"class":3004,"line":4990},[3002,17691,14142],{"class":3042},[3002,17693,3058],{"class":3008},[3002,17695,10349],{"class":3022},[3002,17697,3052],{"class":3008},[3002,17699,3545],{"class":3042},[3002,17701,3058],{"class":3008},[3002,17703,3550],{"class":3022},[3002,17705,11395],{"class":3008},[3002,17707,17708],{"class":3004,"line":5007},[3002,17709,3153],{"emptyLinePlaceholder":3152},[3002,17711,17712],{"class":3004,"line":5016},[3002,17713,5878],{"class":3032},[3002,17715,17716,17718,17720,17722,17724,17726,17728,17730,17732,17734,17736,17738],{"class":3004,"line":5487},[3002,17717,3562],{"class":3022},[3002,17719,3052],{"class":3008},[3002,17721,12812],{"class":3042},[3002,17723,3058],{"class":3008},[3002,17725,3540],{"class":3022},[3002,17727,3052],{"class":3008},[3002,17729,13759],{"class":3042},[3002,17731,3058],{"class":3008},[3002,17733,3550],{"class":3022},[3002,17735,3112],{"class":3008},[3002,17737,4471],{"class":3022},[3002,17739,3304],{"class":3008},[3002,17741,17742,17744,17746,17748,17750,17752,17754,17756,17758,17760,17762,17764],{"class":3004,"line":5492},[3002,17743,3562],{"class":3022},[3002,17745,3052],{"class":3008},[3002,17747,12812],{"class":3042},[3002,17749,3058],{"class":3008},[3002,17751,3540],{"class":3022},[3002,17753,3052],{"class":3008},[3002,17755,13786],{"class":3042},[3002,17757,3058],{"class":3008},[3002,17759,3550],{"class":3022},[3002,17761,3112],{"class":3008},[3002,17763,4471],{"class":3022},[3002,17765,3304],{"class":3008},[3002,17767,17768],{"class":3004,"line":5504},[3002,17769,3310],{"class":3008},[3971,17771,17772,17777,17784,17878,17883,17897],{},[2964,17773,17774],{},[2989,17775,17776],{},"Object Mother vs Builder:",[2964,17778,17779,17780,17783],{},"Існує альтернативний патерн — ",[2989,17781,17782],{},"Object Mother"," (фабричні методи):",[2993,17785,17787],{"className":2995,"code":17786,"language":2997,"meta":2998,"style":2998},"public class AuthorMother {\n    public static Author ivanFranko() {\n        return new Author(\"Іван\", \"Франко\");\n    }\n\n    public static Author tarasShevchenko() {\n        return new Author(\"Тарас\", \"Шевченко\");\n    }\n}\n",[2968,17788,17789,17800,17813,17831,17835,17839,17852,17870,17874],{"__ignoreMap":2998},[3002,17790,17791,17793,17795,17798],{"class":3004,"line":3005},[3002,17792,6805],{"class":4021},[3002,17794,6811],{"class":4021},[3002,17796,17797],{"class":3012}," AuthorMother",[3002,17799,6817],{"class":3008},[3002,17801,17802,17804,17806,17808,17811],{"class":3004,"line":3016},[3002,17803,15376],{"class":4021},[3002,17805,8319],{"class":4021},[3002,17807,3241],{"class":3012},[3002,17809,17810],{"class":3022}," ivanFranko",[3002,17812,3026],{"class":3008},[3002,17814,17815,17817,17819,17821,17823,17825,17827,17829],{"class":3004,"line":3029},[3002,17816,15390],{"class":3218},[3002,17818,15393],{"class":3218},[3002,17820,3241],{"class":3022},[3002,17822,3052],{"class":3008},[3002,17824,3247],{"class":3246},[3002,17826,2975],{"class":3008},[3002,17828,3252],{"class":3246},[3002,17830,3064],{"class":3008},[3002,17832,17833],{"class":3004,"line":3036},[3002,17834,7076],{"class":3008},[3002,17836,17837],{"class":3004,"line":3067},[3002,17838,3153],{"emptyLinePlaceholder":3152},[3002,17840,17841,17843,17845,17847,17850],{"class":3004,"line":3091},[3002,17842,15376],{"class":4021},[3002,17844,8319],{"class":4021},[3002,17846,3241],{"class":3012},[3002,17848,17849],{"class":3022}," tarasShevchenko",[3002,17851,3026],{"class":3008},[3002,17853,17854,17856,17858,17860,17862,17864,17866,17868],{"class":3004,"line":3121},[3002,17855,15390],{"class":3218},[3002,17857,15393],{"class":3218},[3002,17859,3241],{"class":3022},[3002,17861,3052],{"class":3008},[3002,17863,5828],{"class":3246},[3002,17865,2975],{"class":3008},[3002,17867,5833],{"class":3246},[3002,17869,3064],{"class":3008},[3002,17871,17872],{"class":3004,"line":3149},[3002,17873,7076],{"class":3008},[3002,17875,17876],{"class":3004,"line":3156},[3002,17877,3310],{"class":3008},[2964,17879,17880],{},[2989,17881,17882],{},"Коли використовувати що:",[3332,17884,17885,17891],{},[3335,17886,17887,17890],{},[2989,17888,17889],{},"Builder:"," Коли потрібна гнучкість (багато комбінацій полів)",[3335,17892,17893,17896],{},[2989,17894,17895],{},"Object Mother:"," Коли є кілька фіксованих «персонажів», що часто використовуються",[2964,17898,17899],{},"Для більшості тестів Builder є кращим вибором.",[3660,17901],{},[2959,17903,17905],{"id":17904},"запуск-тестів-та-звіти","Запуск тестів та звіти",[3810,17907,17909],{"id":17908},"maven","Maven",[2993,17911,17915],{"className":17912,"code":17913,"language":17914,"meta":2998,"style":2998},"language-bash shiki shiki-themes light-plus dark-plus dark-plus","# Запустити всі тести\nmvn test\n\n# Запустити лише інтеграційні тести (за конвенцією *Test.java)\nmvn test -Dtest=\"*Test\"\n\n# Запустити конкретний тестовий клас\nmvn test -Dtest=JdbcAuthorRepositoryTest\n\n# Запустити конкретний тест\nmvn test -Dtest=JdbcAuthorRepositoryTest#save_shouldInsertNewAuthor_whenValidData\n","bash",[2968,17916,17917,17922,17930,17934,17939,17952,17956,17961,17970,17974,17979],{"__ignoreMap":2998},[3002,17918,17919],{"class":3004,"line":3005},[3002,17920,17921],{"class":3032},"# Запустити всі тести\n",[3002,17923,17924,17927],{"class":3004,"line":3016},[3002,17925,17926],{"class":3022},"mvn",[3002,17928,17929],{"class":3246}," test\n",[3002,17931,17932],{"class":3004,"line":3029},[3002,17933,3153],{"emptyLinePlaceholder":3152},[3002,17935,17936],{"class":3004,"line":3036},[3002,17937,17938],{"class":3032},"# Запустити лише інтеграційні тести (за конвенцією *Test.java)\n",[3002,17940,17941,17943,17946,17949],{"class":3004,"line":3067},[3002,17942,17926],{"class":3022},[3002,17944,17945],{"class":3246}," test",[3002,17947,17948],{"class":4021}," -Dtest=",[3002,17950,17951],{"class":3246},"\"*Test\"\n",[3002,17953,17954],{"class":3004,"line":3091},[3002,17955,3153],{"emptyLinePlaceholder":3152},[3002,17957,17958],{"class":3004,"line":3121},[3002,17959,17960],{"class":3032},"# Запустити конкретний тестовий клас\n",[3002,17962,17963,17965,17967],{"class":3004,"line":3149},[3002,17964,17926],{"class":3022},[3002,17966,17945],{"class":3246},[3002,17968,17969],{"class":4021}," -Dtest=JdbcAuthorRepositoryTest\n",[3002,17971,17972],{"class":3004,"line":3156},[3002,17973,3153],{"emptyLinePlaceholder":3152},[3002,17975,17976],{"class":3004,"line":3180},[3002,17977,17978],{"class":3032},"# Запустити конкретний тест\n",[3002,17980,17981,17983,17985,17988],{"class":3004,"line":3202},[3002,17982,17926],{"class":3022},[3002,17984,17945],{"class":3246},[3002,17986,17987],{"class":4021}," -Dtest=JdbcAuthorRepositoryTest",[3002,17989,17990],{"class":3008},"#save_shouldInsertNewAuthor_whenValidData\n",[3810,17992,17994],{"id":17993},"gradle","Gradle",[2993,17996,17998],{"className":17912,"code":17997,"language":17914,"meta":2998,"style":2998},"# Запустити всі тести\n./gradlew test\n\n# Запустити конкретний клас\n./gradlew test --tests JdbcAuthorRepositoryTest\n\n# Запустити конкретний тест\n./gradlew test --tests JdbcAuthorRepositoryTest.save_shouldInsertNewAuthor_whenValidData\n",[2968,17999,18000,18004,18011,18015,18020,18032,18036,18040],{"__ignoreMap":2998},[3002,18001,18002],{"class":3004,"line":3005},[3002,18003,17921],{"class":3032},[3002,18005,18006,18009],{"class":3004,"line":3016},[3002,18007,18008],{"class":3022},"./gradlew",[3002,18010,17929],{"class":3246},[3002,18012,18013],{"class":3004,"line":3029},[3002,18014,3153],{"emptyLinePlaceholder":3152},[3002,18016,18017],{"class":3004,"line":3036},[3002,18018,18019],{"class":3032},"# Запустити конкретний клас\n",[3002,18021,18022,18024,18026,18029],{"class":3004,"line":3067},[3002,18023,18008],{"class":3022},[3002,18025,17945],{"class":3246},[3002,18027,18028],{"class":4021}," --tests",[3002,18030,18031],{"class":3246}," JdbcAuthorRepositoryTest\n",[3002,18033,18034],{"class":3004,"line":3091},[3002,18035,3153],{"emptyLinePlaceholder":3152},[3002,18037,18038],{"class":3004,"line":3121},[3002,18039,17978],{"class":3032},[3002,18041,18042,18044,18046,18048],{"class":3004,"line":3149},[3002,18043,18008],{"class":3022},[3002,18045,17945],{"class":3246},[3002,18047,18028],{"class":4021},[3002,18049,18050],{"class":3246}," JdbcAuthorRepositoryTest.save_shouldInsertNewAuthor_whenValidData\n",[3810,18052,18054],{"id":18053},"звіт-junit","Звіт JUnit",[2964,18056,18057],{},"Після виконання тестів Maven/Gradle генерують HTML-звіт:",[2993,18059,18062],{"className":18060,"code":18061,"language":3781},[3779],"target/surefire-reports/index.html  (Maven)\nbuild/reports/tests/test/index.html (Gradle)\n",[2968,18063,18061],{"__ignoreMap":2998},[18065,18066,18069,18080,18090,18097,18100,18107,18114,18128,18139,18150,18161,18172,18183,18194,18205,18208,18215,18226,18237,18240,18247,18258,18269,18279,18282,18289,18296],"terminal-preview",{":cursor":18067,"title":18068},"false","mvn test",[18070,18071,18073,16322,18078],"div",{"className":18072},[3004],[3002,18074,18077],{"className":18075},[18076],"opacity-40","$",[2989,18079,18068],{},[18070,18081,18083,18089],{"className":18082},[3004],[3002,18084,18088],{"className":18085},[18086,18087],"text-blue-400","font-bold","[INFO]"," Scanning for projects...",[18070,18091,18093,18096],{"className":18092},[3004],[3002,18094,18088],{"className":18095},[18086,18087]," Building audiobook-platform 1.0.0",[18070,18098],{"className":18099},[3004],[18070,18101,18103,18106],{"className":18102},[3004],[3002,18104,18088],{"className":18105},[18086,18087]," --- maven-surefire-plugin:3.2.5:test ---",[18070,18108,18110,18113],{"className":18109},[3004],[3002,18111,18088],{"className":18112},[18086,18087]," Running com.example.audiobook.repository.JdbcAuthorRepositoryTest",[18070,18115,18117,18122,18123],{"className":18116},[3004],[3002,18118,18121],{"className":18119},[18120],"text-green-400","✓"," save_shouldInsertNewAuthor_whenValidData ",[3002,18124,18127],{"className":18125},[18126],"opacity-60","(42ms)",[18070,18129,18131,18134,18135],{"className":18130},[3004],[3002,18132,18121],{"className":18133},[18120]," save_shouldHandleNullBio_whenBioNotProvided ",[3002,18136,18138],{"className":18137},[18126],"(38ms)",[18070,18140,18142,18145,18146],{"className":18141},[3004],[3002,18143,18121],{"className":18144},[18120]," save_shouldThrowException_whenDuplicateId ",[3002,18147,18149],{"className":18148},[18126],"(35ms)",[18070,18151,18153,18156,18157],{"className":18152},[3004],[3002,18154,18121],{"className":18155},[18120]," findById_shouldReturnAuthor_whenExists ",[3002,18158,18160],{"className":18159},[18126],"(40ms)",[18070,18162,18164,18167,18168],{"className":18163},[3004],[3002,18165,18121],{"className":18166},[18120]," findById_shouldReturnEmpty_whenNotExists ",[3002,18169,18171],{"className":18170},[18126],"(32ms)",[18070,18173,18175,18178,18179],{"className":18174},[3004],[3002,18176,18121],{"className":18177},[18120]," findAll_shouldReturnAllAuthors_whenMultipleExist ",[3002,18180,18182],{"className":18181},[18126],"(45ms)",[18070,18184,18186,18189,18190],{"className":18185},[3004],[3002,18187,18121],{"className":18188},[18120]," update_shouldModifyAllFields_whenAuthorExists ",[3002,18191,18193],{"className":18192},[18126],"(41ms)",[18070,18195,18197,18200,18201],{"className":18196},[3004],[3002,18198,18121],{"className":18199},[18120]," deleteById_shouldRemoveAuthor_whenExists ",[3002,18202,18204],{"className":18203},[18126],"(39ms)",[18070,18206],{"className":18207},[3004],[18070,18209,18211,18214],{"className":18210},[3004],[3002,18212,18088],{"className":18213},[18086,18087]," Running com.example.audiobook.repository.JdbcGenreRepositoryTest",[18070,18216,18218,18221,18222],{"className":18217},[3004],[3002,18219,18121],{"className":18220},[18120]," save_shouldThrowException_whenDuplicateName ",[3002,18223,18225],{"className":18224},[18126],"(37ms)",[18070,18227,18229,18232,18233],{"className":18228},[3004],[3002,18230,18121],{"className":18231},[18120]," findByName_shouldReturnGenre_whenExactMatch ",[3002,18234,18236],{"className":18235},[18126],"(34ms)",[18070,18238],{"className":18239},[3004],[18070,18241,18243,18246],{"className":18242},[3004],[3002,18244,18088],{"className":18245},[18086,18087]," Running com.example.audiobook.repository.JdbcAudiobookRepositoryTest",[18070,18248,18250,18253,18254],{"className":18249},[3004],[3002,18251,18121],{"className":18252},[18120]," save_shouldThrowException_whenAuthorNotExists ",[3002,18255,18257],{"className":18256},[18126],"(36ms)",[18070,18259,18261,18264,18265],{"className":18260},[3004],[3002,18262,18121],{"className":18263},[18120]," deleteAuthor_shouldCascadeDeleteAudiobooks_whenAuthorHasBooks ",[3002,18266,18268],{"className":18267},[18126],"(48ms)",[18070,18270,18272,18275,18276],{"className":18271},[3004],[3002,18273,18121],{"className":18274},[18120]," save_shouldThrowException_whenDurationNegative ",[3002,18277,18149],{"className":18278},[18126],[18070,18280],{"className":18281},[3004],[18070,18283,18285,18288],{"className":18284},[3004],[3002,18286,18088],{"className":18287},[18120,18087]," Tests run: 13, Failures: 0, Errors: 0, Skipped: 0",[18070,18290,18292,18295],{"className":18291},[3004],[3002,18293,18088],{"className":18294},[18120,18087]," BUILD SUCCESS",[18070,18297,18299,18302],{"className":18298},[3004],[3002,18300,18088],{"className":18301},[18086,18087]," Total time: 2.847 s",[3660,18304],{},[2959,18306,18308],{"id":18307},"висновки","Висновки",[2964,18310,18311],{},"Інтеграційні тести з Embedded H2 є критично важливими для JDBC-репозиторіїв. Ключові висновки:",[18313,18314,18315,18319,18322,18326,18329,18333,18336,18340,18347,18351,18354,18358],"steps",{},[3810,18316,18318],{"id":18317},"mock-тести-недостатні","Mock-тести недостатні",[2964,18320,18321],{},"Mock-об'єкти не можуть перевірити коректність SQL-запитів, роботу constraints та транзакційну логіку. Інтеграційні тести виконують реальний SQL на реальній БД.",[3810,18323,18325],{"id":18324},"ізоляція-тестів","Ізоляція тестів",[2964,18327,18328],{},"Кожен тест має отримувати нову порожню БД. H2 in-memory mode ідеально підходить для цього — швидко, без побічних ефектів.",[3810,18330,18332],{"id":18331},"патерн-aaa","Патерн AAA",[2964,18334,18335],{},"Структурування тестів за Arrange-Act-Assert робить їх читабельними та підтримуваними. Кожен тест має один Act — один метод, що тестується.",[3810,18337,18339],{"id":18338},"найменування-тестів","Найменування тестів",[2964,18341,18342,18343,18346],{},"Конвенція ",[2968,18344,18345],{},"methodName_shouldBehavior_whenCondition"," робить тести самодокументованими. Назва тесту описує, що він перевіряє.",[3810,18348,18350],{"id":18349},"тестування-constraints","Тестування constraints",[2964,18352,18353],{},"FK, UNIQUE, CHECK constraints — це бізнес-правила на рівні БД. Інтеграційні тести гарантують, що вони працюють коректно.",[3810,18355,18357],{"id":18356},"test-data-builders","Test Data Builders",[2964,18359,18360],{},"Builders зменшують дублювання коду створення тестових об'єктів та роблять тести читабельнішими.",[3660,18362],{},[2959,18364,18366],{"id":18365},"подальше-читання","Подальше читання",[18368,18369,18370,18381,18391,18401],"card-group",{},[18371,18372,18375,18378],"card",{"icon":18373,"title":18374},"i-heroicons-book-open","Growing Object-Oriented Software",[2964,18376,18377],{},"Steve Freeman, Nat Pryce, 2009",[2964,18379,18380],{},"Розділи 19–21 про інтеграційне тестування та Test Data Builders.",[18371,18382,18385,18388],{"icon":18383,"title":18384},"i-heroicons-beaker","The Art of Unit Testing",[2964,18386,18387],{},"Roy Osherove, 2013",[2964,18389,18390],{},"Глава 8 — про організацію тестів, найменування та патерн AAA.",[18371,18392,18395,18398],{"icon":18393,"title":18394},"i-heroicons-document-text","xUnit Test Patterns",[2964,18396,18397],{},"Gerard Meszaros, 2007",[2964,18399,18400],{},"Каталог патернів тестування: Object Mother, Test Data Builder, Fixture Setup.",[18371,18402,18405,18414],{"icon":18403,"title":18404},"i-heroicons-circle-stack","H2 Database Documentation",[2964,18406,18407],{},[18408,18409,18413],"a",{"href":18410,"rel":18411},"https://h2database.com",[18412],"nofollow","h2database.com",[2964,18415,18416],{},"Офіційна документація H2: режими роботи, сумісність з PostgreSQL, обмеження.",[3660,18418],{},[2964,18420,18421,18424],{},[2989,18422,18423],{},"Наступна стаття серії:"," Testcontainers — тестування з реальною PostgreSQL у Docker (стаття 23).",[2964,18426,18427,18430],{},[2989,18428,18429],{},"Попередня стаття:"," Асинхронність у JDBC через CompletableFuture (стаття 21).",[18432,18433,18434],"style",{},"html pre.shiki code .sHH4Y, html code.shiki .sHH4Y{--shiki-light:#000000;--shiki-default:#D4D4D4;--shiki-dark:#D4D4D4}html pre.shiki code .sN1BT, html code.shiki .sN1BT{--shiki-light:#267F99;--shiki-default:#4EC9B0;--shiki-dark:#4EC9B0}html pre.shiki code .s8Opu, html code.shiki .s8Opu{--shiki-light:#795E26;--shiki-default:#DCDCAA;--shiki-dark:#DCDCAA}html pre.shiki code .spJ8K, html code.shiki .spJ8K{--shiki-light:#008000;--shiki-default:#6A9955;--shiki-dark:#6A9955}html pre.shiki code .siwwj, html code.shiki .siwwj{--shiki-light:#001080;--shiki-default:#9CDCFE;--shiki-dark:#9CDCFE}html pre.shiki code .sJj4R, html code.shiki .sJj4R{--shiki-light:#098658;--shiki-default:#B5CEA8;--shiki-dark:#B5CEA8}html pre.shiki code .sCDza, html code.shiki .sCDza{--shiki-light:#AF00DB;--shiki-default:#CE92A4;--shiki-dark:#CE92A4}html pre.shiki code .sbdoH, html code.shiki .sbdoH{--shiki-light:#A31515;--shiki-default:#CE9178;--shiki-dark:#CE9178}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .su1O8, html code.shiki .su1O8{--shiki-light:#0000FF;--shiki-default:#569CD6;--shiki-dark:#569CD6}html pre.shiki code .s0P7L, html code.shiki .s0P7L{--shiki-light:#800000;--shiki-default:#808080;--shiki-dark:#808080}html pre.shiki code .sKtos, html code.shiki .sKtos{--shiki-light:#800000;--shiki-default:#569CD6;--shiki-dark:#569CD6}html pre.shiki code .sjcCO, html code.shiki .sjcCO{--shiki-light:#EE0000;--shiki-default:#D7BA7D;--shiki-dark:#D7BA7D}",{"title":2998,"searchDepth":3016,"depth":3016,"links":18436},[18437,18438,18439,18442,18443,18447,18451,18452,18453,18458,18463,18468,18476],{"id":2961,"depth":3016,"text":2962},{"id":3664,"depth":3016,"text":3665},{"id":3797,"depth":3016,"text":3798,"children":18440},[18441],{"id":3812,"depth":3029,"text":3813},{"id":3990,"depth":3016,"text":3991},{"id":4602,"depth":3016,"text":4603,"children":18444},[18445,18446],{"id":4606,"depth":3029,"text":4607},{"id":5068,"depth":3029,"text":5069},{"id":5669,"depth":3016,"text":5670,"children":18448},[18449,18450],{"id":5679,"depth":3029,"text":5680},{"id":6271,"depth":3029,"text":6272},{"id":6536,"depth":3016,"text":6537},{"id":8235,"depth":3016,"text":8236},{"id":11680,"depth":3016,"text":11681,"children":18454},[18455,18456,18457],{"id":11691,"depth":3029,"text":11692},{"id":12389,"depth":3029,"text":12390},{"id":14069,"depth":3029,"text":14070},{"id":15054,"depth":3016,"text":15055,"children":18459},[18460,18461,18462],{"id":15124,"depth":3029,"text":15125},{"id":15817,"depth":3029,"text":15818},{"id":16350,"depth":3029,"text":16351},{"id":17904,"depth":3016,"text":17905,"children":18464},[18465,18466,18467],{"id":17908,"depth":3029,"text":17909},{"id":17993,"depth":3029,"text":17994},{"id":18053,"depth":3029,"text":18054},{"id":18307,"depth":3016,"text":18308,"children":18469},[18470,18471,18472,18473,18474,18475],{"id":18317,"depth":3029,"text":18318},{"id":18324,"depth":3029,"text":18325},{"id":18331,"depth":3029,"text":18332},{"id":18338,"depth":3029,"text":18339},{"id":18349,"depth":3029,"text":18350},{"id":18356,"depth":3029,"text":18357},{"id":18365,"depth":3016,"text":18366},"Від mock-об'єктів до реальної БД: налаштування Embedded H2 для інтеграційних тестів, патерн Arrange-Act-Assert, найменування тестів за Given-When-Then, Test Data Builders та тестування FK/UNIQUE/CHECK constraints.","md",null,{},{"title":2341,"description":18477},"C28APgeCTn5nFzHTQJ_VrmD7r2Ann4P6UWKIjU-_fmw",[18484,18486],{"title":2337,"path":2338,"stem":2339,"description":18485,"children":-1},"Від синхронних блокуючих операцій до асинхронної обробки: реалізація AsyncRepository через ExecutorService, інтеграція з CompletableFuture, паралельне виконання запитів та координація асинхронних операцій у транзакціях.",{"title":2345,"path":2346,"stem":2347,"description":18487,"children":-1},"Від емуляції до реальності: архітектура Testcontainers, lifecycle Docker-контейнерів у тестах, тестування PostgreSQL-специфічних функцій (ENUM, JSON, full-text search), паралельне виконання тестів та інтеграція з CI/CD.",1777909229400]