Java & Spring Weekly
211 subscribers
17 photos
63 videos
55 links
Рассказываю о новинках Java & Spring Framework, выпускаю практические обучающие видео
Download Telegram
Продолжим говорить о Mockito. Мы уже научились управлять поведением моков, теперь пора поговорить о проверках — с какими параметрами и в каком порядке вызывались методы. В этом посте расскажу о самых полезных инструментах: verify, verifyNoInteractions, InOrder, @Captor и assertArg.

✅ verify — как проверить вызов метода

Если вы хотите убедиться, что метод был вызван, используйте verify. Это самый частый способ проверок в Mockito.


@Test
void verify_example() {
questions.isFeatureEnabled("abc");

verify(questions).isFeatureEnabled("abc");
}


Можно также проверять, сколько раз вызывался метод:


verify(questions, times(1)).isFeatureEnabled("abc"); // по умолчанию 1
verify(questions, never()).isFeatureEnabled(“xyz”);
verify(questions, atLeastOnce()).isFeatureEnabled(“xyz”);
verify(questions, atLeast(2)).isFeatureEnabled(“xyz");


Mockito также позволяет проверить и асинхронные вызов - взаимодействие с моком произошло в течение указанного промежутка времени:


verify(context, timeout(1000)).doCheck(); // ждать до одной секунды, что произойдет взаимодействие


🚫 verifyNoInteractions — метод не должен вызываться

Иногда нужно убедиться, что с мок-объектом вообще не взаимодействовали:


@Test
void noInteractions_example() {
verifyNoInteractions(questions); // важно: вызовов не было вообще
}


А если вы хотите проверить, что после каких-то действий не было дополнительных вызовов, используйте verifyNoMoreInteractions:


questions.isFeatureEnabled("abc");
verify(questions).isFeatureEnabled("abc");
verifyNoMoreInteractions(questions); // ни одного вызова больше


🔁 InOrder — проверка порядка вызовов

Бывает важно, в каком порядке вызывались методы — тогда пригодится InOrder.


@Test
void inOrder_example() {
questions.isFeatureEnabled("abc");
questions.isFeatureEnabled("xyz");

InOrder inOrder = inOrder(questions);
inOrder.verify(questions).isFeatureEnabled("abc");
inOrder.verify(questions).isFeatureEnabled("xyz");
}


Можно передать в inOrder(...) несколько моков и проверять, как они взаимодействовали между собой.

🎯 @Captor — захват аргументов

Если вы хотите получить значение аргумента, с которым вызывался метод — используйте @Captor. Особенно удобно, если метод принимал сложный объект.


@Captor
ArgumentCaptor<String> stringCaptor;

@Test
void captor_example(@Mock ContextQuestions questions) {
questions.isFeatureEnabled("abc");

verify(questions).isFeatureEnabled(stringCaptor.capture());

assertThat(stringCaptor.getValue()).isEqualTo("abc");
}


Не забудьте аннотацию @ExtendWith(MockitoExtension.class) в классе, чтобы @Captor работал.

Кстати, можно создать ArgumentCaptor прямо в самом тесте и без @Captor:


@Test
void captor_example(@Mock ContextQuestions questions) {
questions.isFeatureEnabled("abc");

final ArgumentCaptor<String> stringCaptor = ArgumentCaptor.forClass(String.class);
verify(questions).isFeatureEnabled(stringCaptor.capture());

assertThat(stringCaptor.getValue()).isEqualTo("abc");
}


🧪 assertArg — лаконичная проверка аргумента

Начиная с Mockito 5, можно использовать assertArg короткий способ проверить аргумент прямо в verify без использования `ArgumentCaptor`-ов.


@Test
void assertArg_example() {
questions.isFeatureEnabled("abc");

verify(questions).isFeatureEnabled(assertArg(arg -> {
assertThat(arg).startsWith("a");
}));
}


Пишите в комментариях, о каких еще интересных возможностях Mockito я не рассказал. А дальше расскажу об одной из моих любимых библиотек для тестирования - AssertJ.
👍6🔥2
AssertJ Shorts №1

Как и обещал, пост про мою любимую библиотеку для тестов - AssertJ. У проекта есть официальный веб-сайт https://assertj.github.io/doc/, он большой, красивый и там есть масса примеров, как круто можно использовать AssertJ.

Вообще, в JUnit есть и своя библиотека assertion-ов, которая неплохо расширяется, например, Hamcrest-ом. Из плюсов - это просто и доступно из коробки, из минусов - недостаточно выразительно. Что доступно в JUnit:


String name = "assertJ”;

assertEquals(“assertJ”, name);


В билиотеке JUnit есть еще несколько assert-ов, но все они вида assertEqual/assertNotEqual/assertTrue. В AssertJ выбор гораздо больше и все начинается с метода assertThat:


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

@Test
void basic_assertions() {
String name = "assertj";

assertThat(name)
.startsWith("asser”)
.endsWith("tj")
.isEqualToIgnoringCase("ASSERTJ")
.doesNotContain("mockito");
}


И дальше уже можно по цепочке добавлять проверки. В зависимости от типа параметра доступны различные вспомогательные методы, например, для целых чисел проверки уже другие:


@Test
void number_example() {
assertThat(42)
.isGreaterThan(10)
.isLessThanOrEqualTo(100)
.isNotZero();
}


Есть удобная поддержка Optional-ов:


@Test
void optional_example() {
Optional<String> value = Optional.of("hello");

assertThat(value)
.isPresent()
.contains("hello");
}

@Test
void optional_empty() {
Optional<String> value = Optional.empty();

assertThat(value)
.isEmpty();
}

@Test
void optional_value() {
Object content = new Object();
Optional<Object> value = Optional.of(content);

assertThat(value)
.hasValue(content);
}


Проверка сложных объектов с AssertJ - это песня! Например, нужно сделать несколько проверок сразу и только если все проходят, только тогда объект удовлетворяет условиям:


@Test
void custom_check() {
User user = new User("Alex", 30);

assertThat(user).satisfies(u -> {
assertThat(u.name).startsWith("A");
assertThat(u.age).isBetween(18, 60);
});
}


Работа с исключениями в AssertJ - просто песня:


@Test
void exception_example() {
assertThatThrownBy(() -> {
throw new IllegalArgumentException("bad input");
})
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("bad");
}


А какую библиотеку используете вы в своих проектах?
🔥11👍2❤1
AssertJ Shorts №2 - Collections

Продолжим про AssertJ. Уверен, за неделю вы уже смогли попробовать библиотеку в деле, поэтому расскажу о других интересных возможностях, которые не бросаются в глаза при чтении документации.

Начнем с моей любимой фичи - удобной работе с коллекциями.

Если у вас есть список объектов и вы хотите проверить их свойства — не надо писать stream().map(...). Просто extracting.


record User(String name, int age) {}

@Test
void extracting_example() {
List<User> users = List.of(
new User("Anna", 25),
new User("Bob", 30)
);

assertThat(users)
.extracting(User::name)
.containsExactly("Anna", "Bob");
}


Можно вытаскивать сразу несколько полей:


assertThat(users)
.extracting("name", "age")
.containsExactly(
tuple("Anna", 25),
tuple("Bob", 30)
);


И даже выполнять преобразование на извлеченных значениях:


assertThat(users)
.extracting(u -> u.name.toUpperCase())
.contains("ANNA", "BOB");


В моей практике часто нужно проверять и коллекцию из единственного элемента. Здесь у AssertJ есть несколько полезных методов, например, если в коллекции должен быть только один элемент, то к нему можно напрямую обратиться через singleElement:


@Test
void single_element_example() {
List<String> names = List.of("Alice");

assertThat(names)
.singleElement()
.isEqualTo("Alice");
}


Если элемент нужно проверить на соблюдение нескольких условий, то можно использовать singleElementSatisfies:


@Test
void single_element_satisfies_example() {
List<User> users = List.of(new User("Ann", 21));

assertThat(users).singleElementSatisfies(user -> {
assertThat(user.name).startsWith("A");
assertThat(user.age).isGreaterThan(18);
});
}


Как быть, если в коллекции есть несколько разношерстных элементов, причем все должны удовлетворять определенному условию, несколько - отдельному условию, и ни один - третьем. И здесь у AssertJ есть отличный набор вспомогательных методов - allMatch, anyMatch и noneMatch:


@Test
void any_match_example() {
List<String> names = List.of("Anna", "Bob", "Charlie");

assertThat(names)
.anyMatch(name -> name.length() == 3); // Bob
}



@Test
void all_match_example() {
List<String> names = List.of("Anna", "Alan");

assertThat(names)
.allMatch(name -> name.startsWith("A"));
}



@Test
void none_match_example() {
List<String> names = List.of("Anna", "Bob", "Charlie");

assertThat(names)
.noneMatch(name -> name.length() > 10); // никто не слишком длинный
}


Ну и напоследок еще несколько полезных методов по работе с коллекциями. Например, проверка размера коллекции, содержимого и порядка элементов в ней:


List<String> names = List.of("Anna", "Bob", "Charlie");

assertThat(names)
.hasSize(3)
.contains("Bob", "Anna")
.containsExactly("Anna", "Bob", "Charlie") // и порядок важен!
.doesNotContain("Diana");


Коллекцию перед проверкой можно и отфильтровать (и по значению поля тоже):


List<String> names = List.of("Anna", "Bob", "Alice");

assertThat(names)
.filteredOn(name -> name.startsWith("A"))
.containsExactly("Anna", "Alice");


В заключительной части расскажу о том, как сделать свой собственный custom assert.
🔥10👍4
AssertJ Shorts №3 - Custom Assertion

Кастомные assertions — одна из сильных сторон AssertJ. Они позволяют писать тесты, которые выглядят как читаемый DSL, например:


assertThat(user).hasValidEmail().isAdult();


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


@Value
public class User {
private final String name;
private final String email;
private final int age;
}


А теперь приступим как самому assertion - создаем класс, наследуем его от AbstractAssert, а в качестве параметров типов указываем класс assertion-а и тот класс, который assertion должен проверять:


import org.assertj.core.api.AbstractAssert;

public class UserAssert extends AbstractAssert<UserAssert, User> {
// (1)
public UserAssert(User actual) {
super(actual, UserAssert.class);
}

// (2)
public static UserAssert assertThat(User actual) {
return new UserAssert(actual);
}
}


Здесь (1) - конструктор, который принимает проверяемый объект, а (2) - первый вызов assertThat, который создаст экземпляр assertion-а. Теперь остается только добавить методы для проверки:


import org.assertj.core.api.AbstractAssert;

public class UserAssert extends AbstractAssert<UserAssert, User> {

// конструктор и assertThat должны быть здесь

public UserAssert hasValidEmail() {
isNotNull();

if (actual.getEmail() == null || !actual.getEmail().contains("@")) {
failWithMessage("Expected user to have valid email, but was <%s>", actual.getEmail());
}

return this;
}

public UserAssert isAdult() {
isNotNull();

if (actual.getAge() < 18) {
failWithMessage("Expected user to be adult (18+), but was <%s>", actual.getAge());
}

return this;
}
}


И в тестах теперь можно написать:


import static your.package.UserAssert.assertThat;

@Test
void custom_assertion_example() {
User user = new User("Alex", "alex@example.com", 20);

assertThat(user)
.hasValidEmail()
.isAdult();
}


Кроме собственных assertion-ов можно еще создать condition, который используется для проверки элементов коллекции. Подойдет, если одно и то же условие используется во многих местах:


@Test
void condition_example() {
User user = new User("Alice", "alice@example.com", 20);
assertThat(user).has(isAdult);

List<User> users = List.of(
new User("Bob", "bob@texample.com", 17),
new User("Alice", "alice@example.com", 20)
);

assertThat(users).haveExactly(1, isAdult); // один взрослый
}


А, isAdult в этом случае - отдельный класс:


import org.assertj.core.api.Condition;

Condition<User> isAdult = new Condition<>(
user -> user.age() >= 18,
"an adult (18+)"
);


Если условие используется только один раз, то можно использовать match:


assertThat(user)
.matches(u -> u.age() >= 18, "age is 18+");


О каких еще возможностях библиотеки assertJ вы хотели бы рассказать?
🔥5👍4
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