JavaRush /Курси /Java Server /Єдина поведінка real

Єдина поведінка real/ mock клієнта

Java Server
Рівень 17 , Лекція 4
Відкрита

1. Єдина поведінка: сенс і користь

Команди вже вміють запускати catalog search і catalog details, а діагностика вже вміє показати, що сталося під час real-виклику. Залишилося одне джерело хаосу: що саме змінюється, коли ми перемикаємося між real і mock. Якщо тут попливуть контракт, формат результату або обробка порожніх і помилкових випадків, уся попередня дисципліна швидко розсиплеться.

Коли кажуть «у нас є real і mock режим», новачки часто чують: «У нас два різні застосунки, просто в одному проєкті». А далі починаються веселощі: у real-режимі друкуємо красиво, у mock-режимі — як вийде; у real-режимі повертаємо DTO, у mock-режимі — рядки; у real-режимі помилки оформлюємо одним способом, у mock-режимі — іншим. У підсумку будь-який код «вище» клієнта змушений знати про режим, розгалужуватися й ускладнюватися.

«Єдина поведінка» — це сувора і дуже практична ідея: режим змінює джерело даних, але не змінює сенс і форму результату. Якщо решта застосунку вміє працювати з CatalogBookSearchItem, то має вміти працювати з ним і в real-режимі, і в mock-режимі. Якщо catalog search у real-режимі може повернути порожній список — mock-режим теж має вміти повернути порожній список, а не падати або повідомляти, що ресурсу немає.

Можна уявити це як правило: у застосунку один «внутрішній контракт», а не два. А режим — це просто те, яку трубу ми підʼєднали до цього контракту.

2. Контракт CatalogClient: інтерфейс і DTO

Щоб real і mock не перетворилися на два різні застосунки, решта коду має бачити один і той самий внутрішній контракт. CatalogClient і далі повертає нормалізовані CatalogBookSearchItem і CatalogBookDetails, а не DTO провайдера, не JsonNode і не сирий JSON. Тут важливо не вигадувати для mock «полегшену версію клієнта», а зберегти вже обрану мову застосунку.

Практична ознака дуже проста:

  • search(query) в обох режимах повертає список CatalogBookSearchItem;
  • details(externalId) в обох режимах повертає CatalogBookDetails;
  • слова зовнішнього провайдера на кшталт docs, key, author_name, numFound залишаються всередині клієнтського шару.

Щойно цей договір витримано, точка входу, друк результатів і решта коду перестають розгалужуватися за режимом. Вони працюють з одним API і взагалі не питають, чи прийшли дані мережею, чи із sample JSON.

3. Mock як джерело даних

Найчастіше mock ламається з однієї причини: люди думають, що mock — це «підставити готовий результат». Тоді mock повертає «вже надруковані рядки», або «вже нормалізовані DTO, зібрані вручну», або навіть «просто System.out.println("мок!")». Такий mock, звісно, швидкий… але марний. Він не перевіряє найважливішого: чи взагалі працює наша логіка читання JSON і мапінгу.

Правильна модель mock-режиму тут дуже приземлена: mock підміняє джерело даних, але не змінює подальший шлях. Тобто в mock-режимі ми беремо sample JSON, читаємо його тим самим ObjectMapper, отримуємо DTO провайдера, а потім нормалізуємо в ті самі DTO, що й у real-режимі.

Зручно тримати в голові таку схему:

flowchart TD
    A["Команда: catalog search/details"] --> B["CatalogClient"]
    B -->|real| C[HTTP-запит]
    B -->|mock| D[JSON-ресурс із classpath]
    C --> E[Provider JSON]
    D --> E[Provider JSON]
    E --> F[Provider DTO]
    F --> G[Mapping]
    G --> H[Normalized DTO]
    H --> I[Єдине виведення]

Тут головний фокус у тому, що після отримання JSON шлях однаковий.

Із цього ж правила одразу випливає, де обирати режим. Достатньо однієї точки збирання: там створюється або RealCatalogClient, або MockCatalogClient, а далі в застосунку живе тільки посилання типу CatalogClient. Якщо if(useMock) починає зʼявлятися в командах, друку або мапінгу, режим уже протікав занадто далеко.

Корисно порівняти real і mock не як «два світи», а як «дві реалізації однієї обіцянки»:

Що порівнюємо real режим mock режим Що має збігатися
Джерело даних HTTP-відповідь JSON із classpath Нормалізований результат
Парсинг JSON ObjectMapper.readValue(...) ObjectMapper.readValue(...) Так, тим самим способом
Mapping DTO провайдера → нормалізований DTO DTO провайдера → нормалізований DTO Один і той самий mapping
Повернення назовні нормалізований DTO нормалізований DTO Обовʼязково
Користувацьке виведення друкує нормалізований DTO друкує нормалізований DTO Один формат

4. Mock JSON у resources

З mock-режимом є одна підступна пастка, яку майже всі ловлять один раз — іноді двічі, щоб закріпити успіх. Пастка називається: «В IDE працює, а після збирання JAR — ні». Причина зазвичай у тому, що mock-файл читають як звичайний файл за шляхом у файловій системі, а в JAR це вже не «просто файл». Тому ми читаємо mock-JSON як ресурс classpath, тобто через getResourceAsStream.

Уявімо, що в нас є файл src/main/resources/mock/search-clean-code.json. Тоді читання виглядає так:

import java.io.IOException;
import java.io.InputStream;

// Читаємо саме як ресурс classpath (це працює і в IDE, і з JAR)
try (InputStream in = getClass().getResourceAsStream("/mock/search-clean-code.json")) {
    if (in == null) {
        // Явно повідомляємо причину: ресурс не знайдено (інакше десь далі буде незрозумілий NPE)
        throw new IllegalStateException("Мок-ресурс не знайдено: /mock/search-clean-code.json");
    }

    // Важливо: використовуємо той самий ObjectMapper, що й у real-режимі
    ProviderSearchResponse response = objectMapper.readValue(in, ProviderSearchResponse.class);
} catch (IOException e) {
    throw new IllegalStateException("Не вдалося прочитати mock-ресурс", e);
}

Тут важливі дві речі. По-перше, шлях починається з / — це абсолютний шлях у classpath. По-друге, ми обовʼязково перевіряємо in == null. Інакше ви отримаєте NullPointerException у місці, яке зовсім не пояснює, що файл просто не знайшовся. А IllegalStateException("Мок-ресурс не знайдено: ...") хоча б говорить правду, хай і без ніжності.

Важлива деталь: тут не створюємо новий ObjectMapper «на місці». Якщо real і mock почнуть читати JSON різними mapper-ами, ви дуже швидко отримаєте дві трохи різні поведінки і почнете ловити привидів.

Далі ви робите те саме, що робили б у real-режимі: із DTO провайдера робите нормалізований DTO. Наприклад, якщо провайдер надсилає docs (список книг), а в книги є key, title і author_name, mapping може бути таким:

import java.util.List;

CatalogBookSearchItem toSearchItem(ProviderBookDoc doc) {
    // У навчальному mapping часто беремо "першого автора", щоб не ускладнювати формат виведення
    List<String> authors = doc.authorNames();
    String author = (authors == null || authors.isEmpty()) ? "Невідомо" : authors.get(0);

    // externalId — це стабільний ідентифікатор із контракту провайдера (ключ/ID), але в нашому DTO
    return new CatalogBookSearchItem(doc.key(), doc.title(), author);
}

Зверніть увагу: ми не намагаємося зробити «ідеальний» mapping і не влаштовуємо війну за те, що таке «правильний автор». Наша мета зараз — щоб поведінка була однаковою в обох режимах і щоб формат результату був стабільним.

Залишається дрібне, але життєве питання: «А як у mock-режимі обрати, який файл читати?» Найпростіший шлях — зробити невелику мапу «запит → ресурс». Так, це майже таблиця відповідностей. І це нормально: mock-режим — це навчальна стабілізація, а не повноцінна система фейкових даних.

import java.util.Map;

// Таблиця відповідностей: запит користувача -> конкретний JSON-ресурс у classpath
private static final Map<String, String> SEARCH_MOCKS = Map.of(
        "clean code", "/mock/search-clean-code.json",
        "unknown-book", "/mock/search-empty.json"
);

І далі ви берете шлях із мапи та читаєте ресурс. Якщо запит невідомий — можна повертати порожній результат, щоб mock-режим був передбачуваним, а не падав через те, що користувач ввів «clean coder» замість «clean code». У навчальному проєкті «передбачувано» майже завжди важливіше, ніж «суперреалістично».

5. Єдине виведення: принтер результатів

Дуже поширена помилка в клієнтських проєктах: «Раз я вже сходив у зовнішній API, я тут же й надрукую». Виглядає зручно, але закінчується тим, що клієнт знає про формат виведення, мову повідомлень і те, що в нас там у main відбувається. А потім ви хочете змінити формат — і раптом лізете в transport-код. Це все одно що змінювати дизайн кухні, розкручуючи двигун автомобіля. Технічно можливо, але емоційно не рекомендовано.

Сенс простий: клієнт повертає дані, а друк — окрема відповідальність. Але робити з цього новий обовʼязковий шар теж не треба. Поки команд небагато, виведення можна спокійно залишити прямо в ReadLaterApplication. Якщо форматування починає розростатися і заважає читати маршрутизацію команд, його зручно винести в окремий CatalogConsolePrinter як невеликий рефакторинг.

Мініприклад такого винесення:

import java.util.List;

class CatalogConsolePrinter {
    void printSearch(List<CatalogBookSearchItem> items) {
        // Порожня видача — це нормальний результат пошуку, а не помилка
        if (items.isEmpty()) {
            System.out.println("Нічого не знайдено");
            return;
        }

        // Друкуємо завжди нормалізовані DTO: формат однаковий у real/mock
        for (CatalogBookSearchItem item : items) {
            // Найпростіший формат: id | title | author (можна ускладнити пізніше)
            System.out.println(item.externalId() + " | " + item.title() + " | " + item.author());
        }
    }

    void printDetails(CatalogBookDetails book) {
        // Деталі друкуються з нормалізованої моделі, а не з DTO провайдера/JSON
        System.out.println(book.externalId()); // OL12345M
        System.out.println(book.title());      // Clean Code
        System.out.println(book.author());     // Robert C. Martin
    }
}

І real, і mock тоді повертають одні й ті самі DTO, а printer друкує їх однаково. Режим і далі змінює тільки джерело даних.

6. Готовність клієнта: критерії

Фраза «клієнт готовий» звучить приємно, але сама по собі нічого не гарантує. В одному проєкті «готовий» означає «один запит працює, якщо інтернет хороший». В іншому — «два режими, негативні сценарії, зрозуміла діагностика і єдиний формат результату». Нам потрібен другий варіант, тому що інакше наступний етап проєкту будуватиметься на піску.

Критерії завершеності — це не бюрократія. Це спосіб не обманути себе. Якщо критерії є, ви швидше помічаєте, де «дірка», і не ловите сюрпризи під час першої ж зміни коду. Для нашої клієнтської фази критерії досить приземлені: команди працюють, режими однакові, provider-деталі ізольовані, а виведення передбачуване.

Нижче — таблиця «як це виглядає на практиці». Це не «домашка», а шпаргалка, за якою ви самі можете подумки перевірити проєкт.

Критерій Як виглядає в real режимі Як виглядає в mock режимі Що це означає для проєкту
Команда catalog search ... працює Приходить список результатів Приходить список результатів Команда не залежить від джерела даних
Команда catalog details <id> працює Приходить картка книги Приходить картка книги Деталі відокремлені від транспортної частини
Формат результату однаковий Друкується через CatalogConsolePrinter Друкується через CatalogConsolePrinter Виведення не розгалужується за режимом
Немає витоків provider DTO Назовні не повертаються provider-типи Назовні не повертаються provider-типи Зовнішній контракт «запертий» усередині клієнта
Mock використовує JSON + mapping JSON читається mapper-ом JSON читається mapper-ом Mock перевіряє реальний шлях даних
Порожній результат обробляється спокійно Друк «Нічого не знайдено» Друк «Нічого не знайдено» Немає «падінь від порожнечі»
Діагностика зрозуміла Є операція/режим/ціль/підсумок/час Є операція/режим/джерело/підсумок/час У разі проблеми ви розумієте, де зламалося

Коли search/details, діагностика і режими тримаються на одному внутрішньому контракті, клієнтська частина перестає бути крихким набором демо-класів. Такий шар уже можна спокійно розкладати по пакетах, виносити з точки входу і збирати залежностями вручну — без страху, що real і mock раптово розійдуться в поведінці.

Зверніть увагу на один тонкий момент. Коли ми говоримо «однакова поведінка», ми не обіцяємо, що реальні дані й мокові дані збігатимуться «буква в букву». Ми обіцяємо, що форма і правила поведінки збігаються. І це набагато важливіше.

Наприклад, у real режимі catalog search clean code може повернути 20 результатів. У mock режимі — 1 або 3. Це нормально. Але якщо real повертає List<CatalogBookSearchItem>, а mock раптово повертає «рядок з одним результатом» — це вже ненормально, тому що код вище клієнта перестає бути єдиним.

Ще один момент: mock-дані мають повторювати саме ту частину контракту, яку ми реально читаємо. Якщо в mapping ми використовуємо doc.key() і doc.title(), а в sample JSON у книги немає key, то mock-режим або впаде, або дасть дивні null-значення. І це насправді добра перевірка: mock-режим нагадує вам, що контракт — це договір, а не «ну там якісь поля».

7. Типові помилки під час роботи з клієнтом

Помилка №1: mock-режим починає повертати «готові рядки», а не дані.
Це виглядає спокусливо: швидше написати return List.of(...) або взагалі System.out.println("MOCK RESULT"), ніж возитися з JSON. Але тоді mock не перевіряє ваш JSON mapping і перетворюється на іграшкову заглушку. Підсумок зазвичай такий: «У mock усе зелене, у real усе падає», і ви не розумієте чому.

Помилка №2: різні формати виведення для real і mock.
Часто це стається не спеціально: у real-режимі ви виводите «красиво», а в mock — просто друкуєте обʼєкт цілком або інші поля. Ніби дрібниця, але на практиці це ламає головну ідею: режим змінює джерело, а не поведінку. Якщо формат виведення різний, ви мимоволі починаєте додавати умови if(useMock) по всьому коду, і проєкт знову перетворюється на два проєкти.

Помилка №3: provider DTO «протікають» назовні, бо так простіше.
Найпідступніший варіант — коли ви спочатку повертаєте provider DTO «на хвилинку», щоб швидше зробити виведення, а потім забуваєте це виправити. За кілька днів provider DTO опиняються в ReadLaterApplication, у принтері, в обробниках команд. Після цього будь-який чх зовнішнього API стане вашим чхом. Виправляти це потім неприємно: доведеться вичищати типи з багатьох місць.

Помилка №4: читання mock-файлів через File, через що все працює тільки в IDE.
В IDE шлях src/main/resources/... «видно», і здається, що все добре. Але після збирання це вже не «файл на диску», а ресурс усередині JAR. У підсумку mock-режим відвалюється в найневдаліший момент. Якщо ви читаєте sample JSON через getResourceAsStream, ви уникаєте цієї проблеми і отримуєте однакову поведінку під час запуску з IDE та під час запуску зібраного артефакту.

Помилка №5: не обробляється порожній результат і «відсутність книги».
Новачок часто тестує лише happy-path: «Ну я ж знайшов clean code». А потім вводить щось неіснуюче — і клієнт або падає, або друкує «null null null», або мовчить. Порожній список — це нормальний результат пошуку, а не помилка. І якщо ви обробляєте його однаково в real і mock, ви отримуєте той самий знак зрілості: застосунок лишається передбачуваним навіть тоді, коли нічого не знайдено.

1
Опитування
Клієнт JSON, рівень 17, лекція 4
Недоступний
Клієнт JSON
Парсинг і мапінг даних
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ