Java & Spring Weekly
210 subscribers
17 photos
63 videos
55 links
Рассказываю о новинках Java & Spring Framework, выпускаю практические обучающие видео
Download Telegram
Forwarded from javaswag
https://javaswag.github.io/episode/72/

Слушать подкаст в Apple | Spotify | Yandex

В 72 выпуске подкаста Javaswag поговорили с Александром Барминым о Спринге и архитектуре Необанка

00:00 Начало
05:34 Значение доменной области в разработке
17:28 IBM FileNet и Java EE
22:45 Проблемы и эволюция Java EE
32:50 Spring и Spring Boot
48:10 Миграция между версиями Spring
56:05 Гибкость и сложности Spring Boot
01:01:02 Адаптация Spring к современным трендам
01:04:50 Проблемы зависимости от Spring
01:07:10 Конкуренция и эволюция Spring
01:14:49 Kotlin и Spring: синергия технологий
01:15:44 Эволюция TransferWise в Neobank
01:16:36 Архитектура Wise: микросервисы и AWS
01:19:21 Kubernetes и проблемы распределенных систем
01:24:55 Консистентность и механизмы реконсиляции
01:29:08 Управление микросервисами и версиями
01:33:20 Автоматизация обновления зависимостей
01:37:07 CI/CD и миграции баз данных
01:41:17 Деплой
01:46:49 Непопулярное мнение о языках программирования
01:50:00 Критика Spring Boot и его магии

Гость https://www.linkedin.com/in/abarmin/

Ссылки:
Канал Александра на YouTube - https://www.youtube.com/@ABarmin
Канал Java & Spring Weekly в Telegram - @java_weekly
Wise Tech Stack - https://medium.com/wise-engineering/wise-tech-stack-2022-edition-a6ac089a382f
Spring Cloud с Борисовым - https://youtu.be/4tSyz_v9w7Q

Кип сейф! 🖖
🔥12
Jump Game II - LeetCode with me 44

Задача на динамическое программирование. Иногда кажется, что самые быстрые алгоритмы на LeetCode это далеко не самые правильные или подходящие, поэтому рассмотрим сначала хороший алгоритм, а потом быстрый.

Ссылки: Ссылка на задачу | Patreon (если вы не в России) | Boosty (если вы в России) | #LeetCode
Media is too big
VIEW IN TELEGRAM
🔥6
Permutations - LeetCode with me 45

Сегодня рассмотрим две задачи на генерацию перестановок. Обе решаются сходным образом - с использованием алгоритма backtracking.

Ссылки: Ссылка на задачу | Patreon (если вы не в России) | Boosty (если вы в России) | #LeetCode
Media is too big
VIEW IN TELEGRAM
👍7❤‍🔥2
Rotate Image - LeetCode with me 46

Задача на поворот двухмерного изображения. Берем и поворачиваем.

Ссылки: Ссылка на задачу | Patreon (если вы не в России) | Boosty (если вы в России) | #LeetCode
Media is too big
VIEW IN TELEGRAM
❤5
Group Anagrams - LeetCode with me 47

Группируем анаграммы вместе. Анаграммы - слова, которые могут быть преобразованы друг в друга путем перестановки букв в них. Очевидное решение с сортировкой работает, но не дает достаточной производительности, поэтому напишем свой несложный алгоритм хеширования.

Ссылки: Ссылка на задачу | Patreon (если вы не в России) | Boosty (если вы в России) | #LeetCode
Media is too big
VIEW IN TELEGRAM
👍9❤4
Pow x ^ n - LeetCode with me 48

Реализуем функцию возведения в степень без использования стандартной библиотеки. Для повышения производительности воспользуемся знаниями из школьного курса алгебры.

Это последнее, семидесятое видео в 2024 году. За весь год мои видео посмотрели более 61 тысячи раз, что составляет более 8 тысяч часов. А еще на канал на YouTube подписалось больше 1100 новых зрителей. Довольно неплохой рост, на мой взгляд!

Спасибо всем, кто был со мной в 2024 году, увидимся в новом, 2025 году!

Ссылки: Ссылка на задачу | Patreon (если вы не в России) | Boosty (если вы в России) | #LeetCode
Media is too big
VIEW IN TELEGRAM
👍10❤3
Уважаемые читатели, поздравляю вас с наступающим Новым годом!🥳
Желаю в новом году решать побольше интересных задач, не терять мотивации и оптимизма.
А еще смотрите мои видео и пишите побольше комментариев 👨‍💻👩‍💻
С праздником! 🎉
Please open Telegram to view this post
VIEW IN TELEGRAM
🥰15🔥9🎄6🤝2
N-Queens - LeetCode with me 49

Первая задача в новом 2025 году. Начнем с уровня Hard и расставим ферзей по шахматной доске. Ничего сложного, обычный backtracking.

Ссылки: Ссылка на задачу | Patreon (если вы не в России) | Boosty (если вы в России) | #LeetCode
Media is too big
VIEW IN TELEGRAM
❤4
Maximum Subarray - LeetCode with me 50

Ищем подмассив произвольной длинный с максимальной суммой. Задача решается с использованием скользящего окна или алгоритма Кадано. Также разберем рекурсивный подход, основанный на идее divide and conquer.

Ссылки: Ссылка на задачу | Patreon (если вы не в России) | Boosty (если вы в России) | #LeetCode
Media is too big
VIEW IN TELEGRAM
❤7✍2
Spring Shorts №1 - Embedded Database

Долго думал, как продолжить, с каким материалом вернуться. С одной стороны, старые выпуски были, на мой взгляд, очень полезны - в них я разбирал внутреннее устройство Spring Framework. С другой стороны, возможно, мой темп подходит не всем - слишком медленно или слишком быстро, не видно весь код сразу, нет возможности быстро вернуться к нужному фрагменту и так далее. Попробую немного другой формат - текстовый, назову его Spring Shorts, а сегодня как раз первый выпуск.

Наверняка, многим известно, что если в Spring Boot добавить в качестве зависимости H2, то, по умолчанию, Spring Boot запустит H2 в памяти. Инстанс H2 можно настроить с помощью JDBC URL, который указывается в параметре spring.datasource.url в application.yml или application.properties. Если URL формата jdbc:h2:mem:test, то база данных будет запущена в памяти и все данные будут пропадать каждый раз, когда приложение перезапускается. Если же URL вида jdbc:h2:/data/test, то данные будут храниться на диске и сохраняться между перезапусками приложения.

Кроме этого, в самом Spring Framework есть возможность создавать in-memory инстансы не только H2, но также HSQL и Derby. Для этого нужно добавить соответствующую зависимость и объявить бин DataSource:


@Bean
DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.DERBY)
.build();
}


EmbeddedDatabaseType это enum, в котором есть H2, HSQL и DERBY. Еще можно указать параметр generateUniqueName и при каждом создании бина у базы данных будет уникальное имя, а если указать addScript, то можно добавить имя файла из ресурсов, в котором будет SQL-скрипт для создания структуры таблиц или добавления данных.

Зачем может понадобиться отдельный DataSource с in-memory базой данных? Для тестов, например. А если хотите добавить поддержку свой базы, то можно через setDataSourceFactory передать свой DataSourceFactory, который будет, например, в докере запускать CouchDB.
❤11🔥7👍4❤‍🔥1🥰1💘1
Spring Shorts №2 - SQL Query

Расскажу еще про одну малоизвестную возможность Spring JDBC (не того, который Spring Data JDBC, а обычного). Самым часто используемым компонентом из этого модуля по праву считается JdbcTemplate - он и в базу пишет, и читает, и запросы выполняет.

В Spring Data есть отличная фича - при запуске приложения валидирует, правильно ли названы методы и есть ли в базе нужные колонки в таблицах. Hibernate делает то же самое для HQL, а вот у JdbcTemplate такой возможности нет. Зато в Spring JDBC есть класс SqlQuery, который позволяет обернуть обычный SQL запрос в объект и также валидировать при старте приложения.

Сделаю небольшой сетап - у меня будет таблица people, которая создается следующим скриптом:


CREATE TABLE people (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);


Доменный объект Person:


public record Person(
Integer id,
String name,
Instant createdAt) {

public Person(String name) {
this(null, name, Instant.now());
}

public Person withId(int id) {
return new Person(id, name, createdAt);
}

}


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


public class FindPeopleQuery extends MappingSqlQuery<Person> {

public FindPeopleQuery(DataSource dataSource) {
super(
dataSource,
"SELECT id, name, created_at FROM people"
);
compileInternal();
}

@Override
protected Person mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Person(
rs.getInt("id"),
rs.getString("name"),
rs.getTimestamp("created_at").toInstant()
);
}
}


Все самое интересное - в конструкторе. Компонент зависит от DataSource, поэтому мы передаем его в качестве параметра в конструктор, а дальше вызываем родительский конструктор и передаем в него запрос, а затем вызываем compileInternal. compileInternal создает PreparedStatement и в этот-то момент и происходит валидация.


public FindPeopleQuery(DataSource dataSource) {
super(
dataSource,
"SELECT id, name, created_at FROM people"
);
compileInternal();
}


Вторая часть - маппер в методе mapRow, который конвертирует строку из базы данных в доменный объект. Ну, здесь обычный RowMapper, такой же как и в JdbcTemplate.

Но как быть, когда в запросе есть параметры? Очень просто - параметры можно объявить и убедиться, что в запросе они точно есть. Выглядит вот так:


public class FindPersonByIdQuery extends MappingSqlQuery<Person> {

public FindPersonByIdQuery(DataSource dataSource) {
super(
dataSource,
"SELECT id, name, created_at FROM people WHERE id = ?"
);
declareParameter(new SqlParameter("id", Types.INTEGER));
compile();
}

public List<Person> findAll() {
return execute();
}
}


Так как это отдельный класс, то можно добавить дополнительные методы, например, findAll и из него уже вызывать внутренние методы SqlQuery.
🔥11
Spring Shorts №3 - Simple JDBC Insert

В прошлый раз мы посмотрели на SqlQuery, но есть ведь еще SimpleJdbcInsert, который позволяет избежать написания запросов на вставку. Почему именно их? Потому что запросы на вставку довольно большие - insert into table values (column1, column2) values (:value1, :value2), а еще важно не перепутать порядок параметров и колонок.

Воспользуемся таблицей из прошлого поста:


CREATE TABLE people (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);


Вставлять будем Person - такой же, как и до этого. А для вставки создадим класс InsertPersonQuery:


public class InsertPersonQuery {

private final SimpleJdbcInsert jdbcInsert;

public InsertPersonQuery(DataSource dataSource) {
this.jdbcInsert = new SimpleJdbcInsert(dataSource)
.withTableName("people")
.usingGeneratedKeyColumns("id")
.usingColumns("name", "created_at");
}

public Person insertPerson(Person person) {
final int generatedId = jdbcInsert.execute(Map.of(
"name", person.name(),
"created_at", person.createdAt()
));
return person.withId(generatedId);
}
}


В конструкторе создаем SimpleJdbcInsert, указываем, в какую таблицу вставлять и значения каких колонок генерируются автоматически базой данных. Также можно указать имена колонок для вставки.

И самое главное - jdbcInsert.execute. Сюда можно передать Map с параметрами, выглядит вот так:


final int generatedId = jdbcInsert.execute(Map.of(
"name", person.name(),
"created_at", person.createdAt()
));


а еще можно завернуть доменный объект в BeanPropertySqlParameterSource и тогда Spring будет сам перекладывать значения из объекта в запрос:


public Person insertPerson(Person person) {
final int generatedId = jdbcInsert
.execute(new BeanPropertySqlParameterSource(person));
return person.withId(generatedId);
}


А вы что используете для доступа к базе данных? Вы все еще кипятите Hibernate? Тогда мы идем к вам!
🔥9👍5
Mockito Shorts №1 - Введение

В прошлый раз был Spring, до этого был Spring и вообще почти всегда я пишу и рассказываю про Spring. Пора рассказать о чем-то другом - сегодня будет про Mockito. Да, про библиотеку для замены реальных зависимостей на настраиваемые тестовые. Рассказывать я буду про совместную работу с JUnit 5 и MockitoExtension.

Юнит-тест с Mockito обычно выглядит как-то так:


@ExtendWith(MockitoExtension.class)
public class MockitoExtensionDemoTest {
}


Чем занимается MockitoExtension? Он выполняет несколько действий:

1. Перед запуском каждого теста он вызывает Mockito.mockitoSession().initMocks(this), который для всех свойств с аннотацией @Mock создает и настраивает объект мока, для всех свойств с аннотацией @Spy создает частичный мок (экземпляр класса, обернутый в мок), а также для всех свойств с аннотацией @InjectMocks создает экземпляр и внедряет зависимости.

2. После выполнения каждого теста вызывает Mockito.validateMockitoUsage(). Если вы видели сообщения вида Unnecessary stubbing или MissingMethodInvocationException, то это результат работы validateMockitoUsage - этот метод проверяет, что все используемые моки правильно настроены и использованы.

3. А еще MockitoExtension проверяет на наличие на тестовом классе аннотации @MockitoSettings, которая позволяет настроить строгость и не показывать ошибок вида unnecessary stubbing.

Можно обойтись и без MockitoExtension, если создавать моки самостоятельно. Для этого вызываем Mockito.mock() и передаем в качестве параметра класс, для которого нужно создать мок. Например, у меня есть следующие связанные компоненты, которые я хочу использовать в своем тесте:


interface ExecutionContext {
ContextQuestions getQuestions();
}

interface ContextQuestions {
boolean isFeatureEnabled(String featureName);
}


Чтобы создать мок для ExecutionContext можно создать поле в тесте и поставить на него аннотацию @Mock:


@ExtendWith(MockitoExtension.class)
public class MockitoExtensionDemo2Test {
@Mock
ExecutionContext context;
}


Или же создать мок вручную:


public class MockitoExtensionDemo2Test {
ExecutionContext context;

@BeforeEach
void setUp() {
context = mock(ExecutionContext.class);
}
}


Теперь у нас есть мок и для работы его нужно настроить. Сделать это можно через метод when:


@Test
void getQuestions_shouldReturnQuestions() {
final ContextQuestions questions = featureName -> true;
when(context.getQuestions()).thenReturn(questions);

assertThat(context.getQuestions()).isNotNull();
}


Что будет, если я попытаюсь вызвать context.getQuestions().isFeatureEnabled() без вызова when? Очевидно, будет NPE:


@Test
void getQuestions_shouldThrowException() {
assertThatThrownBy(() -> context.getQuestions().isFeatureEnabled("abc"))
.isInstanceOf(NullPointerException.class);
}


У Mockito достаточно много параметров, которые могут упростить работу и первый из них это smart nullls:


@Test
void getQuestions_withSmartNulls() {
context = mock(ExecutionContext.class, Answers.RETURNS_SMART_NULLS);
context.getQuestions().isFeatureEnabled("abc");
}


Или


@Mock(answer = Answers.RETURNS_SMART_NULLS)
ExecutionContext context;


В этом случае сообщение об ошибке будет более развернутое:


You have a NullPointerException here:
… name of the class goes here
because this method call was *not* stubbed correctly:


Если хочется замокать ContextQuestions, который находится внутри ExecutionContext, то можно создать два мока:


@Test
void getQuestions_nestedMocks() {
ContextQuestions questions = mock(ContextQuestions.class);
when(context.getQuestions()).thenReturn(questions);
when(questions.isFeatureEnabled("abc")).thenReturn(true);

assertThat(context.getQuestions().isFeatureEnabled("abc")).isTrue();
}
🔥5
Обычно так и делается, однако, Mockito может существенно облегчить жизнь, если использовать Anwers.RETURN_DEEP_STUBS. В этом случае код больше не бросает NPE, а возвращает мок, который возвращает значение по умолчанию:


@Test
void getQuestions_withDeepMocks() {
assertThat(context.getQuestions().isFeatureEnabled("abc")).isFalse();
}


Кроме этого, можно сразу настраивать возвращаемые моки:


@Test
void getQuestions_nestedMocks() {
when(context.getQuestions().isFeatureEnabled("abc")).thenReturn(true);

assertThat(context.getQuestions().isFeatureEnabled("abc")).isTrue();
}


Ну, и напоследок, можно попросить Mockito вызывать реальные методы вместо моков:


@ExtendWith(MockitoExtension.class)
class WithRealMethods {
@Mock(answer = Answers.CALLS_REAL_METHODS)
AllEnabledQuestions questions;
@InjectMocks
DefaultExecutionContext context;

@Test
void getQuestions_willCallRealMethods() {
assertThat(context.getQuestions().isFeatureEnabled("abc")).isTrue();
}
}


Это все на сегодня, в следующий раз посмотрим, как можно настроить мок с помощью thenAnswer вместо thenReturn.
👍8🔥4
Продолжим говорить о Mockito. В прошлом посте я рассказал о том, как настроить мок, чтобы он возвращал заранее заданное значение, сегодня расскажу о том, как сделать так, чтобы мок вычислял значение на лету. Для этого нужно использовать thenAnswer.


@ExtendWith(MockitoExtension.class)
class WithAnswer {
@Mock
ContextQuestions questions;

@Test
void isFeatureEnabled_withAnswer() {
when(questions.isFeatureEnabled(anyString())).thenAnswer(inv -> {
final String name = inv.getArgument(0, String.class);
return "abc".equals(name) || "xyz".equals(name);
});

assertThat(questions.isFeatureEnabled("abc")).isTrue();
}
}


В данном примере мы можем получить доступ к аргументам метода через параметр invocation и далее через getArgument.

Согласитесь, каждый раз писать inv.getArgument может быть утомительно, особенно для методов с одним параметром. Чтобы этого избежать можно использовать AdditionalAnswers:


@Test
void isFeatureEnabled_withAnswers() {
when(questions.isFeatureEnabled(anyString())).then(answer((Answer1<Boolean, String>) arg0 ->
"abc".equals(arg0) || “xyz".equals(arg0)));

assertThat(questions.isFeatureEnabled("abc")).isTrue();
}


В качестве параметра метод answer принимает типизированный интерфейс AnswerX или AnswerVoid, в котором в качестве типов указываются тип возвращаемого значения и тип параметров метода. Таким образом избавились от постоянных вызовов inv.getArgument.

Отдельно нужно сказать, что если метод должен выбрасывать исключение вместо возвращения результата, то можно использовать thenThrow.


@Test
void isFeatureEnabled_withException() {
when(questions.isFeatureEnabled(anyString())).thenThrow(new IllegalStateException("Not implemented"));

assertThatThrownBy(() -> questions.isFeatureEnabled("abc"))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Not implemented");
}


В случае, если метод мока ничего не возвращает, то стандартную конструкцию when(mock).thenReturn() использовать не получится. Вместо этого нужно использовать конструкции doAnswer, doReturn и doThrow:


@ExtendWith(MockitoExtension.class)
class WithVoidReturnType {
@Mock
DefaultExecutionContext context;

@Test
void doCheck_withAnswer() {
doAnswer(invocation -> {
// Simulate some logic
return null;
}).when(context).doCheck();

context.doCheck();

verify(context).doCheck();
}
}


Спасибо, что читаете мои посты, в заключительном посте расскажу о том, как проверять, с какими параметрами вызывались методы мока.
👍11🔥3❤1
Продолжим говорить о 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