0. 들어가며...
코딩을 할 때면 언제나 테스트 코드를 함께 작성하는 편이다.
테스트 코드의 중요성을 모를 개발자는 아마 없을 거라고 생각한다.
마틴 파울러의 <리팩토링>을 최근에 읽었는데, 거기에서도 테스트 코드의 중요성을 많이 강조하더라.
테스트 코드 없이 리팩토링을 한다는 건, 마치 줄 없이 곡예를 하는 것 같다고까지 이야기하니까.
"잘 돌아가던 코드가 리팩토링 후에 왜 안 되지?"라고 한탄하는 순간,
사실 그건 리팩토링이 아니라 코드의 무덤을 파는 일일지도 모른다.
그런데 가만히 생각해보니, "내가 지금껏 테스트 코드를 올바르게 활용하고 있었나?" 싶다.
서비스 레이어는 거의 테스트하지 않고 있었고, 'stub', 'Mock', 'test double' 같은 용어들은 학습했지만,
솔직히 말해 마음에 와닿지 않았다.
"이런 걸 왜 해야 하지?"란 의문과 함께, 어쩌면 좀 더 깊이 있게 공부하진 않았나 싶다.
이번에 블로그를 시작하면서 가장 먼저 테스트 코드에 대해 다시 한번 제대로 정리해보고 싶어졌다.
그러던 중 유튜브에서 눈에 띄는 강의 하나를 발견했다.
영어라서 반만 알아듣고, 코드를 보면서 이해해야 할 듯 싶지만, 아무것도 없는 것보다는 낫겠지.
아무튼, 이번 포스팅에서는 Spring Boot에서 Repository를 테스트하는 방법을 소개해볼까 한다.
https://www.youtube.com/watch?v=jqwZthuBmZY&list=PL82C6-O4XrHcg8sNwpoDDhcxUCbFy855E
https://github.com/teddysmithdev/pokemon-review-springboot
해당 강의에서는 예제 프로젝트인 포켓몬 REST API를 테스트하는 방법에 대해서 소개하고 있다.
이번 강의를 통해서 어떻게 테스트를 만들면 좋을지 배워보려고 한다.
일단 만만한 Repository부터 시작하자.
1. repository test
데이터베이스에 접근하는 객체인 repository는 service layer나 controller layer에 비해 조금 수월한 면이 있다.
일단 강의에서의 가이드 라인 코드를 보면
package com.pokemonreview.api.repository;
import com.pokemonreview.api.models.Pokemon;
import org.assertj.core.api.Assertions;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.List;
import java.util.Optional;
@DataJpaTest
@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2)
public class PokemonRepositoryTests {
@Autowired
private PokemonRepository pokemonRepository;
@Test
public void PokemonRepository_SaveAll_ReturnSavedPokemon() {
//Arrange
Pokemon pokemon = Pokemon.builder()
.name("pikachu")
.type("electric").build();
//Act
Pokemon savedPokemon = pokemonRepository.save(pokemon);
//Assert
Assertions.assertThat(savedPokemon).isNotNull();
Assertions.assertThat(savedPokemon.getId()).isGreaterThan(0);
}
1 - 1. 어노테이션
@DataJpaTest
- @DataJpaTest는 주로 JPA 관련 구성요소(예: 엔티티, 리포지토리 등)를 테스트할 때 사용된다.
- 이 어노테이션은 JPA 관련 설정만 로드하며, 데이터 소스 설정, JPA 엔티티, 리포지토리 등과 같은 데이터 접근 계층의 구성 요소에 초점을 맞춘다.
- @DataJpaTest 테스트용 데이터베이스(인메모리 데이터베이스 등)에 대한 자동 구성이 제공되며, JPA 엔티티 관련 테스트를 보다 쉽게 수행할 수 있다.
- 이 어노테이션은 테스트 후 데이터를 롤백하는 것이 기본 동작이므로, 각 테스트가 독립적으로 실행될 수 있도록 한다.
- 따라서 일반적으로 테스트 코드 클래스 레벨에 붙이는 @Transactional을 안 붙여줘도 된다!
- 전체 애플리케이션 컨텍스트를 로드하지 않기 때문에, @SpringBootTest에 비해 더 빠른 테스트 실행이 가능하다.
- 내가 해보니까 그리 큰 차이는 없는 듯 하다.
- @SpringBootTest → 252ms
- @DataJpaTest → 201ms
@AutoConfigureTestDatabase
- 테스트 수행시 데이터베이스 설정
- @DataJpaTest가 인메모리 데이터베이스를 자동 구성 해주므로 사실상 필요 없어 보임
1 - 2. AAA == Arrange, Act, Assert
흔히 테스트 코드 작성시, given, when, then 형태를 많이 쓰는데 이것과 매우 유사하다.
2. 사이드 프로젝트에 적용
현재 게시판 프로젝트를 REST API로 만드는 사이드 프로젝트를 진행하고 있다.
https://github.com/Griotold/boardsaturday-company-side-project
사내에서 토요일마다 직장동료들과 같이 코드 리뷰를 하기 때문에 boardsaturday라고 이름 지었다.
아무튼, 댓글에 해당하는 Comment 도메인의 repository를 테스트하는 코드를 가이드에 따라 작성해보자.
package com.bizplus.boardsaturday.infrastucture.persistence;
import com.bizplus.boardsaturday.domain.entity.Comment;
import com.bizplus.boardsaturday.domain.entity.Member;
import com.bizplus.boardsaturday.domain.entity.Post;
import com.bizplus.boardsaturday.domain.repository.CommentRepository;
import com.bizplus.boardsaturday.domain.type.ActiveStatus;
import com.bizplus.boardsaturday.domain.type.DeleteStatus;
import com.bizplus.boardsaturday.infrastucture.config.QuerydslConfig;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jdbc.EmbeddedDatabaseConnection;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import javax.persistence.EntityManager;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
@DataJpaTest
@Import({CommentRepositoryImpl.class, QuerydslConfig.class})
@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2)
@ActiveProfiles("test")
class CommentRepositoryImplTest {
@Autowired
CommentRepository commentRepository;
@Autowired
EntityManager em;
@BeforeEach
void dataSet() {
em.createNativeQuery("ALTER TABLE category ALTER COLUMN category_id RESTART WITH 1").executeUpdate();
em.createNativeQuery("ALTER TABLE post ALTER COLUMN post_id RESTART WITH 1").executeUpdate();
em.createNativeQuery("ALTER TABLE post_tag ALTER COLUMN post_tag_id RESTART WITH 1").executeUpdate();
em.createNativeQuery("ALTER TABLE tag ALTER COLUMN tag_id RESTART WITH 1").executeUpdate();
em.createNativeQuery("ALTER TABLE comment ALTER COLUMN comment_id RESTART WITH 1").executeUpdate();
em.createNativeQuery("ALTER TABLE member ALTER COLUMN member_id RESTART WITH 1").executeUpdate();
}
@Test
void CommentRepositoryImpl_create() throws Exception {
// given == Arrange
String content = "Muad'Dib";
Post post = new Post("title", "body", null, ActiveStatus.ACTIVE, DeleteStatus.EXISTING);
Member member = new Member("Griotold", "griotold@naver.com", ActiveStatus.ACTIVE, DeleteStatus.EXISTING);
Comment comment = new Comment(content, ActiveStatus.ACTIVE, DeleteStatus.EXISTING, post, member, null);
// when == Act
Comment savedComment = commentRepository.create(comment);
// then == Assert
assertThat(savedComment).isNotNull();
assertThat(savedComment.getId()).isGreaterThan(0L);
}
}
2 - 1. 어노테이션
@Import
CommentRepositoryImpl은 commentRepository를 구현하고 있으며 스프링 데이터 JPA인 JpaRepository를 상속받는 JpaCommentRepository와 querydsl의 QueryFactory를 필드로 갖고 있다.
따라서, @DataJpaTest만으로는 테스트를 동작시킬 수 없다. DataJpaTest는 위에서 언급했다시피, 엔티티나 스프링 데이터 JPA만 가져오는 어노테이션이다.
이 경우 @Import를 통해 필요한 spring bean을 주입받을 수 있다.
@ActiveProfiles("test")
현재 환경 설정 파일 yml파일을 dev와 test로 나누어 놓은 상태다.
@ActiveProfiles("test")를 사용하면 application-test.yml을 환경 설정 파일로 사용하여 테스트를 실행한다.
가이드에서 봤던 @AutoConfigureTestDatabase를 사용하면 간단히 해결될 것도 같지만
현재 @BeforeEach를 사용하여 각각의 테스트마다 다른 테스트 코드에게 영향을 주지 않기 위해 id를 1로 restart 해주는 네이티브 쿼리를 실행시켜주기 때문에 @ActiveProfiles("test")는 필수적이다. application-test.yml의 ddl-auto 설정이 create라서 각 테스트 마다 ddl문을 새롭게 실행이 되고 네이티브 쿼리를 사용할 수 있게 된다. @AutoConfigureTestDatabase 이것만 가지고서는 네이티브 쿼리를 사용할 수 없다.
@Transactional의 부재
@DataJpaTest가 기본적으로 테스트 종료후 roll back을 수행하기 때문에 @Transactional은 붙이지 않아도 된다.
2 - 2. 테스트 메서드 네이밍
이전에는 @DisplayName을 사용하여 테스트 코드 메서드에 한글로 구분하였다. 이 방법이 안 좋은 이유가 어떤 클래스에서 동작하는지 한 눈에 알아보기 어렵다는 점이다. 가이드에서 본 것 처럼 테스트 메서드 네이밍은
테스트하고자 하는 클래스명_메소드명_옵션
이 괜찮아 보인다.
3. 정리
스프링 데이터 Jpa를 직접적으로 테스트한다면 @DataJpaTest를 사용하는 것이 합리적으로 보이지만 지금처럼 여러 spring bean을 주입받아야 하는 경우 @SpringBootTest를 사용하는 것이 간편하다고 생각한다. 속도도 그리 차이 안난다.
다음번에는 service layer 테스트 방법을 학습한 후 정리할 예정이다.
'테스트' 카테고리의 다른 글
DisplayName을 섬세하게 (4) | 2024.10.21 |
---|---|
테스트 코드에 example data 를 생성하는 4가지 방법 (0) | 2024.03.24 |