이번 개인 과제를 진행하며 작성한 TestCode 관련 회고 내용입니다.
@DataJpaTest?
@DataJpaTest는 Spring Boot에서 제공하는 Test Annotation 중 하나로, JPA 관련 Component들만 로드하여 테스트 환경을 구성할 때 사용됩니다.
이 어노테이션을 사용하면 Repository와 관련된 Bean만 로드하고, 전체 Application Context 를 로드하지 않아 테스트를 더 빠르고 간단하게 수행할 수 있습니다.
그래서 DataJpaTest는 Repository Layer에 대한 격리된 테스트 환경을 설정하는데 유용합니다!
@DataJpaTest의 특징을 정리하면 아래와 같습니다.
1. JPA 관련 Bean만을 로드
@DataJpaTest는 Repository Layer만 테스트할 수 있도록 JPA 관련 Bean들만을 로드하기 때문에 서비스 계층이나 컨트롤러 계층의 빈들은 로드되지 않습니다. (통합 테스트를 진행하지 않는 이상 Service, Controller Test에는 해당 어노테이션을 사용하지 않습니다.)
2. 내장 데이터베이스 사용
기본적으로 내장 데이터베이스를 사용합니다. 별도로 설정하지 않으면 테스트 동안 Embedded Database를 사용하여 독립적인 테스트 환경을 제공합니다.
3. Transaction 관리
각 테스트 메서드는 Transaction 내에서 실행되며 하나의 테스트가 끝이 나면 Rollback됩니다. 이를 통해 테스트 간 데이터 독립성을 유지할 수 있습니다. (Transactiona이 자동으로 생성되기 때문에 해당 부분을 유의하여 TestCode 작성이 필요합니다.)
4. Hibernate 관련 설정
Hibernate와 관련된 설정도 함께 제공됩니다.
TodoRepositoryTest Class
Todo Card 전체를 조회할 때, 기존 TodoRepository에 작성한 QueryDSL이 정상적으로 작동하는지 체크해보는 TestCode를 작성해보았습니다.
시나리오는 각 FIlter를 설정하여 조회할 경우 원하는 값이 원하는 순서대로 조회되는지, PageSize와 같은 Paging 관련 설정도 정상적으로 작동하는지 함께 확인해 보았으며, 작성한 테스트 코드의 일부는 아래와 같습니다!
자세한 코드 내용은 아래의 Github 링크를 참고해주세요.
(https://github.com/JinkownHong/todo-security-project.git)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(value = [QueryDslConfig::class, JpaAuditingConfig::class])
@ActiveProfiles("test")
class TodoRepositoryTest @Autowired constructor(
private val todoRepository: TodoRepository,
private val userRepository: UserRepository
) {
@Test
fun `Search, Category, Completed 필터 모두 입력하지 않았을 때, Created Desc 으로 정렬되어 조회되는지 확인`() {
// GIVEN
userRepository.saveAllAndFlush(DEFAULT_USER_LIST)
todoRepository.saveAllAndFlush(DEFAULT_TODOCARD_LIST)
// WHEN
val pageable = PageRequest.ofSize(10)
val result = todoRepository.findAllWithFilters(
keyword = null, category = null, completed = null, sort = "createdAt", pageable = pageable
)
// THEN
result.shouldNotBeEmpty()
result.content.size.shouldBe(10)
val sortedList = DEFAULT_TODOCARD_LIST.sortedByDescending { it.createdAt }
result.content[0].title.shouldBe(sortedList[0].title)
}
.
.
.
companion object {
private val DEFAULT_USER_LIST = listOf(
User(email = "user1@naver.com", password = "password1", nickname = "user1"),
User(email = "user2@gmail.com", password = "password2", nickname = "user2")
)
private val DEFAULT_TODOCARD_LIST = listOf(
TodoCard(title = "06/24 오늘의 헬스 진행 계획", description = "벤치프레스 5세트, 덤벨프레스 5세트 계획", user = DEFAULT_USER_LIST[0], comments = mutableListOf(), completed = false, category = Category.EXERCISE),
TodoCard(title = "06/24 공부 계획", description = "알고리즘 문제 풀기, 데이터베이스 복습", user = DEFAULT_USER_LIST[1], comments = mutableListOf(), completed = false, category = Category.STUDY),
.
.
TodoCard(title = "07/07 기타 할 일", description = "서류 정리, 물건 정리", user = DEFAULT_USER_LIST[0], comments = mutableListOf(), completed = true, category = Category.OTHER)
).apply {
val defaultCommentList = listOf(
Comment(content = "Comment1 content", user = DEFAULT_USER_LIST[1], todoCard = this[0]),
Comment(content = "Comment2 content", user = DEFAULT_USER_LIST[0], todoCard = this[1]))
this[0].comments.add(defaultCommentList[0])
this[1].comments.add(defaultCommentList[1])
}
}
}
해당 코드의 몇가지 부분을 뜯어보면,
1. @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
- 테스트에서 실제 데이터베이스 구성을 사용하도록 도움을 주는 Annotation으로, 해당 설정은 @DataJpaTest와 함께 사용될 때, 내장 데이터베이스 대신 실제 데이터베이스를 사용하도록 합니다. Test Package 안 경로를 별도로 설정하여 'application.yml' 파일을 생성해놓았고, 안에 Test 전용 h2 데이터베이스를 사용하도록 설정하였습니다.
2. SaveAllFlush()
- 왜 일반적으로 사용하는 Save를 사용하지 않았을까요? 위에서 언급한 DataJpaTest의 특성을 다시 한번 생각해봐야 합니다! 일반적으로 Save는Transaction이 Commit 되기 전까지 실제 데이터베이스에 반영되지 않습니다. 그래서 Flsuh 작업을 함께 수행하여 Save 즉시 Transaction과 상관없이 바로 데이터베이스에 저장할 수 있도록 하기 위해 SaveAllFlush()를 사용하였습니다.
3. GIVEN, WHEN, THEN
- GIVEN (테스트 환경 설정)
- saveAllAndFlush(DEFAULT_USER_LIST),(DEFAULT_TODOCARD_LIST)를 통해 기본 사용자 목록과 할일 카드 목록을 데이터베이스에 저장하여 테스트 환경을 세팅합니다.
- WHEN (테스트할 동작 수행)
- 별도의 필터 설정 없이 모든 할일 카드를 createdAt 기준으로 정렬하여 페이지 크기 10으로 조회합니다.
- THEN (결과 검증)
- 결과의 크기가 10인지 확인하고, 할일 카드 목록이 createdAt 기준 내림차순으로 정렬되는지 결과 목록의 첫 번째 할 일 카드의 제목과의 일치 여부를 통해 확인합니다.
Test Code는 대체로 그렇습니다.. 작성 후 코드를 들여다보면 간단하고, 쉬워 보이지만 작성할 때 문제가 발생하면 잡기가 참~으로 힘들었습니다..!
그래서 아래 테스트 코드 작성 중 어려웠던 부분들도 함께 기록하여 남겨놓으려 합니다!
TodoRepositoryTest 작성 중 마주한 문제들!
1. @Import(value = [JpaAuditingConfig::class])
(결론부터 얘기하면, 현재 해당 클래스를 Import 할 필요는 없으나 추후 BaseTime Class 내부 코드를 JpaAuditing 기능을 사용하여 변경하고자 다음 Class를 Import 하였습니다.)
기본적으로 'CreatedAt Desc' 기준으로 정렬하고 있으며 'BaseTime' Abstract Class를 상속받아 사용하고 있습니다.
다음 Class의 어노테이션을 보면 @PrePersist, @PreUpdate와 같은 어노테이션을 활용 테스트 진행 시 문제없이 잘 작동하는 모습을 보이고 있지만, 처음 @CreatedDate와 같은 어노테이션을 활용했을 때는 Test 진행 시, CreatedAt이 지속적으로 Null을 반환하는 문제가 발생하였습니다.
문제는 DataJpaTest 진행 시, @EnableJpaAuditing을 자동으로 끌고오지 못해 별도로 Import를 해줘야 하는데 해당 어노테이션은 보통 Application 내부에 선언하기 때문에 Application을 Import 할 수 없었습니다. (Application 을 Import 시 Test 의미가 떨어지기 때문에)
때문에 JpaAuditingConfig Class를 별도로 생성하여 해당 Class 내부에 @EnableJpaAuditing을 선언한 후, 해당 Class를 Import 하는 방식으로 문제를 해결할 수 있었습니다.
2. Todo / Comment 순환 참조문제
처음 Comment를 'DEFAULT_COMMENT_LIST'를 생성하였으나, 'DEFAULT_TODOCARD_LIST'가 선언되기 전, 이를 참조하려고 해 순환 참조 문제가 발생합니다. 이를 해결하려면 Comment를 나중에 설정하도록 변경해야 해서,
위 코드에서는 TodoCard 리스트를 먼저 생성한 다음, apply 블록을 사용 각 TodoCard에 Comment를 추가해 순환 참조 문제를 해결할 수 있습니다.