Вторая статья из цикла "Test-Driven Development приложений на Spring Boot" и в этот раз я буду говорить про тестирование доступа к базе данных, важного аспекта интеграционного тестирования. Я расскажу как через тесты определять интерфейс будущего сервиса для доступа к данным, как использовать встраиваемые in-memory базы для тестирования, работать с транзакциями и загружать тестовые данные в базу.
Начну, как и в прошлый раз, с небольшой теоретической части, и перейду к end-to-end тесту.
Пирамида тестирования
Для начала, маленькое, но необходимое, описание такой важной сущности в тестировании, как The Test Pyramid или пирамида тестирования.
Пирамидой тестирования называется подход, когда тесты организуются в несколько уровней.
- UI (или end-to-end, E2E) тестов мало и они медленные, но тестируют реальное приложение — никаких моков и тестовых двойников. На этом уровне часто мыслит бизнес и здесь обитают все BDD фреймворки (см. Cucumber в предыдущей статьей).
- За ними идут интеграционные тесты (сервисные, компонентные — терминология у каждого своя), которые уже фокусируются на конкретном компоненте (сервисе) системы, изолируя его от остальных компонентов через моки / двойники, но по прежнему проверяющие интеграцию с реальными внешними системами — эти тесты подключаются к базе, посылают REST запросы, работаю с очередью сообщений. По-сути, это тесты которые проверяют интеграцию бизнес логики с внешним миром.
- В самом низу находятся быстрые юнит-тесты, которые тестируют минимальные блоки кода (классы, методы) в полной изоляции.
Spring помогает с написанием тестов для каждого уровня — даже для unit-тестов, хотя это может звучать странно, ведь в мире юнит-тестов никакого знания про фреймворк вообще существовать не должно. После написания E2E теста я как раз покажу, как Spring позволяет даже такие чисто "интеграционные" вещи, как контроллеры, тестировать в изоляции.
Но начну я с самой вершины пирамиды — медленного UI теста, которые стартует и тестирует полноценное приложение.
End-to-end test
Итак, новая фича:
Feature: A list of available cakes
Background: catalogue is updated
Given the following items are promoted
| Title | Price |
| Red Velvet | 3.95 |
| Victoria Sponge | 5.50 |
Scenario: a user visiting the web-site sees the list of items
Given a new user, Alice
When she visits Cake Factory web-site
Then she sees that "Red Velvet" is available with price £3.95
And she sees that "Victoria Sponge" is available with price £5.50
И здесь сразу интересный аспект — что делать с предыдущим тестом, про приветствие на главной странице? Он вроде уже не актуален, после запуска сайта на главной уже будет каталог, а не приветствие. Здесь нет однозначного ответа, я бы сказал — зависит от ситуации. Но главный совет — не привязывайтесь к тестам! Удаляйте, когда они теряют актуальность, переписывайте, чтобы было проще читать. Особенно E2E тесты — это должна быть, по-сути, живая и актуальная спецификация. В моем случае, я просто удалил старый тесты, и заменил их новыми, используя некоторые предыдущие шаги и добавив несуществующие.
Теперь же я подошел к важному моменту — выбор технологии для хранения данных. В соответствии с lean подходом, я бы хотел отложить выбор до самого последнего момента — когда я точно будут знать, реляционная модель или нет, какие требование к консистентности, транзакционности. В общем случае, для этого есть решения — например, создание тестовых двойников и различных in-memory хранилищ, но пока я не хочу усложнять статью и сразу выберу технологию — реляционные базы данных. Но чтобы сохранить хоть какую-то возможность выбора БД, я добавлю абстракцию — Spring Data JPA. JPA сама по себе достаточно абстрактная спецификация для доступа к реляционным базам, а Spring Data делает её использование еще проще.
Spring Data JPA по умолчанию использует Hibernate в качестве провайдера, но поддерживает и другие технологии, например EclipseLink и MyBatis. Для людей не очень знакомых с Java Persistence API — JPA это как бы интерфейс, а Hibernate класс, его реализующий.
Итак, чтобы добавить поддержку JPA я добавил пару зависимостей:
implementation('org.springframework.boot:spring-boot-starter-data-jpa')
runtime('com.h2database:h2')
В качестве базы данных я буду использовать H2 — встраиваемую базу данных, написанную на Java, с возможностью работать в in-memory режиме.
Используя Spring Data JPA я сразу определяю интерфейс для доступа к данным:
interface CakeRepository extends CrudRepository<CakeEntity, String> { }
И сущность:
@Entity
@Builder
@AllArgsConstructor
@Table(name = "cakes")
class CakeEntity {
public CakeEntity() {
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@NotBlank
String title;
@Positive
BigDecimal price;
@NotBlank
@NaturalId
String sku;
boolean promoted;
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
CakeEntity cakeEntity = (CakeEntity) o;
return Objects.equals(title, cakeEntity.title);
}
@Override
public int hashCode() {
return Objects.hash(title);
}
}
В описании сущности есть пара не самых очевидных вещей.
@NaturalId
для поля sku
. Это поле используется как “натуральный идентификатор” для проверки равенства сущностей — использование всех полей или @Id
поля в equals
/ hashCode
методах это, скорее, анти-паттерн. О том, как правильно проверять равенство сущностей хорошо написано, например, тут.
- Чтобы хоть немного снизить количество boilerplate кода, я использую Project Lombok — annotation processor для Java. Он позволяет добавлять разные полезные вещи, вроде
@Builder
— чтобы автоматически генерить билдер для класса и @AllArgsConstructor
чтобы создать конструктор для всех полей.
Реализация интерфейса будет предоставлена автоматически Spring Data.
Вниз по пирамиде
Теперь время спуститься на следующий уровень пирамиды. В качестве эмпирического правила, я бы рекомендовал всегда начинать с e2e теста, потому, что это позволит определить "конечную цель" и границы новой фичи, но дальше строгих правил нет. Не обязательно писать сначала интеграционный тест, перед тем, как перейти на юнит-уровень. Просто чаще всего получается, что так удобнее и проще — и вполне естественно спускаться "вниз".
Но конкретно сейчас, я бы хотел сразу нарушить это правило и написать юнит-тест, который поможет определить интерфейс и контракт нового компонента, который пока не существует. Контроллер должен вернуть модель, которую заполнит из некоего компонента X, и я написал такой тест:
@ExtendWith(MockitoExtension.class)
class IndexControllerTest {
@Mock
CakeFinder cakeFinder;
@InjectMocks
IndexController indexController;
private Set<Cake> cakes = Set.of(new Cake("Test 1", "£10"),
new Cake("Test 2", "£10"));
@BeforeEach
void setUp() {
when(cakeFinder.findPromotedCakes()).thenReturn(cakes);
}
@Test
void shouldReturnAListOfFoundPromotedCakes() {
ModelAndView index = indexController.index();
assertThat(index.getModel()).extracting("cakes").contains(cakes);
}
}
Это чистый юнит-тест — никаких контекстов, никаких баз данных тут нет, только Mockito для моков. И этот тест как раз хорошая демонстрация, как Spring помогает юнит тестам — контроллер в Spring MVC это просто класс, методы которого принимают параметры обычных типов и возвращают POJO объекты — View Models. Нет ни HTTP запросов, ни ответов, хедеров, JSON, XML — все это будет автоматически применено ниже по стеку, в виде конвертеров и сериализаторов. Да, есть небольшой "намек" на Spring в виде ModelAndView
, но это обычный POJO и даже от него при желании можно избавиться, он нужен именно для UI контроллеров.
Я не буду много говорить про Mockito, можно все прочитать в официальной документации. Конкретно в этом тесте есть только интересных моментов — я использую MockitoExtension.class
в качестве исполнителя тестов, и он автоматически сгенерит моки для полей, аннотированных @Mock
и потом заинжектит эти моки, как зависимости в конструктор для объекта в поле помеченном @InjectMocks
. Можно все это сделать вручную, используя Mockito.mock()
метод и потом создав класс.
И этот тест помогает определить метод нового компонента — findPromotedCakes
, список тортов, которые мы хотим показать на главной странице. Он не определяет, что это такое, или как это должно работать с базой. Единственная ответственность контроллера — взять то, что ему передали, и вернуть в определенном поле модели ("cakes"). Но тем не менее, в моем интерфейсе CakeFinder
уже есть первый метод, а значит можно писать для него интеграционный тест.
Я сознательно сделал все классы внутри пакета cakes
package private, чтобы никто, за пределами пакета не смог их использовать. Единственный способ получить данные из базы — это интерфейс CakeFinder, который и есть мой “компонент Х” для доступа к базе. Он становится естественным “коннектором”, который я дальше могу легко замокать, если мне нужно будет тестировать что-то в изоляции и не трогать базу. А его единственная реализация — это JpaCakeFinder. И если, например, в будущем тип базы или источник данных поменяется — то нужно будет добавить реализацию интерфейса CakeFinder
, не меняя код, его использующий.
Интеграционный тест для JPA используя @DataJpaTest
Интеграционные тесты — это хлеб и масло Spring. В нем, вообщем-то, все так здорово сделано для интеграционного тестирования, что разработчики иногда не хотят уходить на юнит-уровень или пренебрегают UI уровнем. Это не плохо и не хорошо — повторюсь, что главная цель тестов — это уверенность. И набора быстрых и эффективных интеграционных тестов может быть достаточно, чтобы эту уверенность предоставить. Однако есть опасность, что эти тесты со временем либо будут медленнее и медленнее, либо просто начнут тестировать компоненты в изоляции, вместо интеграции.
Интеграционные тесты могут запустить приложение, как есть (@SpringBootTest
), либо его отдельный компонент (JPA, Web). В моем случае, я хочу написать сфокусированный тест для JPA — поэтому мне нет необходимости конфигурировать контроллеры или любые другие компоненты. За это в Spring Boot Test отвечает аннотация @DataJpaTest
. Это мета-аннотация, т.е. она комбинирует сразу несколько разных аннотаций, конфигурирующих разные аспекты теста.
- @AutoConfigureDataJpa
- @AutoConfigureTestDatabase
- @AutoConfigureCache
- @AutoConfigureTestEntityManager
- @Transactional
Сначала расскажу про каждый в отдельности, а потом покажу готовый тест.
@AutoConfigureDataJpa
Загружает целый набор конфигураций и настраивает — репозитории (автоматическая генерация реализаций для CrudRepositories
), инструменты миграции базы FlyWay и Liquibase, подключение к БД используя DataSource, менеджер транзакций, и, наконец, Hibernate. По-сути, это просто набор конфигураций, актуальных для доступа к данным — сюда не включены ни DispatcherServlet
из Web MVC, ни другие компоненты.
@AutoConfigureTestDatabase
Это один из самых интересных аспектов JPA теста. Эта конфигурация ищет в classpath одну из поддерживаемых embedded баз данных и переконфигурирует контекст, чтобы DataSource указывал на случайно созданную in-memory базу. Так как я добавил зависимость на H2 базу — то больше делать ничего не нужно, просто наличие этой аннотации автоматически для каждого запуска теста предоставит пустую базу, и это просто невероятно удобно.
Стоит помнить, что эта база будет полностью пустой, без схемы. Чтобы сгенерить схему, есть пара вариантов.
- Использовать фичу Auto DDL из Hibernate. Spring Boot Test автоматически поставит это значение в
create-drop
, чтобы Hibernate генерировал схему из описание сущностей и удалял ее в конце сессии. Это невероятно мощная фича Hibernate, которая очень полезна для тестов.
- Использовать миграции созданные Flyway или Liquibase.
Подробнее про разные подходы к инициализации базы можно прочитать в документации.
@AutoConfigureCache
Просто конфигурирует кэш на использование NoOpCacheManager — т.е. не кешировать ничего. Это полезно, чтобы избежать сюрпризов в тестах.
@AutoConfigureTestEntityManager
Добавляет в контекст специальный объект TestEntityManager
, который сам по себе интересный зверь. EntityManager
это главный класс JPA, который отвечает за добавление сущностей в сессию, удаление и подобными вещами. Только вот когда, например, в работу вступает Hibernate — добавление сущности в сессию не значит, что будет выполнен запрос в базу, а загрузка из сессии не означает, что будет выполнен select запрос. За счет внутренних механизмов Hibernate реальные операции с базой будут выполнятся в подходящий момент, который определит сам фреймворк. Но в тестах может быть необходимость принудительно послать что-то в базу, ведь цель тестов как раз тестировать интеграцию. И TestEntityManager
это просто хелпер, который поможет некоторые операции с базой принудительно выполнится — например, persistAndFlush()
заставит Hibernate выполнить все запросы.
@Transactional
Эта аннотация делает все тесты в классе транзакционными, с автоматическим откатом транзакции по завершению теста. Это просто механизм “очистки” базы перед каждым тестом, ведь иначе пришлось бы вручную удалять данные из каждой таблицы.
Должен ли тест управлять транзакцией — это не такой простой и очевидный вопрос, как может показаться. Не смотря на удобство “чистого” состояния базы, наличие @Transactional
в тестах может стать неприятным сюрпризом если “боевой” код не начинает транзакцию сам, а требует существующую. Это может привести к тому, что интеграционный тест пройдет, но при выполнении реального кода из контроллера, а не из теста, в сервисе не будет активной транзакции и метод бросит исключение. Хотя это и выглядит опасно, при наличии высокоуровневых тестов UI тестов, транзакционность тестов не так страшна. На моем опыте я видел только однажды, когда при проходящем интеграционном тесте падал продакшен код, который явно требовал наличие существующей транзакции. Но если все же нужно проверять, что сервисы и компоненты сами правильно управляют транзакциями, можно “перекрыть” аннотацию @Transactional
на тесте с нужным режимом (например, не начинать транзакцию).
Интеграционный тест со @SpringBootTest
Еще хочу отметить, что @DataJpaTest
это не уникальный пример фокусного интеграционного теста, еще есть @WebMvcTest
, @DataMongoTest
и много других. Но одной из самых важных тестовых аннотации остается @SpringBootTest
, которая запускает для тестов приложение “как есть” — со всеми настроенными компонентами и интеграциями. Возникает логичный вопрос — если можно запустить приложение целиком, зачем делать фокусные DataJpa тесты, например? Я бы сказал, что строгих правил тут снова нет.
Если возможно запускать приложения каждый раз, изолировать падения в тестах, не перегружать и не переусложнять Setup теста — то конечно можно и нужно использовать @SpringBootTest.
Однако в реальной жизни, приложения могут требовать много разных настроек, подключатся к разным системам, а я бы не хотел, чтобы мои тесты доступа к БД падали, т.к. не настроено подключение к очереди сообщений. Поэтому важно использовать здравый смысл, и если для того, чтобы заставить тест с @SpringBootTest аннотацией работать нужно замокать половину системы — то есть ли смысл тогда вообще в @SpringBootTest?
Подготовка данных для теста
Один из ключевых моментов для тестов, это подготовка данных. Каждый тест должен выполнятся в изоляции, и подготавливать окружение перед запуском, приводя систему в исходное желаемое состояние. Самый простой вариант это сделать — использовать @BeforeEach
/ @BeforeAll
аннотации и добавлять записи в базу там, используя репозиторий, EntityManager
или TestEntityManager
. Но есть еще один вариант, который позволяет запустить подготовленный скрипт или выполнить нужный SQL-запрос, это аннотация @Sql
. Spring Boot Test перед выполнением теста автоматически запустит указанный скрипт, избавив от необходимости добавлять @BeforeAll
блок, а об очистке данных позаботиться @Transactional
.
@DataJpaTest
class JpaCakeFinderTest {
private static final String PROMOTED_CAKE = "Red Velvet";
private static final String NON_PROMOTED_CAKE = "Victoria Sponge";
private CakeFinder finder;
@Autowired
CakeRepository cakeRepository;
@Autowired
TestEntityManager testEntityManager;
@BeforeEach
void setUp() {
this.testEntityManager.persistAndFlush(CakeEntity.builder().title(PROMOTED_CAKE)
.sku("SKU1").price(BigDecimal.TEN).promoted(true).build());
this.testEntityManager.persistAndFlush(CakeEntity.builder().sku("SKU2")
.title(NON_PROMOTED_CAKE).price(BigDecimal.ONE).promoted(false).build());
finder = new JpaCakeFinder(cakeRepository);
}
...
}
Red-green-refactor цикл
Не смотря на такое количество текста, для разработчика тест все еще выглядит как простой класс с аннотацией @DataJpaTest, но надеюсь, что я смог показать, как много полезного происходит под капотом, о чем разработчику можно не думать. Теперь можно перейти к TDD циклу и в этот раз я покажу пару итераций TDD, с примерами рефакторинга и минимального кода. Чтобы было понятнее, я крайне советую посмотреть историю в Git, там каждый коммит это отдельный и значимый шаг с описанием что и как он делает.
Подготовка данных
Я использую подход с @BeforeAll
/ @BeforeEach
и вручную создаю, все записи в базе. Пример с @Sql
аннотацией вынесен в отдельный класс JpaCakeFinderTestWithScriptSetup
, он дублирует тесты, чего быть, разумеется, не должно, и существует с единственной целью продемонстрировать подход.
Исходное состояние системы — есть две записи в системе, один торт участвует в промоушене и должен быть включен в результат, возвращенный методом, второй — нет.
Первый тест интеграционный тест
Первый тест самый простой — findPromotedCakes
должен включать описание и цену торта, участвующего в промоушене.
Red
@Test
void shouldReturnPromotedCakes() {
Iterable<Cake> promotedCakes = finder.findPromotedCakes();
assertThat(promotedCakes).extracting(Cake::getTitle).contains(PROMOTED_CAKE);
assertThat(promotedCakes).extracting(Cake::getPrice).contains("£10.00");
}
Тест, разумеется, падает — дефолтная реализация возвращает пустой Set.
Green
Естественным желаем будет сразу писать фильтрацию, делать запрос в базу с where
и так далее. Но следуя практике TDD, я должен написать минимальный код чтобы тест прошел. И этот минимальный код — вернуть все записи в базе. Да, так просто и банально.
public Set<Cake> findPromotedCakes() {
Spliterator<CakeEntity> cakes = this.cakeRepository.findAll()
.spliterator();
return StreamSupport.stream(cakes, false).map(
cakeEntity -> new Cake(cakeEntity.title, formatPrice(cakeEntity.price)))
.collect(Collectors.toSet());
}
private String formatPrice(BigDecimal price) {
return "£" + price.setScale(2, RoundingMode.DOWN).toPlainString();
}
Наверное кое-кто возразит, что тут можно сделать тест зеленым даже без базы — просто захардкодить результат, ожидаемый тестом. Я периодически слышу такой аргумент, но думаю, все понимают, что TDD это не догма и не религия, нет смысла доводить это до абсурда. Но если уж очень хочется — то можно, например, рандомизировать данные на установке, чтобы их было не захардкодить.
Refactor
Я здесь не вижу особого рефакторинга, поэтому для этого конкретного теста эту фазу можно пропустить. Но я бы все равно не рекомендовал игнорировать эту фазу, лучше каждый раз в “зеленом” состоянии системы остановиться и подумать — а можно ли что-то порефакторить чтобы сделать лучше и проще?
Второй тест
А вот второй тест уже проверит, что не promoted торт не попадет в результат, возвращаемый findPromotedCakes
.
@Test
void shouldNotReturnNonPromotedCakes() {
Iterable<Cake> promotedCakes = finder.findPromotedCakes();
assertThat(promotedCakes).extracting(Cake::getTitle)
.doesNotContain(NON_PROMOTED_CAKE);
}
Red
Тест, ожидаемо, падает — в базе две записи и код просто возвращает их всех.
Green
И снова можно задуматься — а какой минимальный код можно написать, чтобы тест прошел? Раз уже есть stream и его сборка — можно просто добавить туда filter
блок.
public Set<Cake> findPromotedCakes() {
Spliterator<CakeEntity> cakes = this.cakeRepository.findAll()
.spliterator();
return StreamSupport.stream(cakes, false)
.filter(cakeEntity -> cakeEntity.promoted)
.map(cakeEntity -> new Cake(cakeEntity.title, formatPrice(cakeEntity.price)))
.collect(Collectors.toSet());
}
Перезапускаем тесты — интеграционные тесты теперь зеленые. Настал важный момент — за счет комбинации юнит-теста контроллера и интеграционного теста для работы с БД моя фича готова — и UI тест теперь проходит!
Refactor
И раз все тесты зеленые — настало время рефакторинга. Думаю, не нужно пояснять, что фильтрация в памяти — не лучшая идея, лучше это делать в базе. Чтобы это сделать, я добавил новый метод в CakesRepository
— findByPromotedIsTrue
:
interface CakeRepository extends CrudRepository<CakeEntity, String> {
Iterable<CakeEntity> findByPromotedIsTrue();
}
Для этого метода Spring Data автоматически сгенерил метод, который выполнит запрос вида select from cakes where promoted = true
. Подробнее про генерацию запросов можно почитать в документации к Spring Data.
public Set<Cake> findPromotedCakes() {
Spliterator<CakeEntity> cakes = this.cakeRepository.findByPromotedIsTrue()
.spliterator();
return StreamSupport.stream(cakes, false).map(
cakeEntity -> new Cake(cakeEntity.title, formatPrice(cakeEntity.price)))
.collect(Collectors.toSet());
}
Это хороший пример, какую гибкость дает интеграционное тестирование и подход “черного ящика”. Если бы репозиторий был замокан, то добавить туда новый метод не меняя тесты было не невозможно.
Подключение к production базе
Чтобы добавить немного “реалистичности” и показать, как можно разделять конфигурацию для тестов и основного приложения, я добавлю конфигурацию доступа к данным для “продакшен” приложения.
Добавляется все традиционно секцией в application.yml
:
datasource:
url: jdbc:h2:./data/cake-factory
Это автоматически сохранит данные в файловой системе в папке ./data
. Замечу, что в тестах этой папки создано не будет — @DataJpaTest
автоматически заменит подключение к файловой базе на случайную базу в памяти благодаря наличию @AutoConfigureTestDatabase
аннотации.
Две полезные вещи, которые могут пригодится — это файлы data.sql
и schema.sql
. При запуске приложения, Spring Boot проверит наличие этих файлов в ресурсах и выполнит эти скрипты при их наличии. Эта фича может быть полезной при локальной разработке и прототипировании, в реальных базах, разумеется, нужно использовать инструменты миграции.
Заключение
Итак, в этот раз я показал как через тесты определить интерфейс сервисе для доступа к данным, как написать интеграционный тест, и как писать минимальный код в TDD цикле.
В следующей статье я добавлю Spring Security — покажу как тестировать приложение для разных пользователей и ролей и какие инструменты для этого предоставляет Spring, а так же как определять границы теста.
Источник: https://habr.com/post/433958/?utm_source=habrahabr&utm_medium=rss&utm_campaign=433958
Смотри также популярное:
Зачем нужна Java. http://fetisovvs.blogspot.com/2014/07/java.html
Когда Java наконец помрёт, что с этим делать и что будет с JPoint. https://fetisovvs.blogspot.com/2018/11/java-jpoint-java.html
Разбор основных концепций параллелизма. http://fetisovvs.blogspot.com/2018/04/java.html
Первый контакт с «var» в Java 10. http://fetisovvs.blogspot.com/2018/01/var-java-10-java.html
JAVA 9. Что нового? http://fetisovvs.blogspot.com/2017/10/java-9-java.html
Руководство по Java 9 для тех, кому приходится работать с legacy-кодом. http://fetisovvs.blogspot.com/2018/08/java-9-legacy-java.html
Концепции объектно-ориентированного программирования — ООП в Java. http://fetisovvs.blogspot.com/2017/01/java-java.html
Анимации в Android по полочкам (Часть 1. Базовые анимации). http://fetisovvs.blogspot.com/2018/02/android-1-java.html
Двести пятьдесят русскоязычных обучающих видео докладов и лекций о Java. http://fetisovvs.blogspot.com/2015/12/java-5-java-java.html
Абстрактные классы и методы. http://fetisovvs.blogspot.com/2017/02/java.html
Полное руководство по Java Reflection API. Рефлексия на примерах. http://fetisovvs.blogspot.com/2017/02/java-reflection-api-java.html
Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. http://fetisovvs.blogspot.com/2017/10/java-java.html
Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. (Часть 3). http://fetisovvs.blogspot.com/2017/10/java-3-java.html
ТОП-3 способа конвертировать массив в ArrayList. Пример на Java. http://fetisovvs.blogspot.com/2016/09/3-arraylist-java-java.html
Ввод–вывод в Java. http://fetisovvs.blogspot.com/2016/05/java-java_28.html
Java Challengers #2: Сравнение строк. https://fetisovvs.blogspot.com/2018/11/java-challengers-2-java.html
Enum-Всемогущий. http://fetisovvs.blogspot.com/2017/02/enum-java.html
Массивы в Java. Создание и обработка. http://fetisovvs.blogspot.com/2017/10/java-java_18.html
Arrays, Collections: Алгоритмический минимум. http://fetisovvs.blogspot.com/2017/12/arrays-collections.html
Популярные методы для работы с Java массивами. http://fetisovvs.blogspot.com/2016/09/java-java_29.html
Пример использования метода replace в Java. Как заменить символ в строке? http://fetisovvs.blogspot.com/2017/01/replace-java-java.html
Класс Scanner в Java — описание и пример использования. http://fetisovvs.blogspot.com/2017/01/scanner-java-java.html
Пример использования метода trim в Java: как удалить пробелы в начале и конце строки? http://fetisovvs.blogspot.com/2017/01/trim-java-java.html
Spark — Потрясающий веб-микрофреймворк для Java. http://fetisovvs.blogspot.com/2017/10/spark-java-java.html
Чтение и запись CSV файла с помощью SuperCSV. http://fetisovvs.blogspot.com/2017/01/csv-supercsv-java-java.html
Конструкция try/catch/finally (исключения). http://fetisovvs.blogspot.com/2017/01/trycatchfinally-java.html
1000+ часов видео по Java на русском. http://fetisovvs.blogspot.nl/2017/06/1000-java-java.html
Раздача халявы: нетормозящие треды в Java. Project Loom. http://fetisovvs.blogspot.com/2018/09/java-project-loom-java.html
Шпаргалка Java программиста 7.1 Типовые задачи: Оптимальный путь преобразования InputStream в строку. http://fetisovvs.blogspot.com/2016/04/java-71-inputstream-java.html
Шпаргалки Java программиста 10: Lombok. http://fetisovvs.blogspot.nl/2017/12/java-10-lombok-java.html
Шпаргалки Java программиста 9: Java SE — Шпаргалка для собеседований и повторений. http://fetisovvs.blogspot.com/2017/12/java-9-java-se-java.html
Шпаргалка Java программиста 8. Библиотеки для работы с Json (Gson, Fastjson,
LoganSquare, Jackson, JsonPath и другие). http://fetisovvs.blogspot.com/2016/04/java-8-json-gson-fastjson-logansquare.html
Java 8 и паттерн Стратегия. http://fetisovvs.blogspot.com/2018/03/java-8-java.html
Java EE Concurency API. http://fetisovvs.blogspot.com/2018/08/java-ee-concurency-api-java.html
Реализация ООП-наследования в классах, работающих с SQL и MS Entity Framework. http://fetisovvs.blogspot.com/2017/02/sql-ms-entity-framework.html
Как установить соединение с СУБД MySQL в IntelliJ IDEA в редакции Community. http://fetisovvs.blogspot.com/2016/04/mysql-intellij-idea-community-java.html
Максимально простой в поддержке способ интеграции java-клиента с java-сервером. http://fetisovvs.blogspot.com/2018/09/java-java-java.html
Как с помощью maven работать с библиотеками, которых в maven нет. http://fetisovvs.blogspot.com/2017/03/maven-maven-java.html
Проекты по созданию компиляторов из Java в JavaScript и исполняемые файлы. http://fetisovvs.blogspot.com/2018/01/java-javascript-java.html
Реактивное программирование с JAX-RS. http://fetisovvs.blogspot.com/2018/09/jax-rs-java.html
Компактные строки в Java 9. https://fetisovvs.blogspot.com/2018/10/java-9-java.html
Абстрактный CRUD от репозитория до контроллера: что ещё можно сделать при помощи Spring + Generics. http://fetisovvs.blogspot.com/2018/09/crud-spring-generics-java.html
Диагностика утечек памяти в Java. http://fetisovvs.blogspot.com/2017/03/java-java_18.html
Spring AOP и JavaConfig в плагинах для Atlassian Jira. http://fetisovvs.blogspot.com/2018/04/spring-aop-javaconfig-atlassian-jira.html
Блеск и нищета Java для настольных систем. http://fetisovvs.blogspot.com/2018/04/java-haulmont-java.html
Разбор задачек от Одноклассников на JPoint 2018. http://fetisovvs.blogspot.com/2018/04/jpoint-2018-java.html
Программируем… выход из лабиринта. http://fetisovvs.blogspot.com/2015/10/java.html
Основы работы с IntelliJ IDEA. Интерфейс программы. http://fetisovvs.blogspot.com/2016/09/intellij-idea-java.html
Ускоряем время сборки и доставки java web приложения. http://fetisovvs.blogspot.com/2018/03/java-web-java.html
Открытый урок Java Enterprise «CDI in action». http://fetisovvs.blogspot.com/2018/09/java-enterprise-cdi-in-action-java.html
«Мы все стремимся к сложности, а потом с ней боремся»: интервью с Венкатом Субраманиамом. http://fetisovvs.blogspot.com/2018/09/java_16.html