Java & Spring Weekly
210 subscribers
17 photos
63 videos
55 links
Рассказываю о новинках Java & Spring Framework, выпускаю практические обучающие видео
Download Telegram
AssertJ Shorts №4 - Recursive Comparison и Factories

Как верно заметили в комментариях, я не рассказал еще про некоторые важные фишки AssertJ, такие как usingRecursiveComparison и InstanceOfAssertFactory. Спасибо, что заметили, исправляюсь!

Иногда обычный assertThat(actual).isEqualTo(expected) не работает — например, если сравниваемые объекты содержат вложенные поля, коллекции или даже циклические ссылки. В таких случаях на помощь приходит usingRecursiveComparison().

Он позволяет сравнивать объекты рекурсивно, т.е. по всем вложенным полям, даже если это разные экземпляры одного класса.


record Address(String city, String zip) {}
record User(String name, Address address) {}

@Test
void recursive_comparison_simple() {
User actual = new User("Alice", new Address("London", "12345"));
User expected = new User("Alice", new Address("London", "12345"));

assertThat(actual)
.usingRecursiveComparison()
.isEqualTo(expected);
}


Без usingRecursiveComparsion тест бы упал, так как объекты Address разные.

Кроме рекурсивного сравнения, usingRecursiveComparison позволяет исключать некоторые поля из сравнения. Особенно полезно, например, для полей типа createdAt и updatedAt, значения которых точно не совпадают у разных объектов:


record User(String name, int age) {}

@Test
void ignore_field() {
User actual = new User("Alice", 30);
User expected = new User("Alice", 99); // возраст не важен

assertThat(actual)
.usingRecursiveComparison()
.ignoringFields("age")
.isEqualTo(expected);
}


Еще более интересная возможность - сравнивать списки объектов через usingRecursiveFieldByFieldElementComparator:


List<User> actual = List.of(new User("Alice", new Address("London", "12345")));
List<User> expected = List.of(new User("Alice", new Address("London", "12345")));

assertThat(actual)
.usingRecursiveFieldByFieldElementComparator()
.containsExactlyElementsOf(expected);


Некоторые поля нужно сравнивать по особым правилам, например, BigDecimal с учетом или без учета точности. Здесь помогут withComparatorForFields и withEqualsForType:


record Price(BigDecimal amount) {}

@Test
void with_comparator_for_field() {
Price actual = new Price(new BigDecimal("10.005"));
Price expected = new Price(new BigDecimal("10.004"));

Comparator<BigDecimal> roundedComparator =
Comparator.comparing(bd -> bd.setScale(2, RoundingMode.HALF_UP));

assertThat(actual)
.usingRecursiveComparison()
.withComparatorForFields(roundedComparator, "amount")
.isEqualTo(expected);
}


Здесь amount сравнивается уже после округления до двух знаков после запятой.

Если одни и те же настройки сравнения используются несколько раз, их можно вытащить в отдельную конфигурацию и использовать несколько раз:


RecursiveComparisonConfiguration config = RecursiveComparisonConfiguration.builder()
.withIgnoredFields("id", "timestamp")
.withIgnoreAllActualNullFields(true)
.build();

assertThat(actual)
.usingRecursiveComparison(config)
.isEqualTo(expected);


И кратко про InstanceOfAssertFactory - совсем забыл, насколько крутая это фича. В обычных ассёртах мы можем легко проверять свойства объектов, но при использовании методов вроде extracting() или filteredOn() тип выводится как Object, и AssertJ не знает, какие методы доступны у результата.

Чтобы сохранить тип и использовать нужные ассёрты, можно использовать InstanceOfAssertFactory. Без InstanceOfAssertFactory:


List<Person> people = List.of(new Employee("Alice", "HR"), new Manager("Bob", 5));

assertThat(people)
.filteredOn(p -> p instanceof Manager)
.first() // тип Object
.extracting("teamSize") // работает, но не типобезопасно
.isEqualTo(5);


А теперь с InstanceOfAssertFactory:
❤2
InstanceOfAssertFactory<Manager, ObjectAssert<Manager>> managerAssert =
new InstanceOfAssertFactory<>(Manager.class, Assertions::assertThat);

assertThat(people)
.filteredOn(p -> p instanceof Manager)
.first(managerAssert) // теперь тип — Manager
.extracting(Manager::getTeamSize)
.isEqualTo(5);


Если ты часто используешь типы вроде String, Integer, Map, List, LocalDateTime и т.д., то нет необходимости создавать InstanceOfAssertFactory вручную — AssertJ уже поставляет готовые!


import static org.assertj.core.api.InstanceOfAssertFactories.*;

@Test
void use_predefined_instance_of_assert_factory() {
record Event(String type, Map<String, Object> payload) {}

var events = List.of(
new Event("click", Map.of("x", 10, "y", 20)),
new Event("submit", Map.of("formId", "abc123"))
);

assertThat(events)
.element(0)
.extracting(Event::payload, MAP) // используем готовую фабрику
.containsEntry("x", 10);
}


Спасибо большое за комментарии, пишите еще!
🔥10👍3
Awaility Shorts

Мне кажется, про AssertJ я рассказал все, что знал и использовал сам каждый день. Перейдем к следующей библиотеке, которая мне также помогает каждый день - Awaitility. Если есть необходимость проверить результаты работы, которая происходит в фоне, то Awaitility здесь просто незаменима. Естественно, всегда есть возможность сделать Thread.sleep(1000) в цикле, но Awaitility предлагает гораздо более удобный DSL.

Например, здесь тест ждет до трех секунд, пока значение flag сменится на true. Если за три секунду этого не произойдет - тест упадет.


import static org.awaitility.Awaitility.await;
import static java.util.concurrent.TimeUnit.SECONDS;

@Test
void shouldWaitUntilValueIsSet() {
AtomicBoolean flag = new AtomicBoolean(false);

Executors.newSingleThreadScheduledExecutor()
.schedule(() -> flag.set(true), 1, SECONDS);

await().atMost(3, SECONDS)
.untilTrue(flag);
}


Условие можно передать и лямбдой:


await().atMost(2, SECONDS)
.until(() -> repository.findAll().size() == 3);


DSL Awaitility включает еще несколько очень полезных методов, например, pollInterval - как часто проверять:


await()
.pollInterval(200, MILLISECONDS)
.atMost(5, SECONDS)
.until(() -> repo.isInitialized());


atLeast - минимальное время ожидания, даже если условие будет выполнено сразу, то тест все равно подождет указанное в atLeast время:


await()
.atLeast(1, SECONDS) // в любом случае ждем одну секунду
.atMost(5, SECONDS)
.until(() -> cache.isWarm());


Иногда проверяемый метод может бросать исключения до момента полной инициализации. Эти исключения можно игнорировать через ignoreException:


await()
.ignoreExceptions()
.atMost(2, SECONDS)
.until(() -> service.getStatus().equals("READY"));


Также можно игнорировать исключения только определенных типов:


await()
.ignoreException(IllegalStateException.class)
.until(() -> client.getConnection().isAlive());


И что я люблю больше всего, так это интеграцию с AssertJ через untilAsserted:


await()
.atMost(3, SECONDS)
.untilAsserted(() -> {
assertThat(store.getData()).isNotEmpty();
assertThat(store.getData()).containsExactly("hello");
});


Используете ли вы библиотеку Awaility в своей работе?
🔥7👍4
Про AssertJ поговорили, про Mockito тоже поговорили, даже Awaitility затронули. Значит, пришло время поговорить про WireMock!

WireMock — это инструмент, который поднимает локальный HTTP-сервер прямо внутри теста (или отдельным процессом) и позволяет:

• эмулировать внешние API;
• контролировать ответы (и их задержки);
• проверять, что запросы отправлялись с правильными параметрами;
• работать без реального интернета.

Минимальный пример без расширений для JUnit и другой автоматизации:


import com.github.tomakehurst.wiremock.WireMockServer;
import static com.github.tomakehurst.wiremock.client.WireMock.*;

public class WireMockExampleTest {

private WireMockServer wireMockServer;

@BeforeEach
void setup() {
wireMockServer = new WireMockServer(8089);
wireMockServer.start();

configureFor("localhost", 8089);
stubFor(get(urlEqualTo("/hello"))
.willReturn(aResponse()
.withStatus(200)
.withBody("Hello from WireMock!")));
}

@AfterEach
void tearDown() {
wireMockServer.stop();
}

@Test
void shouldReturnStubbedResponse() {
String response = new RestTemplate()
.getForObject("http://localhost:8089/hello", String.class);

assertThat(response).isEqualTo("Hello from WireMock!");
}
}


Существенную часть здесь занимает настройка самого WireMock-а, которую можно существенно упростить, если добавить расширение для JUnit:


public class WireMockJUnit5Test {

// вот здесь расширение и регистрируется
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().port(8089))
.build();

// обычный тест
@Test
void shouldReturnStubbedResponse() {
wireMock.stubFor(get(urlEqualTo("/hello"))
.willReturn(aResponse()
.withStatus(200)
.withBody("Hello JUnit 5 + WireMock!")));

String response = new RestTemplate()
.getForObject("http://localhost:8089/hello", String.class);

assertThat(response).isEqualTo("Hello JUnit 5 + WireMock!");

wireMock.verify(getRequestedFor(urlEqualTo("/hello")));
}
}


Мне очень нравятся библиотеки, в которых API читаемый. Мне кажется, WireMock в этом плане - одна из лучших библиотек, настраивать сам WireMock одно удовольствие. Настраивать ответы, которые WireMock выдает уже не так просто, но API довольно выразительный и старается помочь изо всех сил.

Самый простой вариант - отвечать заранее заготовленным текстом и статусом:


stubFor(get(urlEqualTo("/hello"))
.willReturn(aResponse()
.withStatus(200)
.withBody("Hello from WireMock!")));


Отвечать ошибкой:


stubFor(get("/error")
.willReturn(serverError())); // статус 500


Отвечать с задержкой:


stubFor(get("/slow")
.willReturn(aResponse()
.withFixedDelay(2000) // задержка 2 секунды
.withStatus(200)
.withBody("Delayed!")));


И, наконец, динамическая генерация ответа на основе данных из запроса:


stubFor(get(urlMatching("/user/.*"))
.willReturn(aResponse()
.withStatus(200)
.withTransformers("response-template")
.withBody("Hello {{request.path.[1]}}!")));


Кроме настройки запросов, WireMock позволяет проверять, какой именно запрос пришел (вдруг, не все параметры указаны). Самые простые варианты:


// проверим, что пришел GET запрос
verify(getRequestedFor(urlEqualTo("/hello")));

// проверим, что пришел POST запрос
verify(postRequestedFor(urlEqualTo("/api/data")));


Как и в случае с Mockito, можно проверять, сколько было запросов:


verify(2, getRequestedFor(urlEqualTo("/hello")));


Можно проверить наличие заголовка, например, что указан правильный content type:


verify(postRequestedFor(urlEqualTo("/submit"))
.withHeader("Content-Type", equalTo("application/json")));
🔥8👍3
Или, что в теле запроса есть JSON в определенном формате:


verify(postRequestedFor(urlEqualTo("/submit"))
.withRequestBody(matchingJsonPath("$.user.id", equalTo("123"))));


Аналогично, можно проверять и GET запросы на наличие параметров:


verify(getRequestedFor(urlPathEqualTo("/list"))
.withQueryParam("page", equalTo("3")));


Какими еще возможностями WireMock вы пользуетесь?
🔥8👍4
Test Containers Shorts

Test Containers — это библиотека для запуска Docker-контейнеров прямо из тестов и сегодня будет короткая заметка об этой библиотеке.

Для примера запустим PostgreSQL прямо из теста (пока без интеграции с JUnit):


import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import static org.assertj.core.api.Assertions.assertThat;

class PostgresTest {

// Поднимаем контейнер PostgreSQL
static final PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");

static {
postgres.start();
}

@Test
void testDatabaseConnection() throws Exception {
var connection = postgres.createConnection("");
var result = connection.createStatement().executeQuery("SELECT 1");
assertThat(result.next()).isTrue();
assertThat(result.getInt(1)).isEqualTo(1);
}
}


Для JUnit есть расширение, которое автоматически запускает и останавливает контейнеры до и после выполнения теста:


@Testcontainers
class JUnit5Test {

@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");

@Test
void testIsRunning() {
assertThat(postgres.isRunning()).isTrue();
}
}


Test Containers довольно удобно интегрируются со спринговыми тестами через @DynamicPropertySource:


@SpringBootTest
@Testcontainers
class UserRepositoryTest {

@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");

@DynamicPropertySource
static void overrideProps(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}

@Autowired
private UserRepository userRepository;

@Test
void testSaveAndFindUser() {
User user = new User(null, "Alice");
userRepository.save(user);

var users = userRepository.findAll();
assertThat(users).hasSize(1);
assertThat(users.get(0).getName()).isEqualTo("Alice");
}
}


А вы используете Test Containers в своей работе?
👍10🔥6
Media is too big
VIEW IN TELEGRAM
Hibernate vs jOOQ vs SQL - Engineering Trade-offs

Hibernate долго был для меня инструментом по умолчанию в Java-разработке — быстрый, удобный, “магический”. Но со временем я стал всё чаще замечать его ограничения и переходить к jOOQ или plain SQL.

В этом видео мой взгляд на ORM глазами инженера, который работал с реальными системами: где Hibernate помогает, а где начинает усложнять жизнь.
🔥14❤2
Media is too big
VIEW IN TELEGRAM
Hibernate - Спасение или проблема?

Hibernate — это мощный ORM, который идеально раскрывается в бизнес-приложениях: CRUD, сложные доменные модели, связи между сущностями и быстрый старт разработки без постоянного написания SQL.

В этом видео разбираем:
— где Hibernate реально помогает
— почему он так хорош для CRUD и агрегатов
— как работает lazy loading и где он может «стрельнуть в ногу»
— когда JPQL уже не хватает и приходится идти в native SQL
— и где проходит граница между удобством ORM и контролем над базой

А как ты используешь Hibernate в своих проектах?
🔥7
Media is too big
VIEW IN TELEGRAM
Почему я всё реже выбираю Hibernate

В этом видео расскажу, где ORM создаёт больше проблем, чем решает, когда стоит использовать чистый SQL и почему jOOQ стал моим инструментом по умолчанию для работы с базой данных в Java.
👍8❤7
Media is too big
VIEW IN TELEGRAM
MCP сервер для Codex/Claude Code на Spring Boot

Создаём MCP Server на Spring Boot и подключаем его к Codex! В этом видео AI получит доступ к коллекции винила: поиск, просмотр и добавление пластинок через MCP. 🚀💿
🔥12❤2👍2
Media is too big
VIEW IN TELEGRAM
MCP клиент на Spring Boot - подключаемся к Ollama и MCP Server

В этом видео собираем MCP Client на Spring Boot и подключаем его к локально запущенному Ollama и MCP серверу на Spring Boot.

MCP Server предоставляет инструменты для управления коллекцией виниловых пластинок и его мы уже делали в прошлом видео.

Как сейчас модно говорить - без маркетинговой шелухи, четко и по делу, только Spring Boot, только стартеры и минимум кода.
🔥8❤3
Media is too big
VIEW IN TELEGRAM
AI-агент теперь ПОМНИТ всё! Добавляем память в Spring AI

В прошлом видео мы научили AI работать с MCP Server и коллекцией виниловых пластинок. Но была одна проблема — после каждого сообщения агент забывал весь предыдущий разговор.

В этом видео исправим это с помощью ChatMemory из Spring AI. Теперь AI будет помнить историю диалога, учитывать контекст и отвечать намного естественнее.
❤5👍5😁1
Media is too big
VIEW IN TELEGRAM
Микросервисы? Или лучше модульный монолит? | Engineering Trade-offs

Все слышали, что будущее за микросервисами. Но почему тогда Amazon, Shopify и другие компании всё чаще говорят о модульных монолитах?

В этом выпуске разбираемся:
* почему микросервисы вообще появились;
* какую цену приходится платить за распределённую систему;
* почему микросервисы часто решают организационные, а не технические проблемы;
* что такое модульный монолит и зачем нужен Spring Modulith;
* как AI меняет требования к архитектуре.

В инженерии редко бывают универсально правильные решения. Есть только trade-offs.
❤11🔥3👍1
Media is too big
VIEW IN TELEGRAM
Spring Modulith - модульный монолит на Spring Boot

В этом видео разбираем Spring Modulith — новый подход к построению модульных монолитов в Spring Boot.

Вы узнаете:
• зачем вообще нужен Spring Modulith;
• как разделить приложение на независимые модули;
• как автоматически проверять архитектурные границы;
❤8🔥3👍2
Media is too big
VIEW IN TELEGRAM
Распиливаем монолит на микросервисы со Spring Modulith

В этом выпуске превращаем модульный монолит в полноценную микросервисную архитектуру. Покажу, какие изменения действительно нужны, как использовать Kafka и HTTP Interface Clients, и как здесь помогает Spring Modulith
👍7❤4
Media is too big
VIEW IN TELEGRAM
SQL vs NoSQL: История борьбы и сотрудничества - Engineering Trade-offs

В начале 2010-х все были уверены, что SQL скоро исчезнет, а будущее за NoSQL. Но прошло 15 лет, и PostgreSQL по-прежнему остается одной из самых популярных баз данных. Что пошло не так?

В этом выпуске Engineering Trade-offs разбираемся, почему появился NoSQL, какие реальные проблемы он решил, почему этого оказалось недостаточно и как SQL и NoSQL в итоге изменили друг друга.
👍9
Media is too big
VIEW IN TELEGRAM
Как разбить большую таблицу на партиции в PostgreSQL

Рассказываю о том, как работать с партициями в PostgreSQL. Несколько примеров:

1. Создаем таблицу с несколькими партициями сразу.
2. Мигрируем существующие данные в таблицу с партициями.

Пишите в комментариях, какой подход при миграции использовали вы.
🔥9
Media is too big
VIEW IN TELEGRAM
MongoDB Sharding: как разделить миллион документов между серверами

Разбираемся, как работает MongoDB Sharding: поднимаем sharded cluster, распределяем миллион документов между серверами, смотрим на chunks, balancer и разбираемся, как MongoDB выбирает нужный shard для запроса
❤5👍2