JavaRush /Блог /Java-проекты /Добавляем все, что связано с БД. (Часть 2) - "Java-проект...
Roman Beekeeper
35 уровень

Добавляем все, что связано с БД. (Часть 2) - "Java-проект от А до Я"

Статья из группы Java-проекты
Всем привет. Напомню: в первой части мы добавляли 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.javarush_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: она будет отображать количество активных пользователей. Создаем repository пакет рядом с пакетами bot, command, service. В этом пакете создаем еще один — entity. В пакете entity создаем класс TelegramUser:

package com.github.javarushcommunity.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.javarushcommunity.jrtb.repository;

import com.github.javarushcommunity.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.javarushcommunity.jrtb.service;

import com.github.javarushcommunity.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.javarushcommunity.jrtb.service;

import com.github.javarushcommunity.jrtb.repository.TelegramUserRepository;
import com.github.javarushcommunity.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.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import com.github.javarushcommunity.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.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import com.github.javarushcommunity.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.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import com.github.javarushcommunity.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 и JavarushTelegramBot, чтобы они научились передавать нужный нам новый сервис. CommandContainer:

package com.github.javarushcommunity.jrtb.command;

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

import static com.github.javarushcommunity.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.javarushcommunity.jrtb.command;

import static com.github.javarushcommunity.jrtb.command.CommandName.STAT;
import static com.github.javarushcommunity.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.javarushcommunity.jrtb.repository;

import com.github.javarushcommunity.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

Полезные ссылки:

Все изменения можно увидеть вот здесь в созданном пулл-реквесте. Всем спасибо за прочтение.

Список всех материалов серии в начале этой статьи.

Комментарии (11)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Anonymous #3465899 Уровень 3
8 августа 2024
Не собирался билд, была вот такая ошибка:

Error:  Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.10.1:testCompile (default-testCompile) on project telegrambot: Compilation failure
Помогло только добавление в помник плагина с флагом testFailureIgnore = true:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.2.5</version>
    <configuration>
        <testFailureIgnore>true</testFailureIgnore>
    </configuration>
</plugin>
Anonymous #2856674 Уровень 18
8 апреля 2024
У меня, к сожалению, данные при билде в CI подтягиваются из application.properties вместо application-test.properties , поэтому пришлось в application.properties заменить URL БД на локалхост, иначе флайвей не находит базу
Peace The Ball Уровень 32
3 ноября 2023
Не знаю на сколько внимательно мне удавалось следить за развитием событий этого проекта? Насколько я понял то в файле application-test.properties занчения полей bot.username и bot.token были указаны 'не верные'. Но в этом случае у меня никак не проходили тесты с TelegramUserRepositoryIT.java. Всегда выкидывалась ошибка Error removing old webhook. 401 Unauthorized. Только после того как я указал истынные значения переменных bot.username и bot.token в файле application-test.properties - тесты наконец то прошли успешно.
Сергей Тишин Уровень 20
3 марта 2023
Делаю проект с postgres, столкнулся с такой проблемой, что после пуша на гитхаб билд не проходит с ошибкой: FATAL: password authentication failed for user "root" Хотя локально всё сбилдилось нормально и всё работает. В чём может быть причина? Пароли проверил везде, все совпадает
Iryna Уровень 23
31 мая 2022
Когда запушила проект на гитхаб, у меня не проходил билд с ошибкой – "Access denied for user dev_tb_db_user@172.19.0.1 using password "YES". Пришлось в файле application-test.properties указать spring.datasource.username=root а не spring.datasource.username=dev_tb_db_user. Mожет есть идеи почему на гитхабе возникла эта ошибка? Локально все отлично работало.
Ivan Уровень 41
28 июля 2021
Все как всегда на высоте, но есть небольшое замечание:) В Issues по задачей планировалось использование базы H2, из описания задачи H2 database for testing purposes added integration tests for with h2 added жаль что в итоге реализовали с MySQL.