Разработка с Дарьей Матвеевой – Telegram
Разработка с Дарьей Матвеевой
75 subscribers
15 photos
15 links
Этот блог про разработку на Java. Пишу про проблемы, с которыми сталкиваюсь, и их решения.
Download Telegram
Сейчас наша команда активно ищет новых разработчиков на Java, и я уже несколько недель участвую в собеседованиях.
Наблюдения:

▫️ Теперь понимаю, почему иногда, будучи кандидатом, получала вопросы, казавшиеся странными.
Например вопрос интервьюера, много ли вы писали кода на рабочем месте? Казалось бы, конечно, ведь я разработчик. Но вот например один из кандидатов сказал, что уже несколько месяцев не пишет код, а занимается задачами devops, хотя в резюме как раз указано разработчик .

▫️ Обычно наш тимлид дает несколько фрагментов кода и просит найти и исправить ошибку. И почему-то кандидаты неохотно пишут код. Большинство людей просто размышляли вслух, всего пару человек писали и исправляли. Один из них правда не приблизился к решению, но начал активно рефакторить существующие классы, из-за чего решение перестало компилироваться.

▫️И уже несколько человек сказали, что долго работали в реактивном стэке, и поэтому забыли Hibernate.
👍2😁2
Сейчас на нашем проекте идет активный перевод существующих spring boot микросервисов с oracle на postgres. Задача разработчика - адаптировать нативные запросы, затем завести микросервис локально у себя на машине и вызвать пару эндпойнтов, чтобы убедиться в минимальной работоспособности. Затем микросервис развертывается на дев стенде и передается в тестирование QA команде.
Этот процесс осложняется тем, что микросервисы часто имеют множество зависимостей от других микросервисов, и чтобы запустить один, нужно запустить несколько других.
И вдобавок, не все сервисы содержат юнит-тесты.
Поэтому нужно было писать довольно много тестов. И очень удобно оказалось запускать их лишь для репозиториев с нативными запросами, не поднимая весь контекст спринга:

1. К тестовому классу добавила аннотацию DataJpaTest (вместо SpringBootTest, которая поднимает весь контекст)
2. DataJpaTest использует embedded in-memory БД. Чтобы подключиться к нужной базе, использовала AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
3. Ну и ActiveProfile("test"), чтобы Spring Boot подхватил параметры подключения к базе из профиля test в test-application.yml

И когда нативные запросы отлажены с помощью такой конфигурации, для тестирования эндпойнтов использую MockMvc.
А в планах разобраться с WireMock, чтобы мокировать запросы к другим сервисам, и, соответственно, избавиться от необходимости дополнительно их развертывать.

#рабочее
3👍1🔥1
Продолжаю заметки по миграции spring boot микросервисов с oracle на postgres.
Во время адаптации миграции несколько раз сталкивалась с особенностями конвертации полей различных типов в двух БД.
Вот несколько примеров:

✔️ В oracle в нативном запросе есть джойн по колонкам с типами varchar and number.
Например:
create table employee ( id number )
create table tasks ( id number, employee_id varchar )

select * from employee e join tasks t on e.id = t.employee_id;


Oracle неявно делает конвертацию to_number(t.employee_id)=e.id, и если в таблице employee колонка employee_id содержит только строки, которые можно конвертировать в число, то запрос отработает нормально. Но postgres выдаст ошибку, и для решения нужно будет явно привести один тип к другому.

✔️Изначально в oracle в таблице БД есть колонка с типом number, а в коде, в сущности hibernate, она маппится на поле с типом String. В postgres колонка мигрирует с типом bigint. При работе приложения, oracle спокойно делает конвертацию, а postgres выдает ошибку на несоответствие типов.
Решение - заменить тип поля в сущности на Long.
Чуть было не пропустила эту ошибку, т.к. на первых порах писала тесты лишь для тех методов репозиториев, где объявлены нативные запросы. А она обнаружилась случайно как раз для сущности, для которой нет таких методов. После этого начала писать тесты вообще для всех сущностей и отловила не одну такую ситуацию.

✔️ В oracle в таблице БД есть колонка с типом varchar, которая маппится на поле с типом boolean. Предполагается, что в БД значения колонки 0/1.
Опять же, oracle конвертирует, а для корректной работы в postgres, нужно добавить аннотацию @Type(type="org.hibernate.type.NumericBooleanType") на поле сущности.

#рабочее
🫡3
Продолжение заметок по миграции spring boot микросервисов с oracle на postgresql.

1. Конструкция limit в postgresql заменяет подзапрос с аналитической функцией row_number в oracle.

Postgresql:
select product_id
from product
order by create_date limit 1

Oracle
:
select product_id
from (
select product_id, row_number() over (order by modify_date) as rn
from product
) t
where t.rn=1


2. Склейка строк через || в postgreql не null-безопасна в отличии от oracle. Если один из аргументов is null, то вся строка превратится в null. Поэтому лучше использовать функции concat и concat_ws.

3. Неочевидные ошибки вида Error near 'type', Error near 'precision'
Оказалось, что type и precision - это ключевые слова в postgresql, и при использовании их в качестве алиасов требуется указать as.

4. Insert и not null поля.
Cоздание сущности employee (через hibernate) работало в oracle, а в postgresql стало падать с ошибкой, что не заполнено обязательное поле is_retired.
Оказалось, что в oracle поле в таблице employee было объявлено с конструкцией on null, а в postgresql мигрировало без него, т.к. нет прямого аналога:

Oracle:
create table employee (
is_retired numeric default on null 0 not null
)

Postgresql:
create table employee (
is_retired numeric not null default 0
)

А если в postgresql явно передается null, то это переопределяет default.
В итоге пришлось в коде явно прописать дефолтное значение для атрибута isRetired.

#рабочее
🔥4👍1
https://dmatveeva.github.io/sql-relational-theory-1/

В статье – обзор первой главы книги “Sql и реляционная теория” Дж. Дейта.

Что такое реляционная модель и зачем она нужна разработчику?

При изучении sql реляционная модель обычно упоминается мимоходом, а основной упор делается на изучение языка sql, или даже какого-то его диалекта, например для oracle. Между тем, SQL и реляционная теория - не одно и то же, а профессионал в области баз данных должен знать реляционную теорию, т.к. вся область знания об управлении базами данных построена на ней. Разработчик может использовать БД разных вендоров, различные диалекты, но базовые принципы всегда останутся те же самые.
👍21
Возникла проблема при старте сервиса - долго не отвечают heathchecks сервиса в openshift.
Для данного сервиса healthchecks в Openshift проверяли liveness и readiness эндпойнты актуатора в Spring boot, и проблема оказалась в readiness probe.
Выяснилось, что при старте сервиса запускается продолжительный по времени метод формирования отчета, который сохраняется в БД в ожидании, когда его считает другой сервис.
Возник вопрос, насколько актуальна readiness probe для сервиса, который не принимает входящих запросов и лишь формирует отчет? Однако оказалось, что отключить ее нет возможности.
Поэтому следующим шагом стал разбор того, что происходит при старте.
Метод формирования отчета запускался с помощью аннотации:
@EventListener(AppricationReadyEvent.class)
public void generate() {
//логика создания отчета
}


В документации к классу AppricationReadyEvent сказано:
Event published as late as conceivably possible to indicate that the application is ready to service requests.

В документации к классу ReadinessState:
"Readiness" state of the application.
An application is considered ready when it's live and willing to accept traffic. "Readiness" failure means that the application is not able to accept traffic and that the infrastructure should stop routing requests to it.


Выглядит логично, что AppricationReadyEvent должен быть опубликован после того, как readiness probe станет UP.
Посмотрим, когда выставляет статус UP.

В методе SpingApplication.run(...), после создания, инициализации и подготовки контекста идет вызов listeners.ready(...)
  public ConfigurableApplicationContext run(String... args) {
...
try {
...
context = createApplicationContext();
...
}
try {
if (context.isRunning()) {
listeners.ready(context, startup.ready());
}
}
...
}

Здесь вызывается EventPublishingRunListener#ready, который и выставляет статус readiness probe в UP:

org.springframework.boot.context.event.EventPublishingRunListener#ready
@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context, timeTaken));
AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC);
}

А context.publishEvent в свою очередь вызывает

org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent
@Override
public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
...
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null && listener.supportsAsyncExecution()) {
try {
executor.execute(() -> invokeListener(listener, event));
}...
}


invokeListeners(...) - как раз приводит к вызову нашего метода generate, помеченного аннотацией @EventListener(AppricationReadyEvent.class)
Поэтому, пока он не завершится, readiness probe не будет переведена в статус UP.
Переопределить метод EventPublishingRunListener#ready нет возможности, поэтому было решено при старте запускать формирование отчета в новом потоке. Т.к. продолжительный метод генерации выполняется в новом потоке, в основном потоке метод generateAtStartup быстро завершается, не блокируясь, и readiness probe переводится в UP.

@Autowired
ScheduledExecutorService executorService;

@EventListener(ApplicationReadyEvent.class)
public void generateAtStartup() {
executorService.schedule(this::generateReport, 20, TimeUnit.SECONDS);
}

private void generate() { //логика создания отчета }

#рабочее
😁1
Прошла обучение для того, чтобы проводить технические собеседования по Java в компанию.
Провела тестовое интервью с кандидатом под наблюдением опытного интервьюера.
Также пообщалась с другими коллегами, кто имеет большой опыт проведения собеседований.
Записала советы, как проводить интервью эффективно:

▫️ Хотя и есть стандартные вопросы, которые необходимо задать, надо в первую очередь отталкиваться от уровня кандидата. Если человек на вопрос отвечает поверхностно, спросить о нюансах использования, о деталях реализации и т.д.

▫️ Если человек не может сформулировать ответ на вопрос, но при этом у него в резюме указан большой опыт в данной области, то стоит переформулировать тот же запрос, но с точки зрения практики. Например, если кандидат не может сказать формальное определение RESTful-вебсервиса, но при этом у него опыт разработки 10+ лет, то понятно, что он их разрабатывал и понимает, что это.

▫️ Если человек не понимает вопрос, нужно переформулировать на конкретном примере.

▫️ Если человек ответил неправильно или неполно, можно явно подсказать, что хочется услышать в ответе. Тот, кто разбирается в теме, сразу поймет о чем речь, и дополнит ответ.

▫️ Важно узнать, что человек понимает сложность используемых алгоритмов/структур данных.
🔥4
Появилась задача - оптимизировать сложный аналитический sql-запрос.

Первый возникший вопрос - как измерять эффективность оптимизации. Ведь при повторном выполнении запроса данные кэшируются, и запрос может выполняться на порядки быстрее, чем в первый раз. Существуют хаки для очистки кэшей - остановить Postgresql, очистить кэш ОС. Но это применимо только для локальной БД. В условиях кластера это может не сработать, да и прав на такие действия может не быть.
Остается только ориентироваться на план запроса. Основная цель - на каждом этапе минимизировать количество обрабатываемых строк. В моем случае запрос представлял собой соединение основной таблицы со многими другими, фильтрация по определенным атрибутам, а затем группировка по сущностям из основной таблицы.

План показал, что промежуточные результаты содержали миллионы строк, поэтому группировка была очень медленной. Также, в отчете были необходимы поля из основной таблицы, и для этого по ним тоже проводилась группировка (просто чтобы иметь возможность указать их в select).
Что было сделано:

1. Соединения таблиц с селективными фильтрами были вынесены в подзапрос до группировки, благодаря чему количество группируемых строк уменьшилось до десятков тысяч.

2. Тут оказалось, что количество таблиц, соединяемых в основном подзапросе, оказалось больше 10. В Postgresql после определенного количества соединяемых таблиц (по умолчанию 8) оптимизатор перестает искать оптимальный план и просто обрабатывает их в порядке объявления. А таблица с наиболее селективным фильтром оказалась в конце списка таблиц, поэтому в плане сначала не было заметно никакого улучшения. Но, при перемещении этой таблицы наверх списка, селективный фильтр сработал первым и дал значительное ограничение промежуточных результатов.

3. После группировки еще раз присоединила основную таблицу, чтобы выбрать из нее атрибуты, необходимые в отчете. Так ушла необходимость "протаскивать" их наверх из основного подзапроса, и проводить по ним лишнюю группировку.

Также надо иметь в виду, что оптимизатор Postgresql строит план на основе статистики - количества строк в таблицах, селективности атрибутов, по которым идет фильтрация и т.д.. Поэтому оптимизация, проведенная на dev-стенде, может быть неактуальной для промышленных данных, и ее надо обязательно протестировать на prod-стенде.

#рабочее
🔥3
HikariCP и Postgresql

Чтобы выполнить запрос в базе данных, сервер приложений должен установить с ней соединение. Это может быть затратно, т.к. надо открыть соединение через драйвер, открыть TCP-сокет, выполнить запрос, закрыть сокет, закрыть соединение. Таких подключений может быть сотни и тысячи, из-за чего лимит оперативной памяти может быстро исчерпаться.
Поэтому при обращении к БД клиенты (сервера приложений) используют пулы соединений. В случае, если в пуле есть свободное соединение, оно берется из пула. Если пул исчерпан, то запросы встают в очередь и ждут свободного соединения.

У нас на проекте в качестве сonnection pooler используется HikariCP. С базой данных Oracle приложение работало без проблем, но после перехода на Postgresql БД начала падать по непонятной причине. Оказалось, что hikari заполняет пул сразу максимальным количеством соединений, а не создает их постепенно. Поэтому все сервисы резервировали максимум соединений, и БД не выдерживала. Postgresql каждое соединение обрабатывает как отдельный процесс, а Oracle - как поток внутри общего процесса. В результате Oracle лучше держит большее число соединений.
Проблема решилась установлением корректных настроек HikariCP.

#рабочее
👍8
В системе, которую разрабатывает наша команда, у пользователей есть функция добавления описания различным продуктам. Появилась необходимость проанализировать, насколько осмысленно заполняются эти описания. Для этого нужно было проверить соответствующую колонку из БД Postgresql на то, являются ли ее значения комбинацией определенных строк, не несущих смысла, таких как 'описание', 'данные', 'продукт', всяких специальных символов и т.п.
Решение в лоб - просто сделать цепочку replace, заменяя каждое встреченное слово-заменитель на пустую строку, и проверить, превратилась ли все описание в пустую строку:
select id, denoscription, replace(replace(replace....replace(denoscription, 'описание', '')...'') ...'') = '' has_no_meaning
from product p

Но учитывая, что таких слов для проверки было более 100, это было бы весьма тяжело читать и поддерживать. Да и сам запрос немаленький, в нем рассчитываются и другие параметры.
Намного удобнее сделать проверку с использованием регулярного выражения:
select id, denoscription, regexp_replace(denoscription, '(описание|продукт|...|данные|-|,|\.)' , '', 'gi')) = '' has_no_meaning
from product p

Regexp_replace заменяет любое из слов, перечисленных в круглых скобках через черту, на пустую строку. Параметр g указывает на то, что заменяем все совпадения, а нет только первое встреченное, i - что игнорируем верхний/нижний регистр.
Еще удобнее не перечислять слова в теле запроса, а поместить их в отдельную таблицу. Так в будущем будет легко добавлять новые.
create table t_descr_substitute (
substitute varchar(2000)
);
insert into t_descr_substitute (substitute) values ('описание'); ...

Тогда запрос будет таким:
select  id, denoscription, regexp_replace(denoscription, a.pattern, '', 'gi') = '' has_no_meaning
from product p
join ( select '(' + string_agg(substitute, '|') + ')' pattern from t_descr_substitute ) a on 1=1

Здесь с помощью string_agg собираем строку-паттерн для регулярного выражения и затем используем ее в функции regexp_replace.

В какой-то момент во время экспериментов показалось, что регулярные выражения не работают для этой задачи, т.к. на любое описание проверка возвращала true, как будто все описания состоят из заменителей. Оказалось, что проблема в точке. В regex выражении она означает любой символ, соответственно все описания подходили под шаблон. Стоило записать ее с обратной чертой \. и все заработало как надо.

#рабочее
🤔6💅1
Pet-проект

У меня есть pet-проект, на котором тренируюсь в использовании новых технологий. Основной код был написан около полутора лет назад, и недавно решила его пересмотреть. Заметила много моментов, которые можно улучшить, и провела рефакторинг:

▫️ Привела API в соответствие с принципами REST. Было не совсем понятно, для чего нужен тот или иной эндпойнт, из-за не очень логичного разделения методов по контроллерам. Много лишних path variables переделала в request parameters. Итоговый вариант оказался чище и понятней.

▫️ Доработала UI. Добавила datepicker-s и datetimepicker-s там, где раньше был просто текстовый ввод дат. Привела формы и таблицы во всем проекте к единому формату, а также поправила поля в формах, чтобы выглядели одинаково.

▫️ Переделала генерацию ключей для entity. Ранее использовался один генератор для всех сущностей, поэтому при ручном обновлении БД через скрипты приходилось внимательно следить, чтобы не перепутать id сущностей разных типов, иначе возникали ошибки.

▫️ Заметила, что при старте контейнера с базой данных скрипты инициализации отрабатывают довольно медленно. Оказалось, что изначально сгенерированные тестовые данные выгрузила из БД отдельными insert для каждой строки. После объединения строк инициализация пошла гораздо быстрее.

▫️ Провела также рефакторинг классов и методов, которые показались теперь излишне запутанными.

В планах — поднять зависимости до самых новых версий и отрефакторить deprecated код, появившийся за это время.
👍8
Сейчас на рабочем проекте провожу миграцию сервисов со Spring Boot 2 на Spring Boot 3. Вот некоторые шаги этого процесса:

Для начала подключить зависимость spring-boot-properties-migrator, которая позволяет анализировать файлы конфигурации (application.yml) в рантайме, и автоматически определяет устаревшие или переименованные свойства.

Перейти на Java 17 с 11, т.к Spring Boot 3 не поддерживает более ранние версии.

Перевести Swagger со Springfox на Springdoc. Обе эти библиотеки служат для интеграции Swagger со Spring Boot. Но Springfox уже не развивается (последний коммит в репозитории был в 2021), не совместима со Spring Boot 3 и не поддерживает OpenAPI 3.

Часть диалектов hibernate устарели, т.к. в Hibernate 6 появился механизм автоматического определения диалекта на основе используемой базы данных. Поэтому больше нет нужды использовать диалекты, специфичные для конкретной версии БД, например PostgreSQL10Dialect.

Поправить конфигурации стартеров. В Spring Boot 3 автоконфигурации рекомендуется объявлять в файле META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, а не в spring.factories..

Перейти на Spring Security 6. Требуется обновление кода, т.к. методы для конфигурации авторизации и аутентификации значительно изменились.

#рабочее
👍4
Во время перевода одного из приложений на Spring Boot 3 возникла ошибка - оказалось, что в приложении добавлены в качестве зависимостей два spring boot стартера, которые содержали конфигурационные бины с одинаковыми именами.
После исправления названия одного из бинов возникла другая ошибка — Spring не мог определить, какой из бинов одинакового типа использовать. В приложении нужен был бин из стартера №1. Возможные решения:
1. Пометить бин из стартера №1 как @Primary. Тогда использоваться будет именно он.
2. Пометить бин из стартера №2 аннотацией @ConditionalOnMissingBean, чтобы он создавался только в случае, не существует первого, т.е. если не подключен первый стартер (для сервисов, которые используют только второй стартер).
3. В моем случае, оказалось возможным провести рефакторинг стартеров, и первый стартер был подключен во второй в качестве зависимости, а бин из второго стартера соответственно был удален.

#рабочее
👍3
Тестирование под различными профилями Spring

Подготовила очередной сервис для перехода на Spring Boot 3.
Тесты прошли успешно, локально сервис запускался успешно. Уже собиралась выкладывать правки на ревью.
Но просматривая файлы для коммита, вспомнила, что на тестовом стенде надо отключить Swagger, и решила проверить, что он действительно не доступен при работе под профилем test. И вот запускаю приложение - а оно не стартует. Нет спрингового бина, который требуется в кастомизированном OpenApiWebMvcResource. При чтении документации выяснилось, что нужный бин не создается при отключении свойства springdoc.api-docs.
Для фикса оказалось достаточно добавить к ресурсу аннотацию @ConditionalOnBean(SpringDocConfiguration.class), чтобы он вообще не создавался при отключенном Swagger.
Но из этой ошибки вынесла для себя важность тестирования приложения под всеми объявленными профилями. Ведь если бы не проверила этот момент локально, то баг выявился бы лишь на тестовом стенде, что потребовало бы отдельного фикса, пересборки дистрибутива, повторного развертывания, а также затронуло бы команду тестирования. А сейчас фикс получился быстрым и безболезненным. Поэтому в список необходимых проверок перед созданием MR для себя добавила также тестирование приложения под разными профилями.
1🤔1
Как Spring ищет бины при старте приложения?

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

Если просто создать класс с нужным функционалом в библиотеке, пометить его как @Component, и подключить библиотеку в проект в качестве зависимости, создаст ли фреймворк этот бин? Ответ - нет.
В приложении Spring Boot, application класс обычно помечается аннотацией @SpringBootApplication, которая включает в себя аннотацию @ComponentScan. И фреймворк будет сканировать на предмет наличия бинов только пакет, в котором объявлен класс помеченный какой-либо из этих аннотаций, и его подпакеты.
Spring не сканирует весь classpath или все приложение для поиска бинов.
Если надо инжектировать в класс бин из подключенной библиотеки, например ru.mylib.MyBean, можно, например, добавить к application классу аннотацию @ComponentScan("ru.mylib").

Есть и другой способ - использовать spring boot starters.
Чем стартер отличается от библиотеки? Помимо того, что стартер обычно содержит набор библиотек, стартеры включают в себя автоконфигурации.
Автоконфигурация - это класс конфигурации, помеченный анннотацией @Configuration или @AutoConfiguration. В стартере они должны быть перечислены в файле spring.factories или org.springframework.boot. autoconfigure.AutoConfiguration.imports. При запуске приложения Spring Boot сканирует classpath, и находит все jar с данными файлами. И создает все бины, которые объявлены в файлах автоконфигурации.

В частности, можно превратить нашу библиотеку myLib в стартер, добавив автоконфигурацию с аннотацией @ComponentScan("ru.mylib"), и тогда все бины из данного пакета будут созданы и доступны во время работы приложения.
Продолжаю проводить технические интервью в компанию.
Немного заметок за 3 месяца:

1. В начале интервью стала лучше определять уровень кандидата, и теперь стараюсь задавать вопросы, отталкиваясь от уровня, а не просто пробегая по всем вопросам из списка.

2. Часто делаю акцент на Spring, т.к. вопросы по чистой Java у всех на слуху, и обычно кандидаты отвечают нормально.

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

3. Встречаю нередко, что кандидаты не понимают сложностей алгоритмов/используемых структур данных, и просто заучивают. Был случай, когда человек правильно рассказал сложности методов стандартных СД, но неожиданно не смог ответить про сложность вложенного цикла.

4. Очень интересно и ценно, когда кандидат не просто отвечает на вопрос, но приводит пример из своего опыта. Такое обычно встречаю у сильных кандидатов.

5. Стало проще соблюдать тайминг, и возвращать кандидата к теме, если разговор уходит в сторону. Но до сих пор порой выхожу за пределы стандартных полутора часов, т.к. хочется составить максимально объективное мнение. И бывает, что под конец интервью звучат именно те вопросы, которые повышают общую оценку.
🔥5👍1
Testcontainers и неудачные тесты

В последнее много занимаюсь интеграционными тестами на тестконтейнерах.
Во время тестового прогона в какой-то момент получила ошибку - контейнер не создается из-за нехватки памяти на сервере. Подключилась к серверу, выполнила docker ps - а там крутится множество контейнеров kafka и postgres, которые я использую в тестах.
Потушила висящие контейнеры, тесты снова стали запускаться, но остался вопрос, что это было? Ведь тестконтейнеры должны автоматически останавливаться и удаляться после прогона теста. Опытным путем выяснила, что контейнеры не удаляются, если тест был остановлен принудительно.
Жизненным циклом тестконтейнеров управляет специальный контейнер ryuk. Он живет, пока живет JVM, и поэтому логично, при принудительной остановке JVM он экстренно завешает свою работу, не успевая остановить другие контейнеры.
Но неожиданно оказалось, что у коллеги, при точно таком же запуске и остановке тестов, контейнеры удаляются ryuk-ом.
Стала разбираться, почему у меня ruyk завершается в первую очередь, и не успевает удалить другие контейнеры, а у коллеги сначала удаляет, а потом уже сам останавливается? Оказалось, что это зависит от версии тестконтейнеров. Я использовала самую новую, 1.20.2, а коллега более раннюю.

Чтобы в будущем другие разработчики не сталкивались с непредсказуемыми ошибками, задокументировала это поведение в спецификации к родительскому тестовому классу.
👍6
Код и дизайн

Как понять назначение системы?
Раньше думала, что надо внимательно изучить код, тогда вся система станет автоматически ясна.
Но оказалось, это неверно.
Нужно изучить именно дизайн, документацию. Дизайн - это как раз верхнеуровневое описание системы. Одному дизайну может соответствовать множество реализаций. Поэтому из кода и нельзя понять назначение системы. Но в идеале разработчику нужно стремиться к такой реализации, чтобы ее невозможно было сопоставить ни с каким другим дизайном.

Для закрепления этого принципа решила переписать несколько классов из моего пет-проекта.
Для начала выбрала класс, который представляет собой генератор списка случайных координат. Эти координаты потом используются для построения маршрута на UI. Логика довольно простая - есть метод, который принимает начальную и конечную координату поездки, максимальную скорость и время, с каким интервалом генерируются координаты. Для получения списка точек нужно обратиться к внешнему API, преобразовать результат, а затем просто сохранять координаты в БД с заданным временным интервалом.

Уже на этапе формулирования этой простой логики возникли проблемы. Код был написан 2 года назад, и было сложно вспомнить, что он делает, т.к. метод представлял собой просто очень длинное полотно.

Но когда удалось вспомнить и сформулировать назначение кода, дело пошло быстрей.
Во-первых, оказалось, что логика обращения к внешнему API и преобразования полученного ответа очень тесно переплетены. Вынесла обращение к API в отдельный класс. Теперь в будущем без больших проблем можно заменить его на другой, если потребуется.

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

Циклы заменила на декларативные stream-ы, это сделало код еще более наглядным.

В итоге новый код был написан по заранее сформулированному дизайну, а не по наитию, как старый код.
При этом старалась, чтобы из реализации максимально точно можно было бы воспроизвести дизайн.
И старый, и новый код делают одно и то же, но новый оказался намного более понятным и читаемым.
Liquibase и тестовые данные

Приступив к задаче в новом микросервисе, столкнулась с необходимостью сгенерировать много тестовых данных. Можно было бы по примеру уже готовых тестов написать init-скрипт или создать нужные entity и сохранить их прямо в тесте.
Но нашлось более универсальное решение - настроить liquibase для тестовых сценариев.
У нас уже были тесты на тестконтейнерах, и они использовали liquibase, как и само приложение. Вначале показалось, что будет достаточно просто создать тестовый чейнджлог и миграции с тестовыми данными. Но возникли сложности - оказалось, что все таблицы создаются в схеме public, хотя в БД они живут в отдельной схеме. Это вызывало ошибки.
Помимо того, на проекте есть тесты как для postgresql c тестконтейнерами, так и тесты для h2. А в моей задаче добавились миграции с использованием системных таблиц postgresql. И конечно, при добавлении новых миграций тесты для h2 упали.
Разобралась как настроить liquibase для работы с кастомной схемой, добавила 2 тестовых чейнджлога - для h2 и для postgres, настроила профайлы для работы с разными БД. В результате все тестовые данные можно использовать для всех тестов, а добавлять их стало быстрее и проще.
👍1
В продолжение предыдущей заметки.

Написала юнит тесты к очередной задаче. Тестируемый метод A был приватным, поэтому в тестах пришлось вызывать публичный метод B, вызывающий А. Метод А делает выполняет некую логику и сохраняет результат в БД. Вроде бы все просто, надо дернуть В, и проверить что в БД оказались нужные данные. Но на деле оказалось, что метод B вызывает также методы C, D, E... которые требуют еще данные для собственных расчетов. Когда я замокала все побочные вызовы, код выглядел мягко говоря, не очень понятно.

Решила применить тот же подход, что в предыдущей заметке - сделать интеграционные тесты с testcontainers.
Настроила liquibase, добавила тестовые данные, замокала пару вызовов... Тесты прошли успешно и стали выглядеть гораздо чище. Но в консоли обнаружилось несколько ошибок.
Оказалось, что приложение использует несколько схем в БД, и одна регулярная таска, которая запускается при старте приложения, пытается получить данные таблиц из разных схем и падает. И хоть на работу теста это не влияет, но некрасиво оставлять такие грязные логи. К тому же, если в будущем тесты упадут, это может запутать разработчика, который будет фиксить багу.

Liquibase для работы с кастомными схемами уже был настроен. Дальше разобралась, как настроить несколько датасорсов для работы с разными схемами. Теперь тесты стали полностью эмулировать работу приложения в слое обращения к БД. И можно легко создавать более сложные интеграционные тестовые сценарии.
🔥4