[SpringBoot] @DataJpaTest In-Memory DB를 활용하여 테스트하기
안녕하세요. J4J입니다.
이번 포스팅은 @DataJpaTest In-Memory DB를 활용하여 테스트하는 방법에 대해 적어보는 시간을 가져보려고 합니다.
관련 글
[SpringBoot] Layer별 테스트 코드 작성하기 (1) - JPA를 이용한 Repository 테스트
들어가기에 앞서
@DataJpaTest 어노테이션을 활용하여 Repository 테스트 환경을 구성하는 방법은 위의 관련 글 링크에서 자세하게 확인할 수 있습니다.
기본적인 구성 방법에 대해 궁금하셨던 분들은 링크를 참고해 주시면 됩니다.
해당 글에서는 @DataJpaTest를 활용할 때 우리가 프로젝트에서 사용하는 실제 물리 DB가 아닌 in-memory DB를 활용하여 테스트하는 방법에 대해 적어보려고 합니다.
In-Memory DB 활용하는 이유
spring 개발을 하시면서 in-memory DB에 대해 접해보신 분들이 많이 계실 겁니다.
in-memory DB에 대해 간단하게 얘기해 보면 메모리 상에서 DB와 관련된 작업들을 모두 처리하기 때문에 디스크 상에 데이터들을 관리하는 다른 DB들에 비해 빠른 속도로 작업을 처리해 줄 수 있습니다.
왜냐하면 디스크보다는 메모리에 접근하는 속도가 더 빠르기 때문입니다.
하지만 디스크에 데이터가 저장되지 않기 때문에 메모리에 저장되어 있는 모든 데이터들은 휘발성이 됩니다.
왜냐하면 데이터를 가지고 있는 메모리가 비워지는 상황이 발생되면 저장되어 있는 모든 데이터가 사라지게 될 것이고, 메모리가 비워지는 상황은 쉽게 발생되기 때문입니다.
in-memory DB 가 무엇이 있는지에 대해 얘기해 볼 수 있는 것들은 대표적으로 redis, h2 등을 거론해 볼 수 있습니다.
그리고 일반적으로 spring test 환경에서 사용되는 DB로는 h2가 존재합니다.
본론으로 돌아와서 테스트를 수행할 때 in-memory DB를 사용하게 될 경우 얻을 수 있는 이점은 다음과 같습니다.
- 메모리 내에서 데이터 조작을 수행하기 때문에 더 빠른 속도로 테스트를 수행
- 실행되는 테스트 안에서만 독립적으로 수행되기 때문에 다른 것에 영향을 받지 않음
- 실행이 완료된 테스트는 모두 사라지기 때문에 완료된 테스트가 이후 테스트에 영향을 주지 않음
- 프로젝트에 사용되는 실제 물리 DB의 존재와 상관없이 테스트 가능
일반적으로 얘기해 볼 수 있는 이점은 위와 같이 얘기해볼 수 있습니다.
그 외에도 CI/CD 파이프라인 설정 등이 되어 있는 프로젝트에서도 배포를 할 때 항상 테스트도 함께 수행되고 싶은 경우에도 외부 설정과 전혀 관련이 없어지게 되기 때문에 편리하게 테스트 과정을 추가해 볼 수 있습니다.
in-memory DB를 이용하여 테스트 환경을 구성할 경우 위와 같이 많은 이점들을 얻을 수 있지만 단점도 존재하기는 합니다.
여러 단점들이 있겠지만 가장 문제가 될 수 있는 부분은 다음과 같습니다.
- 실제 환경과 동작되는 구성이 달라 모든 상황에 대해 완벽한 테스트를 했다고 볼 수는 없음
위의 단점을 가지고 있기 때문에 분명히 테스트 과정에서는 문제가 없었지만 실제 운영되는 과정에서 예상치 못한 문제를 경험할 수 있습니다.
하지만 언제, 어느 시점에 발생될지 정확히 판단할 수 없는 부분이기도 하며 in-memory DB를 사용할 경우 얻을 수 있는 이점들이 많이 존재하기 때문에 일반적으로는 in-memory DB를 설정하여 테스트 환경을 구성하는 것을 권장드립니다.
In-Memory DB 사용 전 테스트
사용 설정을 위해 간단하게 테스트 코드를 작성해 보겠습니다.
테스트를 위해 사용되는 DB는 mysql이며 다음과 같이 설정 값을 추가하여 프로젝트에서 사용되는 실제 물리 DB에 접근하는 테스트 코드를 작성하겠습니다.
// table
create table student (
no bigint auto_increment primary key,
name varchar(200),
age int,
school_no bigint
);
// entity
package com.jforj.repositorymemorytestconfig.entity;
import jakarta.persistence.*;
import lombok.*;
@ToString
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "student")
public class StudentEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long no;
private String name;
private int age;
private long schoolNo;
@Builder
public StudentEntity(String name, int age, long schoolNo) {
this.name = name;
this.age = age;
this.schoolNo = schoolNo;
}
}
// jpa repository
package com.jforj.repositorymemorytestconfig.repository;
import com.jforj.repositorymemorytestconfig.entity.StudentEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface StudentJpaRepository extends JpaRepository<StudentEntity, Long> {
}
// repository
package com.jforj.repositorymemorytestconfig.repository;
import com.jforj.repositorymemorytestconfig.entity.StudentEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.stream.Collectors;
@Repository
@RequiredArgsConstructor
public class StudentRepository {
private final StudentJpaRepository studentJpaRepository;
public List<String> insertAndSelectNames(String name, int age, long schoolNo) {
if (StringUtils.hasText(name)) {
StudentEntity studentEntity =
StudentEntity
.builder()
.name(name)
.age(age)
.schoolNo(schoolNo)
.build();
studentJpaRepository.save(studentEntity);
}
List<StudentEntity> studentEntities = studentJpaRepository.findAll();
return studentEntities.stream().map(StudentEntity::getName).collect(Collectors.toList());
}
}
위와 같이 설정되어 있는 상황에서 StudentRepository에 정의되어 있는 메서드에 대한 단위 테스트를 작성하려고 하면 다음과 같이 작성할 수 있습니다.
package com.jforj.repositorymemorytestconfig.repository;
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.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Repository;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
// JPA 테스트 코드 구성 설정을 도와주는 어노테이션 ( + @Repository 관련 설정 포함)
@DataJpaTest(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class))
// 프로젝트에서 설정한 DB를 사용하고 싶은 경우 다음과 같이 설정
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class StudentRepositoryTest {
@Autowired
private StudentRepository studentRepository;
@Test
void insertAndSelectNames() {
// given
String givenName = "테스트 이름";
int givenAge = 10;
long givenSchoolNo = 2;
// when
List<String> names =
studentRepository
.insertAndSelectNames(givenName, givenAge, givenSchoolNo);
// then
assertTrue(names.contains(givenName));
}
}
그리고 테스트를 실행해 보면 올바르게 테스트가 진행된 것을 볼 수 있습니다.
하지만 여기서 메서드 파라미터에 name 값을 넣지 않은 경우 insert 처리를 수행하지 않는 것을 볼 수 있습니다.
그리고 테스트를 수행하고 있는 테이블에 강제로 단위 테스트에서 작성되고 있던 name 값을 적재해 두겠습니다.
다음으로 단위 테스트 코드에서 name parameter를 null로 변경하여 insert 처리는 발생되지 않는 테스트를 진행해 보겠습니다.
package com.jforj.repositorymemorytestconfig.repository;
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.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Repository;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
// JPA 테스트 코드 구성 설정을 도와주는 어노테이션 ( + @Repository 관련 설정 포함)
@DataJpaTest(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class))
// 프로젝트에서 설정한 DB를 사용하고 싶은 경우 다음과 같이 설정
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class StudentRepositoryTest {
@Autowired
private StudentRepository studentRepository;
@Test
void insertAndSelectNames() {
// given
String givenName = "테스트 이름";
int givenAge = 10;
long givenSchoolNo = 2;
// when
List<String> names =
studentRepository
.insertAndSelectNames(null, givenAge, givenSchoolNo);
// then
assertTrue(names.contains(givenName));
}
}
단위 테스트를 통해 확인하고 싶었던 부분은 insert 처리가 발생되지 않았기 때문에 테스트가 올바르게 동작되지 않는 것입니다.
하지만 실제 물리 DB에 연결되어 테스트를 수행하고 있기 때문에 아무리 모든 테스트가 끝난 이후에 rollback이 된다고 하더라도 독립적이지 않기 때문에 테스트에 영향을 주는 것을 확인할 수 있습니다.
이런 점들이 위에서 얘기했던 in-memory DB를 사용할 경우 얻을 수 있는 이점들이 됩니다.
In-Memory DB 사용 환경 설정
위의 설정을 기반으로 h2 기반의 in-memory DB 사용 환경 설정을 해보겠습니다.
[ 1.h2 dependency 추가 ]
각자 사용하고 계시는 패키지 관리 설정 파일에 h2 dependency를 추가해 주시면 됩니다.
테스트 환경에서만 사용되기 때문에 gradle을 사용하는 경우는 다음과 같이 넣어주시면 됩니다.
dependencies {
// h2 db
testImplementation("com.h2database:h2:2.1.214")
}
[ 2. test properties 생성 ]
기존 프로젝트 동작에는 설정해 둔 물리 DB를 사용해야 되기 때문에 테스트를 하는 환경에서만 h2를 사용하도록 profile 관리를 해보겠습니다.
테스트를 할 때만 사용할 것이기 때문에 test profile을 위한 properties (or yml) 파일을 다음과 같이 작성해 볼 수 있습니다.
// application-test.yml
spring:
config:
activate:
# test profile 설정
on-profile: test
datasource:
# h2 기반 datasource 설정 (mysql을 사용하는 경우)
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:test;MODE=MYSQL
username: sa
password:
jpa:
hibernate:
# entity 기반 table 자동 생성을 위한 ddl-auto 설정
ddl-auto: create-drop
# database dialect 설정
database-platform: org.hibernate.dialect.H2Dialect
[ 3. test class에 test profile 추가 ]
기존에 작성되어 있던 단위 테스트 클래스에 설정해 둔 test profile 기반 동작이 될 수 있도록 @ActiveProfiles 어노테이션을 추가해 주겠습니다.
package com.jforj.repositorymemorytestconfig.repository;
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.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Repository;
import org.springframework.test.context.ActiveProfiles;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
// JPA 테스트 코드 구성 설정을 도와주는 어노테이션 ( + @Repository 관련 설정 포함)
@DataJpaTest(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class))
// 프로젝트에서 설정한 DB를 사용하고 싶은 경우 다음과 같이 설정
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
// test profile 설정 기반으로 테스트 동작 설정
@ActiveProfiles("test")
class StudentRepositoryTest {
@Autowired
private StudentRepository studentRepository;
@Test
void insertAndSelectNames() {
// given
String givenName = "테스트 이름";
int givenAge = 10;
long givenSchoolNo = 2;
// when
List<String> names =
studentRepository
.insertAndSelectNames(givenName, givenAge, givenSchoolNo);
// then
assertTrue(names.contains(givenName));
}
}
In-Memory DB 사용 환경 설정 테스트
위와 같이 설정을 마쳤다면 환경 설정 이전과 동일하게 단위 테스트를 진행해 보겠습니다.
먼저 위의 설정처럼 메서드 parameter에 name값을 넣어 insert 동작이 이루어지도록 해두면 다음과 같이 테스트가 성공하는 것을 볼 수 있습니다.
다음으로 parameter name 값을 null로 변경하여 테스트를 해보면 독립적으로 테스트가 이루어지기 때문에 다음과 같이 테스트가 실패하는 것을 볼 수 있습니다.
package com.jforj.repositorymemorytestconfig.repository;
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.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Repository;
import org.springframework.test.context.ActiveProfiles;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
// JPA 테스트 코드 구성 설정을 도와주는 어노테이션 ( + @Repository 관련 설정 포함)
@DataJpaTest(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class))
// 프로젝트에서 설정한 DB를 사용하고 싶은 경우 다음과 같이 설정
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
// test profile 설정 기반으로 테스트 동작 설정
@ActiveProfiles("test")
class StudentRepositoryTest {
@Autowired
private StudentRepository studentRepository;
@Test
void insertAndSelectNames() {
// given
String givenName = "테스트 이름";
int givenAge = 10;
long givenSchoolNo = 2;
// when
List<String> names =
studentRepository
.insertAndSelectNames(null, givenAge, givenSchoolNo);
// then
assertTrue(names.contains(givenName));
}
}
이상으로 @DataJpaTest In-Memory DB를 활용하여 테스트하는 방법에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.