1. Що ми називаємо «мінімальним робочим процесом» JSON-тесту
Коли вперше чуєш «JSON tests», дуже легко уявити собі щось страшне: конфіги ObjectMapper, якісь модулі, загадкові серіалізатори, а наприкінці — тест, що порівнює два полотна тексту на три екрани. Насправді все набагато спокійніше. Мінімальний робочий процес — це кілька дуже конкретних кроків, які повторюються з DTO в DTO, як добрий ранковий ритуал: почистив зуби, випив води, не написав @SpringBootTest.
Ідея така: у нас є DTO, наприклад CreateArticleRequest або ArticleDetailsResponse. Або ми беремо JSON-вхід і перевіряємо, що він коректно прочитався в об’єкт — це десеріалізація, або беремо об’єкт і перевіряємо, що він коректно записався в JSON — це серіалізація. Усе. Жодних контролерів, сервісів і репозиторіїв. Лише контракт форми даних.
Щоб цей ритуал був швидким і однаковим, Spring Boot дає slice @JsonTest, а ми використовуємо JacksonTester<T> як «швейцарський ніж» для читання, запису та зручних перевірок.
Невелика схема, щоб не загубитися, хто з ким розмовляє:
flowchart LR T["Тест JUnit 6"] --> JT["JacksonTester<T>"] JT -->|parseObject| DTO["Java DTO"] DTO -->|write| JT JT --> JSON["Вміст JSON"] JSON -->|assertions| T
2. @JsonTest: що піднімається в контексті
@JsonTest — це slice-анотація Spring Boot, тобто вона піднімає не весь застосунок, а лише той мінімальний шматок контексту, який потрібен для роботи з JSON. Це ключове: ми перевіряємо не зв’язування шарів, а конкретний ризик — серіалізацію та десеріалізацію.
Усередині такого тесту зазвичай є налаштований ObjectMapper з усіма налаштуваннями Boot, і Boot автоматично готує інфраструктуру для JacksonTester. При цьому не запускається сервлет-контейнер, не піднімаються контролери, не підтягується Spring Security, не стартують міграції бази даних. Для студента це означає приємну річ: тести виконуються дуже швидко, їх можна писати багато, і вони дають ранній сигнал про зміну контракту.
З практичної точки зору @JsonTest для вас — це режим із чіткою метою: «я зараз тестую JSON як контракт». Усе інше — HTTP, коди статусу, MockMvc, @WebMvcTest — ми свідомо не чіпаємо, тому що це інший шар і інше завдання.
Мінімальний каркас тесту виглядає так — буквально «скелет» на 5 рядків:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
@JsonTest // Піднімаємо лише JSON-slice, а не весь застосунок
class CreateArticleRequestJsonTest {
@Autowired
private JacksonTester<CreateArticleRequest> json; // Тестер, привʼязаний до конкретного DTO
}
Так, поки тест порожній. Але важливе вже сталося: Boot підняв slice, і ми отримали JacksonTester<CreateArticleRequest>.
3. JacksonTester<T>: навіщо він потрібен
Новачок часто запитує: «А навіщо мені JacksonTester, якщо є ObjectMapper? Я ж можу зробити objectMapper.readValue(...) і writeValueAsString(...)». Можете. Тільки потім у вас раптово виявиться 50 тестів, у кожному — по 5 рядків однотипного коду серіалізації, плюс різні способи порівняння JSON, плюс думка: «ой, я забув підключити такий самий mapper, як у застосунку».
JacksonTester вирішує саме цю побутову проблему. Він робить дві речі.
Перша — прив’язує тест до типу. JacksonTester<CreateArticleRequest> ніби говорить: «Я працюю саме з цим DTO». Помилитися складніше, автодоповнення в IDE приємніше, тест читається простіше.
Друга — дає зручні операції “write/parse” і готові містки до перевірок: ви не просто отримуєте рядок JSON, а об’єкт (JsonContent<T>) з можливістю точково витягувати значення за JsonPath і порівнювати їх із fixture-файлом, не влаштовуючи в коді тесту текстовий артхаус.
Мінімальна думка, яку варто утримувати: JacksonTester — це не магія, а ергономіка. Він не замінює розуміння контракту, він просто прибирає зайві рухи руками.
4. Request DTO: JSON → об’єкт
Request DTO — це те, що клієнт надсилає нам усередину системи. Для JSON-тесту request DTO найчастіше перевіряється з боку десеріалізації: ми подаємо JSON і запитуємо себе: «У Java-об’єкті вийшло саме те, що ми очікуємо?».
Зараз ми спеціально робитимемо максимально простий приклад. Наша мета — опанувати механіку @JsonTest + JacksonTester, а не протестувати весь домен ContentHub.
Припустімо, у нас є DTO CreateArticleRequest з полями title, summary, body, category. Ми хочемо зафіксувати, що JSON із такими полями читається коректно.
Тест у стилі AAA виходить коротким і зрозумілим:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import static org.assertj.core.api.Assertions.assertThat;
@JsonTest // Увімкнено JSON-slice: швидкий тест контракту DTO
class CreateArticleRequestJsonTest {
@Autowired
private JacksonTester<CreateArticleRequest> json; // Уміє parse/write для конкретного типу
@Test
void parsesCreateArticleRequest() throws Exception {
// Arrange: вхідний JSON (зручніше читати як text block, ніж як рядок з екрануванням)
String content = """
{
"title": "Spring Testing",
"summary": "Short",
"body": "Text",
"category": "JAVA"
}
""";
// Act: десеріалізуємо JSON у DTO
CreateArticleRequest request = json.parseObject(content);
// Assert: перевіряємо 1–2 змістовні поля, а не просто "не null"
assertThat(request.getTitle()).isEqualTo("Spring Testing");
assertThat(request.getCategory()).isEqualTo("JAVA");
}
}
Зверніть увагу на кілька речей, які здаються дрібницями, але потім економлять години.
Ми явно пишемо JSON просто в тесті. Для першого проходу це добре: усе видно очима, нічого не сховано. Коли DTO розростається, JSON можна винести у fixture-файл, але базова механіка від цього не змінюється.
Ми перевіряємо не лише «об’єкт створився», а два поля, які реально несуть зміст. Якщо тест перевіряє тільки «не null», він найчастіше доводить лише те, що Jackson не зламався. Це слабкий доказ.
І важливий момент: parseObject(...) повертає DTO, і далі ми перевіряємо його як звичайний Java-об’єкт. AssertJ тут почувається як удома.
5. Response DTO: об’єкт → JSON
Response DTO — це те, що система віддає назовні. Для JSON-тесту response DTO зазвичай перевіряється з боку серіалізації: ми будуємо об’єкт і запитуємо: «JSON виглядає так, як має виглядати наш контракт?».
Головний виклик тут психологічний: хочеться почати перевіряти все одразу. Але для першого проходу це лише роздує тест. Достатньо 1–2 полів, щоб зафіксувати базову техніку серіалізації й не втратити фокус.
Нехай у нас є ArticleDetailsResponse, і ми хочемо перевірити, що поле title серіалізується як title, а не як articleTitle. Навіть якщо зараз нас цікавить лише title, корисно створювати DTO вже в його реальній формі: у клієнта в тому самому контракті живуть status і publishedAt.
Приклад із точковою перевіркою за JsonPath:
import java.time.Instant;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import static org.assertj.core.api.Assertions.assertThat;
@JsonTest // Тут ми тестуємо контракт JSON, а не HTTP-шар
class ArticleDetailsResponseJsonTest {
@Autowired
private JacksonTester<ArticleDetailsResponse> json; // Тестер для серіалізації ArticleDetailsResponse
@Test
void writesTitle() throws Exception {
// Arrange: створюємо DTO в тій формі, в якій клієнт реально його читає
ArticleDetailsResponse response = new ArticleDetailsResponse(
"Spring Testing Basics",
"spring-testing",
ArticleStatus.PUBLISHED,
Instant.parse("2026-03-18T10:15:30Z")
);
// Act + Assert: серіалізуємо й точково перевіряємо поле за JsonPath
assertThat(json.write(response))
.extractingJsonPathStringValue("@.title")
.isEqualTo("Spring Testing Basics");
}
}
Тут є важливий нюанс: метод json.write(response) повертає не просто рядок, а JSON-вміст, до якого можна застосовувати спеціальні assert-методи. Ви ніби отримуєте «JSON із ручками»: можна витягти @.title, @.slug і так далі.
Якщо вам хочеться побачити «сирий JSON» — наприклад, щоб зрозуміти, що взагалі вийшло, — зазвичай можна дістати рядок. У Spring Boot це часто виглядає так:
import java.time.Instant;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import static org.assertj.core.api.Assertions.assertThat;
@JsonTest // Slice-тест: швидко і по суті
class ArticleDetailsResponseRawJsonTest {
@Autowired
private JacksonTester<ArticleDetailsResponse> json; // Використовуємо той самий налаштований mapper, що й у застосунку
@Test
void writesRawJson() throws Exception {
// Arrange
ArticleDetailsResponse response = new ArticleDetailsResponse(
"Spring Testing Basics",
"spring-testing",
ArticleStatus.PUBLISHED,
Instant.parse("2026-03-18T10:15:30Z")
);
// Act: отримуємо "сирий" JSON рядком — корисно для налагодження
String raw = json.write(response).getJson();
// Assert: демонстраційний приклад
assertThat(raw).contains("\"slug\":\"spring-testing\"");
}
}
Так, contains(...) — не ідеальний спосіб тестувати JSON: для робочих контрактів краще JsonPath, JSONassert або fixture-порівняння. Але тут він добре показує саму механіку: «об’єкт → JSON-рядок → перевірка».
6. Операції JacksonTester: шпаргалка
Коли ви тільки починаєте, легко заплутатися: де write, де parse, що повертається, куди застосовувати assertThat. Гарний спосіб не нервувати — тримати перед очима одну маленьку карту API.
| Що ми хочемо довести | Звідки починаємо | Метод JacksonTester | Що отримуємо | Як зазвичай перевіряємо |
|---|---|---|---|---|
| JSON коректно читається в DTO | Рядок JSON або файл | parseObject(...) | Java DTO | assertThat(dto.getX())... |
| DTO коректно перетворюється на JSON | Java DTO | write(...) | JSON-вміст | extractingJsonPath..., getJson(), isEqualToJson(...) |
Усе. Якщо ви запам’ятали цю таблицю — ви вже вмієте робити 80% повсякденних JSON-тестів.
7. Мікрофабрики тестових DTO
У response-тестах вам потрібно створювати DTO. І ось тут починаються класичні страждання новачка: або він створює DTO просто в тесті й отримує 25 рядків налаштування заради однієї перевірки, або пише «універсальний builder» на 200 рядків і ховає зміст тесту за завісою.
У мінімальному робочому процесі корисний третій шлях: маленька фабрика на 5–8 рядків, просто в тест-класі, яка створює об’єкт із зрозумілими значеннями. Жодної магії, просто акуратний helper.
Наприклад:
private ArticleDetailsResponse samplePublishedArticle() {
// Один зрозумілий еталонний обʼєкт: title, slug, status і publishedAt уже зафіксовані
return new ArticleDetailsResponse(
"Spring Testing Basics",
"spring-testing",
ArticleStatus.PUBLISHED,
java.time.Instant.parse("2026-03-18T10:15:30Z")
);
}
І тест стає читабельнішим:
@Test
void writesSlug() throws Exception {
// Arrange: беремо обʼєкт із маленької фабрики, щоб не дублювати налаштування
ArticleDetailsResponse response = samplePublishedArticle();
// Act + Assert: серіалізуємо й перевіряємо конкретне поле за JsonPath
assertThat(json.write(response))
.extractingJsonPathStringValue("@.slug")
.isEqualTo("spring-testing");
}
Зверніть увагу: це не «DSL» і не «тестова інфраструктура». Це просто спосіб не дублювати "Spring Testing Basics", "spring-testing", PUBLISHED і фіксоване publishedAt у кожному тесті. І водночас не ховати зміст: ви все ще бачите, які дані важливі.
8. Типові помилки в @JsonTest і JacksonTester
Помилка №1: намагатися перетворити JSON-тест на інтеграційний.
Дуже часта спокуса — трохи підтягнути в JSON-тест сервіси, потім репозиторій, потім «ну раз уже Spring піднявся…». На цьому місці @JsonTest перестає бути вузьким slice і перетворюється на незрозумілу істоту: наче й не @SpringBootTest, але вже й не швидкий контрактний тест. Якщо ви ловите себе на думці «а давайте тут перевіримо ще процес публікації», ви звернули не туди. Поверніться до питання: «Я зараз тестую форму JSON чи поведінку системи?».
Помилка №2: перевіряти лише “не впало”.
Тест виду «парситься → об’єкт не null» або «write → JSON не порожній» створює відчуття безпеки, але насправді доводить дуже мало. Такий тест не зловить ані перейменування поля, ані зникнення поля, ані зміну значення enum. Мінімальна дисципліна: фіксуйте хоча б 1–2 змістовні поля. Це дешево і різко підсилює доказову силу.
Помилка №3: змішувати в одному тест-класі десяток DTO.
Іноді здається зручним: «О, я зроблю DtoJsonTest і перевірю там усе підряд». Потім цей клас стає кладовищем контекстів і сценаріїв, а падіння одного тесту психологічно відчувається як «зламалося все». Набагато простіше жити за правилом: один DTO — один тестовий клас. Так і читається краще, і підтримувати простіше, і на рев’ю зрозуміліше.
Помилка №4: будувати «ідеальний універсальний тестовий builder» занадто рано.
Якщо ви на самому початку JSON-блоку будуєте супер-абстрактний генератор DTO з купою опцій, ви витрачаєте час не на тестування контракту, а на розробку тестового фреймворку. Для мінімального робочого процесу краще маленькі, чесні фабрики просто в тесті, які повертають об’єкт для конкретного сценарію.
Помилка №5: порівнювати JSON «як рядок цілком» усюди підряд.
Повне порівняння JSON іноді дуже корисне, особливо коли контракт зручніше фіксувати цілком через fixture-файл, але якщо ви почнете порівнювати цілі JSON-рядки вже на рівні «перевіримо поле title», ви отримаєте крихкі тести: пробіл, порядок полів, зайве поле — і у вас червоне. Мінімальний робочий процес краще починати з точкових перевірок (extractingJsonPath...) і лише потім, коли це справді потрібно, переходити до «контракт цілком».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ