JavaRush /Java Blog /Random-KO /데이터베이스와 관련된 모든 것을 추가합니다. (2부) - "Java 프로젝트 A부터 Z까지"
Roman Beekeeper
레벨 35

데이터베이스와 관련된 모든 것을 추가합니다. (2부) - "Java 프로젝트 A부터 Z까지"

Random-KO 그룹에 게시되었습니다
안녕하세요 여러분. 상기시켜 드리겠습니다. 첫 번째 부분에서는 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에서 이를 가져옵니다. 데이터베이스 사용자 이름과 비밀번호 값을 한 곳에서만 설정하도록 이렇게 했습니다. 이를 애플리케이션의 도커 이미지와 데이터베이스의 도커 컨테이너에 전달합니다. 다음으로 SpringBoot가 데이터베이스에 대한 변수를 허용하도록 지시하기 위해 Dockerfile을 업데이트해야 합니다.
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에 전달할 항목에는 기본값이 필요하므로 일부를 입력했습니다. 두 가지 요소로 마지막 줄을 확장하여 DB 사용자 이름과 비밀번호를 애플리케이션 시작에 전달합니다.
"-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를 실행하기 전에 환경 변수를 추가하는 방법을 이미 알고 있습니다. 이렇게 하려면 내보내기 var_name=var_value를 실행하면 됩니다. 따라서 다음 두 줄만 추가합니다.
export BOT_DB_USERNAME='prod_jrtb_db_user'
export BOT_DB_PASSWORD='Pap9L9VVUkNYj99GCUCC3mJkb'
여기에서 데이터베이스 사용자 이름과 비밀번호를 설정합니다. 물론, 봇의 이름과 토큰에 대해 하는 것처럼 bash 스크립트를 실행할 때 이러한 변수를 전달할 수 있습니다. 하지만 제가 보기에는 이것은 불필요한 것 같습니다. 실제로 데이터베이스에 접근하려면 데이터베이스가 배포될 서버의 IP를 알아야 하며, 요청에 대해 허용된 IP 주소 목록에 있어야 합니다. 나로서는 이것으로 이미 충분하다. 기반이 마련되었습니다. 이제 개발자가 더 이해하기 쉬운 작업, 즉 코드 작성 작업을 수행할 수 있습니다. 그 전에는 DevOps 엔지니어가 하는 일, 즉 환경을 설정하는 일을 하고 있었습니다.

저장소 계층 추가

일반적으로 애플리케이션에는 세 가지 계층이 있습니다.
  1. 컨트롤러는 애플리케이션의 진입점입니다.
  2. 서비스는 비즈니스 로직이 작동하는 곳입니다. 우리는 이미 이것을 부분적으로 가지고 있습니다: SendMessageService는 비즈니스 로직을 명시적으로 표현합니다.
  3. 리포지토리는 데이터베이스 작업을 수행하는 장소입니다. 우리의 경우 이것은 텔레그램 봇입니다.
이제 세 번째 레이어인 리포지토리를 추가하겠습니다. 여기서는 Spring 생태계의 프로젝트인 Spring Data를 사용합니다. Habré에 관한 이 기사에서 그것이 무엇인지 읽을 수 있습니다 . 우리는 다음과 같은 몇 가지 사항을 알고 이해해야 합니다.
  1. 우리는 JDBC를 사용하여 작업할 필요가 없습니다. 더 높은 추상화를 사용하여 직접 작업할 것입니다. 즉, 데이터베이스의 테이블에 해당하는 POJO를 저장합니다. Java Persistence API 에서 공식적으로 호출되는 이러한 클래스를 엔터티 라고 부를 것입니다 (이것은 ORM, 즉 JDBC 작업에 대한 추상화를 통해 데이터베이스 작업을 위한 일반적인 인터페이스 세트입니다). 데이터베이스에 저장할 엔터티 클래스가 있고 필요한 테이블에 정확하게 기록됩니다. 데이터베이스에서 검색할 때 동일한 개체를 받게 됩니다.
  2. Spring Data는 JpaRepository , CrudRepository 등 의 인터페이스 세트를 사용하도록 제안합니다. 다른 인터페이스도 있습니다. 전체 목록은 여기에서 찾을 수 있습니다 . 장점은 메소드를 구현하지 않고도 사용할 수 있다는 것입니다(!). 게다가 인터페이스에 새로운 메소드를 작성할 수 있는 특정 템플릿이 있으며 자동으로 구현됩니다.
  3. Spring은 개발을 최대한 단순화합니다. 이렇게 하려면 자체 인터페이스를 만들고 위에서 설명한 인터페이스를 상속해야 합니다. 그리고 Spring이 이 인터페이스를 사용해야 한다는 것을 알 수 있도록 Repository 주석을 추가합니다.
  4. 존재하지 않는 데이터베이스로 작업하기 위한 메서드를 작성해야 하는 경우에도 문제가 되지 않습니다. 작성해 보겠습니다. 거기서 무엇을 어떻게 해야 하는지 보여드리겠습니다.
이번 글에서는 TelegramUser의 전체 경로를 따라 추가하는 작업을 하고 이 부분을 예시로 보여드리겠습니다. 나머지 작업은 다른 작업으로 확장하겠습니다. 즉, /start 명령을 실행하면 사용자 데이터베이스에 active = true가 기록됩니다. 이는 사용자가 봇을 사용하고 있음을 의미합니다. 사용자가 이미 데이터베이스에 있는 경우 active = true 필드를 업데이트합니다. /stop 명령을 실행할 때 사용자를 삭제하지 않고 활성 필드만 false로 업데이트하므로 사용자가 봇을 다시 사용하려는 경우 봇을 시작하고 중단한 부분부터 시작할 수 있습니다. 그리고 테스트할 때 어떤 일이 일어나고 있는지 확인할 수 있도록 /stat 명령을 생성합니다. 이 명령은 활성 사용자 수를 표시합니다. 봇, 명령, 서비스 패키지 옆에 리포지토리 패키지를 만듭니다 . 이 패키지에서는 또 다른 단일 엔터티를 만듭니다 . 엔터티 패키지에서 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를 사용하지만 다른 구현도 사용할 수 있습니다. 다음은 우리가 사용하는 주석 목록입니다.
  • 엔터티 - 데이터베이스 작업을 위한 엔터티임을 나타냅니다.
  • 테이블 - 여기서는 테이블의 이름을 정의합니다.
  • Id - 주석은 테이블의 기본 키가 될 필드를 나타냅니다.
  • - 테이블에서 필드 이름을 결정합니다.
다음으로 데이터베이스 작업을 위한 인터페이스를 만듭니다. 일반적으로 이러한 인터페이스의 이름은 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는 활성 필드가 true 인 tg_user 테이블에서 모든 레코드를 가져와야 한다는 것을 이해합니다 . TelegramUser 엔터티 작업을 위한 서비스를 추가합니다(다른 엔터티의 서비스가 다른 엔터티의 저장소와 직접 통신할 수 없다는 맥락에서 해당 엔터티의 서비스를 통해서만 SOLID의 종속성 반전을 사용합니다). 패키지에 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);
   }
}
여기서는 Autowired 주석을 사용하여 생성자에서 TelegramuserRepository 객체의 종속성 주입(클래스 인스턴스 도입)을 사용한다는 점에 유의해야 합니다 . 변수에 대해 이 작업을 수행할 수 있지만 이는 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 객체를 생성자에 전달하여 새 사용자를 저장합니다. 또한 Java에서 Optional의 장점을 사용하면 다음 논리가 작동합니다. 데이터베이스에 사용자가 있으면 간단히 해당 사용자를 활성화하고, 그렇지 않은 경우 새 활성 사용자를 생성합니다. 중지명령:
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);
               });
   }
}
같은 방식으로 TelegramServiceTest를 StopCommand에 전달합니다. 추가 논리는 다음과 같습니다. 해당 채팅 ID를 가진 사용자가 있는 경우 이를 비활성화합니다. 즉, active = false로 설정합니다. 이것을 어떻게 직접 눈으로 볼 수 있습니까? 봇의 통계를 표시하는 새 명령 /stat를 만들어 보겠습니다. 이 단계에서는 모든 사용자가 사용할 수 있는 간단한 통계가 됩니다. 앞으로는 이를 제한하여 관리자에게만 접근을 허용할 예정입니다. 통계에는 활성 봇 사용자 수라는 항목이 하나 있습니다. 이렇게 하려면 CommandName에 STAT("/stat") 값을 추가합니다 . 다음으로 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 메소드 를 사용하여 모든 활성 사용자 목록을 얻고 컬렉션의 크기를 가져옵니다. 또한 이제 오름차순 클래스인 CommandContainerJavarushTelegramBot 를 업데이트하여 필요한 새 서비스를 전송하는 방법을 학습해야 합니다. 명령컨테이너:
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 주석을 추가합니다. 이는 애플리케이션 컨텍스트에서 이를 수신한다는 의미입니다. 또한 설명에 새로운 통계 명령이 표시되도록 HelpCommand 클래스를 업데이트하겠습니다 .

수동 테스트

docker-compose-test.yml과 JavarushTelegramBotApplication 클래스의 기본 메서드에서 데이터베이스를 시작하겠습니다. 다음으로 일련의 명령을 작성합니다.
  • /stat - 데이터베이스가 비어 있으면 이 봇을 사용하는 사람이 0명일 것으로 예상합니다.
  • /start - 봇을 시작합니다.
  • /stat - 이제 1명이 봇을 사용할 것으로 예상됩니다.
  • /stop - 봇을 중지합니다.
  • /stat - 다시 0명이 사용할 것으로 예상됩니다.
"A부터 Z까지의 Java 프로젝트": 데이터베이스와 관련된 모든 것을 추가합니다.  파트 2 - 2결과가 동일하다면 기능이 올바르게 작동하고 봇이 제대로 작동하고 있다고 말할 수 있습니다. 문제가 발생하더라도 문제가 되지 않습니다. 디버그 모드에서 기본 메서드를 다시 시작하고 전체 경로를 명확하게 탐색하여 오류가 무엇인지 찾습니다.

테스트를 작성하고 업데이트합니다.

생성자를 변경했으므로 테스트 클래스도 업데이트해야 합니다. 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") 주석을 사용하여 테스트 프로필의 사용을 지정한다는 점에 유의하는 것이 중요합니다 . 이것이 바로 데이터베이스의 올바른 속성을 계산하는 데 필요한 것입니다. 테스트를 실행하기 전에 데이터베이스를 준비하는 것이 좋을 것입니다. 이 문제에 대한 접근 방식이 있습니다. 테스트에 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 테스트는 다음과 같습니다(보시다시피 통합 테스트의 이름은 달라집니다. 테스트가 아니라 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());
   }
}
우리는 테스트를 작성했지만 GitHub에서 CI 프로세스를 시작하면 어떤 일이 일어날 것인가?라는 질문이 생깁니다. 데이터베이스가 없습니다. 지금은 실제로 빨간색 빌드만 있을 것입니다. 이를 위해 빌드 시작을 구성할 수 있는 GitHub 작업이 있습니다. 테스트를 실행하기 전에 필요한 설정으로 데이터베이스 시작을 추가해야 합니다. 알고 보니 인터넷에 예제가 많지 않으므로 어딘가에 저장해 두는 것이 좋습니다. .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 블록이 있습니다 . 여기서는 CI 프로세스에 MySQL을 추가하고 동시에 필요한 변수를 정의합니다. 이제 우리가 원하는 모든 것을 추가했습니다. 마지막 단계는 변경 사항을 푸시하고 빌드가 통과되어 녹색이 되는지 확인하는 것입니다.

문서 업데이트

pom.xml에서 프로젝트 버전을 0.3.0-SNAPSHOT에서 0.4.0-SNAPSHOT으로 업데이트하고 RELEASE_NOTES에도 추가해 보겠습니다.
## 0.4.0-SNAPSHOT

*   JRTB-1: added repository layer.
이 모든 과정이 끝나면 커밋, 푸시, 풀 요청을 생성합니다. 그리고 가장 중요한 것은 우리의 빌드가 친환경적이라는 것입니다!"A부터 Z까지의 Java 프로젝트": 데이터베이스와 관련된 모든 것을 추가합니다.  파트 2 - 3

유용한 링크:

생성된 풀 요청 에서 모든 변경 사항을 여기에서 볼 수 있습니다 . 읽어주신 모든 분들께 감사드립니다.

시리즈의 모든 자료 목록은 이 기사의 시작 부분에 있습니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION