
데이터베이스를 초기화 해야하는 이유는 각각의 테스트는 독립된 환경에서 검증이 되어야 하기 때문이다.
테스트 코드를 작성하면 각 테스트마다 초기화된 데이터 베이스로 테스트 하기 위해
데이터 베이스를 초기화 해주어야 한다.
그 때 나는 보통 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();
}
}
EntityManager
tableNames
afterPropertiesSet
execute()
“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를 지킬 수 있게된다.
DatabaseCleanup을 보면 DELETE가 아닌 TRUNCATE를 하고있는데
왜 TRUNCATE일까? 둘의 차이는 여러가지가 있지만
이 코드를 작성한 의미는 속도 측면이다.
DELETE는 로우를 하나씩 제거하는 반면, TRUNCATE는 테이블의 공간 자체를 통으로 날려버리기 때문이다.