JavaRush /Java блог /Random UA /Додаємо все, що пов'язане із БД. (Частина 2) - "Java-прое...
Roman Beekeeper
35 рівень

Додаємо все, що пов'язане із БД. (Частина 2) - "Java-проект від А до Я"

Стаття з групи Random UA
Всім привіт. Нагадаю: у першій частині ми додавали Flyway. Продовжимо.

Додавання бази даних до docker-compose.yml

Наступний етап – налаштування роботи з базою даних у основного docker-compose.yml. Додамо БД у docker-compose файл:
version: '3.1'

services:
 jrtb-bot:
   depends_on:
     - jrtb-db
   build:
     context: .
   environment:
     BOT_NAME: ${BOT_NAME}
     BOT_TOKEN: ${BOT_TOKEN}
     BOT_DB_USERNAME: ${BOT_DB_USERNAME}
     BOT_DB_PASSWORD: ${BOT_DB_PASSWORD}
   restart: always
 jrtb-db:
   image: mysql:5.7
   restart: always
   environment:
     MYSQL_USER: ${BOT_DB_USERNAME}
     MYSQL_PASSWORD: ${BOT_DB_PASSWORD}
     MYSQL_DATABASE: 'jrtb_db'
     MYSQL_ROOT_PASSWORD: 'root'
   ports:
     - '3306:3306'
   expose:
     - '3306'
Я додав ще такий рядок у наш додаток:
depends_on:
 - jrtb-db
Це означає, що перед запуском програми ми очікуємо, що запуститься база даних. Далі можна помітити додавання ще двох змінних, які нам потрібні для роботи з БД:
${BOT_DB_USERNAME}
${BOT_DB_PASSWORD}
Їх ми отримаємо в docker-compose так само, як і для телеграм-бота через змінні оточення. Зробив я це для того, щоб ми мали лише одне місце, де ми виставляємо значення імені користувача БД та його пароль. Ми передаємо їх у докер образ нашого додатка та у докер контейнер нашої бази даних. Далі потрібно оновити Dockerfile, щоб навчити наш SpringBoot приймати змінні для бази даних.
FROM adoptopenjdk/openjdk11:ubi
ARG JAR_FILE=target/*.jar
ENV BOT_NAME=test.codegym_community_bot
ENV BOT_TOKEN=1375780501:AAE4A6Rz0BSnIGzeu896OjQnjzsMEG6_uso
ENV BOT_DB_USERNAME=jrtb_db_user
ENV BOT_DB_PASSWORD=jrtb_db_password
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Dspring.datasource.password=${BOT_DB_PASSWORD}", "-Dbot.username=${BOT_NAME}", "-Dbot.token=${BOT_TOKEN}", "-Dspring.datasource.username=${BOT_DB_USERNAME}", "-jar", "app.jar"]
Тепер додаємо змінні бази даних до Dockerfile:
ENV BOT_DB_USERNAME=jrtb_db_user
ENV BOT_DB_PASSWORD=jrtb_db_password
Значення змінних будуть інші. Ті, які ми передамо в Dockerfile, проте вимагають дати значення за замовчуванням, тому я і ввів якісь. Розширюємо останній рядок двома елементами, за допомогою яких ми передамо в запуск програми ім'я користувача ДБ та його пароль:
"-Dspring.datasource.password=${BOT_DB_PASSWORD}", "-Dbot.username=${BOT_NAME}"
Останній рядок у Dockerfile (який починається з ENTRYPOINT) повинен бути без перенесення елементів. Якщо зробити перенесення, цей код працювати не буде. Останній крок — оновити файл start.sh , щоб передавати змінні для бази даних.
#!/bin/bash

# Pull new changes
git pull

# Prepare Jar
mvn clean
mvn package

# Ensure, that docker-compose stopped
docker-compose stop

# Add environment variables
export BOT_NAME=$1
export BOT_TOKEN=$2
export BOT_DB_USERNAME='prod_jrtb_db_user'
export BOT_DB_PASSWORD='Pap9L9VVUkNYj99GCUCC3mJkb'

# Start new deployment
docker-compose up --build -d
Ми вже вміємо додавати змінні середовища, перш ніж запустити docker-compose. Для цього просто потрібно виконати export var_name=var_value. Тому додаємо всього два рядки:
export BOT_DB_USERNAME='prod_jrtb_db_user'
export BOT_DB_PASSWORD='Pap9L9VVUkNYj99GCUCC3mJkb'
Саме тут ми задаємо ім'я користувача БД та його пароль. Звичайно, можна було б ці змінні передавати при запуску баш скрипта, як ми це робимо для ім'я та токена бота. Але мені здається, що це зайве. Щоб реально отримати доступ до бази даних, потрібно знати IP-сервера, на якому буде розгорнута БД, і бути в списку дозволених IP-адреса для запиту. Як на мене, цього вже й так достатньо. Основу закладено: тепер можна займатися більш зрозумілими для розробника речами — писати код. До цього ми займалися тим, що роблять DevOps інженери – налаштовували оточення.

Додаємо Repository шар

Зазвичай у додатку є три шари:
  1. Контролери - точки входу до програми.
  2. Сервіси – місце роботи бізнес-логіки. Це вже частково є: SendMessageService — явний представник бізнес-логіки.
  3. Репозиторії – місце роботи з базою даних. У нашому випадку це телеграм-бот.
Ось зараз додаватимемо третій шар — репозиторії. Тут ми будемо використовувати проект із екосистеми Spring - Spring Data. Почитати про те, що це таке, можна в цій статті на Хабрі . Нам потрібно знати та розуміти кілька моментів:
  1. У нас не буде роботи з JDBC: ми працюватимемо одразу з вищими абстракціями. Тобто зберігати об'єкти POJO, які відповідають таблицям у базі даних. Такі класи ми називатимемо entity , оскільки вони називаються офіційно в Java Persistence API (це загальний набір інтерфейсів для БД через ORM, тобто абстракція над роботою з JDBC). У нас буде клас entity, який ми зберігатимемо в БД, і вони запишуться саме в таблицю, яка нам потрібна. Такі ж об'єкти ми отримуватимемо при пошуку в базі даних.
  2. Spring Data пропонує використовувати їх набір інтерфейсів: JpaRepository , CrudRepository , etc... Є й інші інтерфейси: повний перелік можна знайти тут . Принадність полягає в тому, що можна використовувати їх методи, не реалізовуючи їх(!). Більше того, є певний шаблон, за допомогою якого можна писати в інтерфейсі нові методи, і вони будуть реалізовані автоматично.
  3. Spring полегшує нашу розробку як може. Для цього нам потрібно створити свій інтерфейс і успадковуватись від вищеописаних. А щоб Spring знав, що йому потрібно використовувати цей інтерфейс, додати інструкцію Repository.
  4. Якщо нам потрібно буде написати метод для роботи з базою даних, якого немає, то це також не проблема — напишемо. Покажу, що та як там робити.
У рамках цієї статті ми проведемо роботу з додаванням по всьому шляху TelegramUser та покажемо на його прикладі цю частину. Решту вже розширюватимемо на інших завданнях. Тобто при виконанні команди /start ми будемо записувати до бази даних нашого користувача active = true. Це означатиме, що користувач користується роботом. Якщо користувач вже є у БД, оновлюватимемо поле active = true. При виконанні команди /stop ми не видалятимемо користувача, а тільки оновимо поле active на значення false, щоб якщо користувач знову захоче використовувати бота, він міг його запустити і продовжити з того моменту, де зупинився. А щоб при тестуванні було видно, що відбувається, ми створимо команду /stat: вона відображатиме кількість активних користувачів. Створюємо репозиторіюпакет поруч із пакетами bot, command, service. У цьому пакеті створюємо ще один entity . У пакеті entity створюємо клас TelegramUser:
package com.github.codegymcommunity.jrtb.repository.entity;

import lombok.Data;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

/**
* Telegram User entity.
*/
@Data
@Entity
@Table(name = "tg_user")
public class TelegramUser {

   @Id
   @Column(name = "chat_id")
   private String chatId;

   @Column(name = "active")
   private boolean active;
}
Тут видно, що у нас усі анотації з javax.persistence пакету. Це загальні інструкції, які використовуються для всіх реалізацій ORM. За замовчуванням Spring Data Jpa використовується Hibernate, хоча можна використовувати й інші реалізації. Ось перелік анотацій, які ми використовуємо:
  • Entity - говорить про те, що це сутність для роботи з БД;
  • Table – тут ми визначаємо ім'я таблиці;
  • Id - анотація говорить, яке поле буде Primary Key у таблиці;
  • Column - визначаємо ім'я поля таблиці.
Далі створюємо інтерфейс для роботи з базою даних. Зазвичай імена таких інтерфейсів пишуться за шаблоном — EntiryNameRepository. У нас буде TelegramuserRepository:
package com.github.codegymcommunity.jrtb.repository;

import com.github.codegymcommunity.jrtb.repository.entity.TelegramUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
* {@link Repository} for handling with {@link TelegramUser} entity.
*/
@Repository
public interface TelegramUserRepository extends JpaRepository<TelegramUser, String> {
   List<TelegramUser> findAllByActiveTrue();
}
Тут видно, як я додав метод findAllByActiveTrue() , який ніде не реалізую. Але це не завадить йому працювати. Spring Data зрозуміє, що потрібно отримати всі записи з таблиці tg_user, які мають поле active = true . Додаємо сервіс по роботі з сутністю TelegramUser (застосовуємо dependency inversion із SOLID у контексті того, що сервіси інших сутностей не можуть безпосередньо спілкуватися з репозиторієм іншої сутності – лише через сервіс тієї сутності). Створюємо в пакеті service TelegramUserService, в якому поки що буде кілька методів: зберегти користувача, отримати користувача за його ID та відобразити список активних користувачів. Спершу створюємо інтерфейс TelegramUserService:
package com.github.codegymcommunity.jrtb.service;

import com.github.codegymcommunity.jrtb.repository.entity.TelegramUser;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

/**
* {@link Service} for handling {@link TelegramUser} entity.
*/
public interface TelegramUserService {

   /**
    * Save provided {@link TelegramUser} entity.
    *
    * @param  telegramUser provided telegram user.
    */
   void save(TelegramUser telegramUser);

   /**
    * Retrieve all active {@link TelegramUser}.
    *
    * @return the collection of the active {@link TelegramUser} objects.
    */
   List<TelegramUser> retrieveAllActiveUsers();

   /**
    * Find {@link TelegramUser} by chatId.
    *
    * @param chatId provided Chat ID
    * @return {@link TelegramUser} with provided chat ID or null otherwise.
    */
   Optional<TelegramUser> findByChatId(String chatId);
}
І, власне, реалізація TelegramUserServiceImpl:
package com.github.codegymcommunity.jrtb.service;

import com.github.codegymcommunity.jrtb.repository.TelegramUserRepository;
import com.github.codegymcommunity.jrtb.repository.entity.TelegramUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

/**
* Implementation of {@link TelegramUserService}.
*/
@Service
public class TelegramUserServiceImpl implements TelegramUserService {

   private final TelegramUserRepository telegramUserRepository;

   @Autowired
   public TelegramUserServiceImpl(TelegramUserRepository telegramUserRepository) {
       this.telegramUserRepository = telegramUserRepository;
   }

   @Override
   public void save(TelegramUser telegramUser) {
       telegramUserRepository.save(telegramUser);
   }

   @Override
   public List<TelegramUser> retrieveAllActiveUsers() {
       return telegramUserRepository.findAllByActiveTrue();
   }

   @Override
   public Optional<TelegramUser> findByChatId(String chatId) {
       return telegramUserRepository.findById(chatId);
   }
}
Тут слід зазначити, що ми використовуємо dependency injection (вводимо екземпляр класу) об'єкта TelegramuserRepository за допомогою інструкції Autowired , причому на конструкторі. Можна робити це і для змінної, але саме цей підхід рекомендує нам команда Spring Framework.

Додаємо статистику для бот

Далі потрібно оновити команди /start та /stop. Коли використовується команда /start, потрібно зберегти в базі даних нового користувача і поставити йому active = true. А коли буде / stop, оновити дані користувача: встановити active = false. Поправимо клас StartCommand :
package com.github.codegymcommunity.jrtb.command;

import com.github.codegymcommunity.jrtb.repository.entity.TelegramUser;
import com.github.codegymcommunity.jrtb.service.SendBotMessageService;
import com.github.codegymcommunity.jrtb.service.TelegramUserService;
import org.telegram.telegrambots.meta.api.objects.Update;

/**
* Start {@link Command}.
*/
public class StartCommand implements Command {

   private final SendBotMessageService sendBotMessageService;
   private final TelegramUserService telegramUserService;

   public final static String START_MESSAGE = "Привет. Я Javarush Telegram Bot. Я помогу тебе быть в курсе последних " +
           "статей тех авторов, котрые тебе интересны. Я еще маленький и только учусь.";

   public StartCommand(SendBotMessageService sendBotMessageService, TelegramUserService telegramUserService) {
       this.sendBotMessageService = sendBotMessageService;
       this.telegramUserService = telegramUserService;
   }

   @Override
   public void execute(Update update) {
       String chatId = update.getMessage().getChatId().toString();

       telegramUserService.findByChatId(chatId).ifPresentOrElse(
               user -> {
                   user.setActive(true);
                   telegramUserService.save(user);
               },
               () -> {
                   TelegramUser telegramUser = new TelegramUser();
                   telegramUser.setActive(true);
                   telegramUser.setChatId(chatId);
                   telegramUserService.save(telegramUser);
               });

       sendBotMessageService.sendMessage(chatId, START_MESSAGE);
   }
}
Тут ми передаємо в конструктор ще й об'єкт TelegramuserService, за допомогою якого зберігатимемо нового користувача. Далі, використовуючи принади Optional у джаві, працює наступна логіка: якщо користувач у базі у нас є, просто робимо його активним, якщо ні – створюємо нового активного. StopCommand:
package com.github.codegymcommunity.jrtb.command;

import com.github.codegymcommunity.jrtb.repository.entity.TelegramUser;
import com.github.codegymcommunity.jrtb.service.SendBotMessageService;
import com.github.codegymcommunity.jrtb.service.TelegramUserService;
import org.telegram.telegrambots.meta.api.objects.Update;

import java.util.Optional;

/**
* Stop {@link Command}.
*/
public class StopCommand implements Command {

   private final SendBotMessageService sendBotMessageService;
   private final TelegramUserService telegramUserService;

   public static final String STOP_MESSAGE = "Деактивировал все ваши подписки \uD83D\uDE1F.";

   public StopCommand(SendBotMessageService sendBotMessageService, TelegramUserService telegramUserService) {
       this.sendBotMessageService = sendBotMessageService;
       this.telegramUserService = telegramUserService;
   }

   @Override
   public void execute(Update update) {
       sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), STOP_MESSAGE);
       telegramUserService.findByChatId(update.getMessage().getChatId().toString())
               .ifPresent(it -> {
                   it.setActive(false);
                   telegramUserService.save(it);
               });
   }
}
У StopCommand так само передаємо TelegramServiceTest. Додаткова логіка така: якщо ми маємо користувач з таким chat ID, ми його деактивуємо, тобто ставимо active = false. Як це побачити на власні очі? Зробимо нову команду /stat, яка відображатиме статистику бота. На цьому етапі це буде проста статистика, доступна всім користувачам. Надалі ми її обмежимо та зробимо доступ лише для адміністраторів. У статистиці буде один запис: кількість активних користувачів робота. Для цього додаємо значення STAT("/stat") у CommandName. Далі створюємо StatCommand клас:
package com.github.codegymcommunity.jrtb.command;

import com.github.codegymcommunity.jrtb.service.SendBotMessageService;
import com.github.codegymcommunity.jrtb.service.TelegramUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.telegram.telegrambots.meta.api.objects.Update;

/**
* Statistics {@link Command}.
*/
public class StatCommand implements Command {

   private final TelegramUserService telegramUserService;
   private final SendBotMessageService sendBotMessageService;

   public final static String STAT_MESSAGE = "Javarush Telegram Bot использует %s человек.";

   @Autowired
   public StatCommand(SendBotMessageService sendBotMessageService, TelegramUserService telegramUserService) {
       this.sendBotMessageService = sendBotMessageService;
       this.telegramUserService = telegramUserService;
   }

   @Override
   public void execute(Update update) {
       int activeUserCount = telegramUserService.retrieveAllActiveUsers().size();
       sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), String.format(STAT_MESSAGE, activeUserCount));
   }
}
Тут все просто: ми отримуємо список всіх активних користувачів за допомогою методу retrieveAllActiveUsers і отримуємо розмір колекції. Також тепер потрібно оновити класи по висхідній: CommandContainer і Javarush TelegramBot , щоб вони навчабося передавати потрібний нам новий сервіс. CommandContainer:
package com.github.codegymcommunity.jrtb.command;

import com.github.codegymcommunity.jrtb.service.SendBotMessageService;
import com.github.codegymcommunity.jrtb.service.TelegramUserService;
import com.google.common.collect.ImmutableMap;

import static com.github.codegymcommunity.jrtb.command.CommandName.*;

/**
* Container of the {@link Command}s, which are using for handling telegram commands.
*/
public class CommandContainer {

   private final ImmutableMap<String, Command> commandMap;
   private final Command unknownCommand;

   public CommandContainer(SendBotMessageService sendBotMessageService, TelegramUserService telegramUserService) {

       commandMap = ImmutableMap.<String, Command>builder()
               .put(START.getCommandName(), new StartCommand(sendBotMessageService, telegramUserService))
               .put(STOP.getCommandName(), new StopCommand(sendBotMessageService, telegramUserService))
               .put(HELP.getCommandName(), new HelpCommand(sendBotMessageService))
               .put(NO.getCommandName(), new NoCommand(sendBotMessageService))
               .put(STAT.getCommandName(), new StatCommand(sendBotMessageService, telegramUserService))
               .build();

       unknownCommand = new UnknownCommand(sendBotMessageService);
   }

   public Command retrieveCommand(String commandIdentifier) {
       return commandMap.getOrDefault(commandIdentifier, unknownCommand);
   }

}
Тут ми додали до карти нову команду та передали через конструктор TelegramUserService. А ось у самому роботі зміниться тільки конструктор:
@Autowired
public JavarushTelegramBot(TelegramUserService telegramUserService) {
   this.commandContainer = new CommandContainer(new SendBotMessageServiceImpl(this), telegramUserService);
}
Тепер ми передаємо у вигляді аргументу TelegramUserService, додаючи інструкцію Autowired. Це означає, що ми її отримаємо з Application Context. Також оновимо клас HelpCommand , щоб у описі з'явилася ще й нова команда статистики.

Мануальне тестування

Запустимо базу даних з docker-compose-test.yml та main метод у класі JavarushTelegramBotApplication. Далі пишемо набір команд:
  • /stat - очікуємо, що з порожній базі даних людина, використовують цей бот, буде нуль;
  • /start - запускаємо бота;
  • /stat - тепер очікуємо, що робота буде використовувати 1 людина;
  • / stop - зупиняємо робота;
  • /stat - очікуємо, що знову буде 0 людей використовувати.
"Java-проект від А до Я": Додаємо все, що пов'язане з БД.  Частина 2 - 2Якщо у вас в результаті буде так само, можна сказати, що функціонал відпрацював правильно і справний бот. Якщо щось піде не так - не біда: перезапускаємо main метод у режимі дебага і проходимо чітко по всьому шляху, щоб знайти, в чому була помилка.

Пишемо та оновлюємо тести

Оскільки ми змінабо конструктори, потрібно буде оновити тестові класи. У класі AbstractCommandTest нам потрібно додати ще одне поле - замокний клас TelegramUserService , який потрібен для трьох команд:
protected TelegramUserService telegramUserService = Mockito.mock(TelegramUserService.class);
Далі в CommandContainer оновимо метод init() :
@BeforeEach
public void init() {
   SendBotMessageService sendBotMessageService = Mockito.mock(SendBotMessageService.class);
   TelegramUserService telegramUserService = Mockito.mock(TelegramUserService.class);
   commandContainer = new CommandContainer(sendBotMessageService, telegramUserService);
}
У StartCommand потрібно оновити getCommand() метод:
@Override
Command getCommand() {
   return new StartCommand(sendBotMessageService, telegramUserService);
}
Також і в StopCommand:
@Override
Command getCommand() {
   return new StopCommand(sendBotMessageService, telegramUserService);
}
Далі підемо новими тестами. Створюємо типовий тест для StatCommand :
package com.github.codegymcommunity.jrtb.command;

import static com.github.codegymcommunity.jrtb.command.CommandName.STAT;
import static com.github.codegymcommunity.jrtb.command.StatCommand.STAT_MESSAGE;

public class StatCommandTest extends AbstractCommandTest {
   @Override
   String getCommandName() {
       return STAT.getCommandName();
   }

   @Override
   String getCommandMessage() {
       return String.format(STAT_MESSAGE, 0);
   }

   @Override
   Command getCommand() {
       return new StatCommand(sendBotMessageService, telegramUserService);
   }
}
Це із простого. Тепер поговоримо про те, як ми тестуватимемо роботу з базою даних. Все, що ми робабо раніше, це були юніт-тести. В інтеграційному тесті перевіряється інтеграція між кількома частинами програми. Наприклад, програми та бази даних. Тут все буде складніше, тому що для тестування нам потрібна база даних. Тому коли ми локально проганятимемо наші тести, у нас має бути запущена база даних з docker-compose-test.yml. Щоб прогнати цей тест, потрібно запустити все SpringBoot додаток. Для тестового класу є інструкція SpringBootTest, яка запустить програму. Але цей підхід нам не підійде, тому що при запуску програми запускатиметься і телеграм-бот. Але тут є протиріччя. Тести запускатимуться як локально у нас на машині, так і публічно, через GitHub Actions. Щоб тести пройшли із запуском усієї програми, ми повинні їх запускати з валідними даними по телеграм-боту: тобто, за його ім'ям та токеном… Тому у нас два варіанти:
  1. Таки зробити публічними ім'я та токен бота і сподіватися, що все буде добре, ніхто його не буде використовувати та заважати нам.
  2. Вигадати інший шлях.
Я вибрав другий варіант. У тестуванні SpringBoot є інструкція DataJpaTest , яка створена, щоб при тестуванні бази даних ми використовували тільки потрібні нам класи і не чіпали інші. Але нам це підходить, тому що телеграм-бот зовсім не запускатиметься. Отже, не потрібно передавати йому валідне ім'я і токен!))) Отримаємо тест, в якому перевіримо, що методи, які Spring Data нам реалізує, працюють так, як ми очікуємо. Тут важливо зазначити, що ми за допомогою інструкції @ActiveProfiles("test")задаємо використання профілю test. А це нам і потрібно, щоб ми вважали правильні проперти для нашої бази даних. Добре мати підготовлену базу даних перед запуском наших тестів. Для цієї справи є такий підхід: додати до тесту анотація Sql та передати їй колекцію імен скриптів, які потрібно запустити перед початком тесту:
@Sql(scripts = {"/sql/clearDbs.sql", "/sql/telegram_users.sql"})
У нас вони лежатимуть на шляху ./src/test/resources/ + шлях, вказаний в анотації. Ось як вони виглядають:
clearDbs.sql:
DELETE FROM tg_user;

telegram_users.sql:
INSERT INTO tg_user VALUES ("123456789", 1);
INSERT INTO tg_user VALUES ("123456788", 1);
INSERT INTO tg_user VALUES ("123456787", 1);
INSERT INTO tg_user VALUES ("123456786", 1);
INSERT INTO tg_user VALUES ("123456785", 1);
INSERT INTO tg_user VALUES ("123456784", 0);
INSERT INTO tg_user VALUES ("123456782", 0);
INSERT INTO tg_user VALUES ("123456781", 0);
Ось як у результаті виглядатиме наш тест TelegramUserRepositoryIT (як бачите, і ім'я для інтеграційного тестування буде інше — додаємо не Test, а IT):
package com.github.codegymcommunity.jrtb.repository;

import com.github.codegymcommunity.jrtb.repository.entity.TelegramUser;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;

import java.util.List;
import java.util.Optional;

import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.NONE;

/**
* Integration-level testing for {@link TelegramUserRepository}.
*/
@ActiveProfiles("test")
@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE)
public class TelegramUserRepositoryIT {

   @Autowired
   private TelegramUserRepository telegramUserRepository;

   @Sql(scripts = {"/sql/clearDbs.sql", "/sql/telegram_users.sql"})
   @Test
   public void shouldProperlyFindAllActiveUsers() {
       //when
       List<TelegramUser> users = telegramUserRepository.findAllByActiveTrue();

       //then
       Assertions.assertEquals(5, users.size());
   }

   @Sql(scripts = {"/sql/clearDbs.sql"})
   @Test
   public void shouldProperlySaveTelegramUser() {
       //given
       TelegramUser telegramUser = new TelegramUser();
       telegramUser.setChatId("1234567890");
       telegramUser.setActive(false);
       telegramUserRepository.save(telegramUser);

       //when
       Optional<TelegramUser> saved = telegramUserRepository.findById(telegramUser.getChatId());

       //then
       Assertions.assertTrue(saved.isPresent());
       Assertions.assertEquals(telegramUser, saved.get());
   }
}
Тести написали, але постає питання: а що буде із запуском нашого CI процесу на GitHub? Він же не матиме бази даних. На даний момент справді буде просто червоний білд. Для цього ми маємо GitHub actions, в якому ми можемо налаштувати запуск нашого білда. До запуску тестів потрібно додати запуск бази даних із потрібними налаштуваннями. Як виявилося, на просторах інтернету не так багато прикладів, тому раджу зберегти собі десь це. Оновимо файл .github/workflows/maven.yml:
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven

name: Java CI with Maven

on:
 push:
   branches: [ main ]
 pull_request:
   branches: [ main ]

jobs:
 build:
   runs-on: ubuntu-latest
   steps:
   - uses: actions/checkout@v2
   - name: Set up MySQL
     uses: mirromutth/mysql-action@v1.1
     with:
       mysql version: '5.7'
       mysql database: 'dev_jrtb_db'
       mysql root password: 'root'
       mysql user: 'dev_jrtb_db_user'
       mysql password: 'dev_jrtb_db_password'
   - name: Set up JDK 1.11
     uses: actions/setup-java@v1
     with:
       java-version: 1.11
   - name: Build with Maven
     run: mvn -B package --file pom.xml
Тепер тут є новий блок Set up MySQL . У ньому додаємо MySQL до нашого CI процесу, принагідно визначаючи потрібні для нас змінні. Тепер уже все, що хотіли ми додали. Останній етап – запушити зміни та подивитися, що білд пройде і буде зелений.

Оновлюємо документацію

Оновимо версію проекту з 0.3.0-SNAPSHOT на 0.4.0-SNAPSHOT в pom.xml і додамо в RELEASE_NOTES також:
## 0.4.0-SNAPSHOT

*   JRTB-1: added repository layer.
Після цього створюємо комміт, пуш і пулл-реквест. І найголовніше – наш білд зелений!"Java-проект від А до Я": Додаємо все, що пов'язане з БД.  Частина 2 - 3

Корисні посилання:

Усі зміни можна побачити ось тут у створеному пулл-реквесті . Дякую всім за прочитання.

Список всіх матеріалів серії на початку цієї статті.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ