데이터베이스를 초기화 해야하는 이유는 각각의 테스트는 독립된 환경에서 검증이 되어야 하기 때문이다.
테스트 코드를 작성하면 각 테스트마다 초기화된 데이터 베이스로 테스트 하기 위해
데이터 베이스를 초기화 해주어야 한다.
그 때 나는 보통 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
는 테이블의 공간 자체를 통으로 날려버리기 때문이다.