비지니스 코드에 의존성 없이 테스트 시 데이터 베이스 초기화하는 방법

더기·2022년 4월 13일
1

JPA

목록 보기
2/3
post-thumbnail

이전까지 테스트 코드 작성 시 데이터베이스 초기화 방법


데이터베이스를 초기화 해야하는 이유는 각각의 테스트는 독립된 환경에서 검증이 되어야 하기 때문이다.

테스트 코드를 작성하면 각 테스트마다 초기화된 데이터 베이스로 테스트 하기 위해
데이터 베이스를 초기화 해주어야 한다.
그 때 나는 보통 repository.deleteAll()을 사용했었다.
이전에는 몰랐는데 이번 프로젝트를 하면서 고민을 하게되며, 단점과 다른 방법을 알게 되었다.

이전 방법의 단점


repository.deleteAll()의 단점은 테스트 코드가 프로덕션 코드에 의존하게 되는 것이다.
프로덕션 코드에 의존하게 되면, repository가 변경될 경우 테스트 코드에 의존 역시 변경되어야 하기 때문이다.
객체지향의 OCP를 위반하는 것이다.

해결 방법


프로덕션 코드와의 의존을 제거해보도록 하자.

먼저 @BeforeEach를 담당하는 테스트 클래스를 만든다.
나는 API 테스트를 RestAssured를 사용하기 때문에 RestAssured.port = port를 넣었고,
중요한 부분은 DatabaseCleanup, databaseCleanup.execute()이다.
해당 클래스를 import하는 방식으로 사용할 것이다.

import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import project.myblog.utils.DatabaseCleanup;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AcceptanceTest {
    @LocalServerPort
    int port;

    @Autowired
    private DatabaseCleanup databaseCleanup;

    @BeforeEach
    public void setUp() {
        RestAssured.port = port;
        databaseCleanup.execute();
    }
}

DatabaseCleanup

EntityManager

  • JPA를 사용하기 때문에 주입받았다.

tableNames

  • 영속성 컨텍스트에 등록된 모든 엔티티 테이블을 보관할 컬렉션

afterPropertiesSet

  • InitializingBean의 구현 메서드로 스프링의 모든 빈이 주입되고 난후 동작하는 메서드이다.
  • 내부 코드를 보면 영속성 컨텍스트에 있는 모든 엔티티를 꺼내서
    Entity 애노테이션이 달린 클래스들을 필터링한다.
  • 피터링된 클래스들은 자바 파일이기 때문에 upperCamel(SoccerMember)형태이기 때문에 JPA에서 자동으로 생성해주는
    데이터 베이스 테이블명 규칙인 lowerUnderscore로 변경한다.
    데이터 베이서 테이블명 규칙이 다르다면 다른 케이스로 변환하면 된다.
    그럼 tableNames에 담는다.

execute()

  • @Transactional이 있는 이유는 JPA는 당연하지만, 트랜잭션 내에서 동작해야하기 때문이다.
  • 클라이언트가 사용하는 인터페이스이다.
  • “SET REFERENTIAL_INTEGRITY FALSE"로 테이블의 제약 조건들을 비활성화하고,
    ”TRUNCATE TABLE " + tableName”으로 TRUNCATE하고,
    "ALTER TABLE " + tableName + " ALTER COLUMN ID RESTART WITH 1"으로 데이터베이스의
    ID컬럼을 1부터 시작하도록 초기화한다.
    마지막으로 "SET REFERENTIAL_INTEGRITY TRUE"로 다시 테이블의 제약 조건들을 활성화한다.
import com.google.common.base.CaseFormat;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
import java.util.stream.Collectors;

@Service
@ActiveProfiles("test")
public class DatabaseCleanup implements InitializingBean {
    @PersistenceContext
    private EntityManager entityManager;

    private List<String> tableNames;

    @Override
    public void afterPropertiesSet() {
        tableNames = entityManager.getMetamodel().getEntities().stream()
                .filter(e -> e.getJavaType().getAnnotation(Entity.class) != null)
                .map(e -> CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, e.getName()))
                .collect(Collectors.toList());
    }

    @Transactional
    public void execute() {
        entityManager.flush();
        entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();

        for (String tableName : tableNames) {
            entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
            entityManager.createNativeQuery("ALTER TABLE " + tableName + " ALTER COLUMN ID RESTART WITH 1").executeUpdate();
        }

        entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
    }
}

사용 방법은 AcceptanceTest를 상속받아 사용하면
AcceptanceTest에서 databaseCleanup.execute()에 의해
테스트마다 데이터 베이스를 초기화하여 작업하게 되며,
테스트끼리 꼬일 일이 없게된다.

이렇게 되면, 프로덕션 코드에 의존성을 제거하여, OCP를 지킬 수 있게된다.

TRUNCATE VS DELETE

DatabaseCleanup을 보면 DELETE가 아닌 TRUNCATE를 하고있는데
TRUNCATE일까? 둘의 차이는 여러가지가 있지만
이 코드를 작성한 의미는 속도 측면이다.
DELETE는 로우를 하나씩 제거하는 반면, TRUNCATE는 테이블의 공간 자체를 통으로 날려버리기 때문이다.

profile
wwqew11

0개의 댓글