유닛 테스트(unit test)는 컴퓨터 프로그래밍에서 소스 코드의 특정 모듈이 의도된 대로 정확히 작동하는지 검증하는 절차다.
즉, 모든 함수와 메소드에 대한 테스트 케이스(Test case)를 작성하는 절차를 말한다.
이를 통해서 언제라도 코드 변경으로 인해 문제가 발생할 경우, 단시간 내에 이를 파악하고 바로 잡을 수 있도록 해준다.
이상적으로, 각 테스트 케이스는 서로 분리되어야 한다. 이를위해 가짜 객체(Mock object)를 생성하는 것도 좋은 방법이다.
- 위키백과
배경
현재 내가 일하고 있는 조직에서는 테스트 코드 작성을 하지 않는다. 이로 인해 코드 수정이나 기능을 추가해야 할 때 수동으로 테스트를 진행해야 했다. 작은 변경사항에도 전체 기능을 다시 테스트해야 하는 상황이 자주 발생했고, 이는 상당히 소모적이라고 느껴졌다.
이런 경험을 통해 테스트 코드의 중요성을 더욱 실감하게 되었다. 테스트 코드가 있었다면 변경사항 검증이 더 효율적이고 신뢰할 수 있었을 거라고 생각한다. 이후 혼자 진행하는 프로젝트를 맡게 되었다. 테스트 코드를 작성하면서 개발을 하고싶었지만 일정에 맞춰 빠르게 개발을 해야 했기 때문에 테스트 코드를 작성하지 않고 개발을 진행했다. 이후 Mybatis에서 JPA로 마이그레이션하는 작업을 하면서 단위 테스트를 작성하기로 결정했다.
이번 프로젝트를 진행하면서 계층별 (Controller, Service, Repository) 단위 테스트 했던 내용을 정리하려고 한다.
본 글에서는 JUnit를 사용한 Spring Data JPA, Querydsl, Mybatis를 사용한 Repository 계층 단위 테스트에 대해 다룬다.
잘못된 내용이 있거나 더 좋은 코드를 알고계신다면 마음껏 댓글에 남겨주세요.
개발 환경
- Java 8
- Spring Boot 2.x
- Gradle
- Spring Data JPA, Mybatis, QueryDsl
- JSP
- JUnit, Mockito
- H2
- IntelliJ
구현
Gradle
dependencies {
...
// Testing
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-inline:5.2.0'
test {
useJUnitPlatform()
}
...
}
- org.springframework.boot:spring-boot-starter-test : Spring Boot 테스트를 위한 의존성. JUnit, AssertJ, Hamcrest, Mockito 등 테스트에 필요한 다양한 라이브러리가 포함되어 있다.
- org.mockito:mockito-inline:5.2.0 : final 클래스, static 메서드는 모킹할 수 있도록 해준다. static 메서드를 모킹할 필요가 없다면 의존할 필요 없다. 관련 내용은 별도 글을 작성했다. (링크)
- useJUnitPlatform() : JUnit5를 사용할 수 있도록 해준다. JUnit5는 자바를 위한 단위 테스트 프레임워크이다. 람다를 사용하기 때문에 Java8 이상부터 지원하고 있다. 테스트를 위한 편리한 기능들을 제공해준다.
Repository
단위테스트를 진행할 Repository 클래스다. Spring Data JPA, Querydsl를 사용해서 JPA Repsoitory 인터페이스, Querydsl Repository 인터페이스, Querydsl Repository 구현 클래스가 있다. 예시를 위해 대부분 메서드는 제외했다.
searchChargeTeamIdsByTaskId()는 입력받은 업무 ID에 해당하는 업무를 담당한 팀 ID 목록을 조회하는 역할을 한다.
@Repository
public interface JpaTaskManagerRepository extends JpaRepository<TaskEntity, Long>, JpaTaskManagerRepositoryCustom {
Optional<TaskEntity> findById(Long id);
}
public interface JpaTaskManagerRepositoryCustom {
List<Long> searchChargeTeamIdsByTaskId(Long id);
}
@Repository
@RequiredArgsConstructor
public class JpaTaskManagerRepositoryImpl implements JpaTaskManagerRepositoryCustom {
private final JPAQueryFactory query;
@Override
public List<Long> searchChargeTeamIdsByTaskId(Long id) {
QTaskOrganizationMappingEntity tom = QTaskOrganizationMappingEntity.taskOrganizationMappingEntity;
return query
.select(tom.organization.id)
.from(tom)
.where(tom.task.id.eq(id))
.fetch();
}
}
Test
Repository Test 클래스이다. Repository 단위 테스트는 Repository의 메서드나 쿼리를 독립적으로 검증한다. Repository 테스트에서는 데이터베이스에 대한 테스트를 하기 때문에 모킹을 사용할 필요가 거의 없다. 아래 테스트 코드를 부분별로 나누어 살펴보자
@DataJpaTest
@Import(QueryDslConfig.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class JpaTaskManagerRepositoryTest {
@Autowired
private JpaTaskManagerRepository taskManagerRepository;
@Autowired
private JpaTaskOrganizationMappingRepository taskOrganizationMappingRepository;
@Test
@DisplayName("특정 Task를 담당하는 Team ID 목록을 조회합니다.")
void searchChargeTeamIdsByTaskId() {
// given
Random random = new Random();
long taskId = (long) random.nextInt(100) + 1;
List<TaskOrganizationMappingEntity> dummyMappingEntityList = TestUtils.createDummyMappingEntityList(taskId);
taskOrganizationMappingRepository.saveAll(dummyMappingEntityList);
// when
List<Long> actualChargeTeamIdList = taskManagerRepository.searchChargeTeamIdsByTaskId(taskId);
// then
List<Long> expectedChargeTeamIdList = dummyMappingEntityList.stream()
.map(TaskOrganizationMappingEntity::getOrganization)
.map(OrganizationEntity::getId).collect(Collectors.toList());
assertThat(actualChargeTeamIdList).usingRecursiveFieldByFieldElementComparator().containsExactlyElementsOf(expectedChargeTeamIdList);
}
...
}
@DataJpaTest
@Import(QueryDslConfig.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class JpaTaskManagerRepositoryTest {
@Autowired
private JpaTaskManagerRepository taskManagerRepository;
- @DataJpaTest: Spring Boot에서 제공하는 어노테이션으로, JPA를 테스트하기 위한 설정을 자동으로 구성한다. @Transaction을 포함하고 있기 때문에 각 테스트가 독립적으로 실행된다. Mybatis로 개발을 했다면 @MybatisTest 애너테이션을 사용하면 된다.
- @Import(QueryDslConfig.class): Spring Data JPA와 Querydsl을 통합해서 개발을 했다면 @Import를 통해 Querydsl 관련 설정을 가져와야 한다. QueyrDslConfig 클래스는 Querydsl 사용을 위한 설정을 한다.
- @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE): 테스트에서 실제 DB를 사용하도록 설정한다. Replace.NONE 옵션을 삭제하면 Spring Boot에서 제공하는 내장 DB를 사용하게 된다. 내장 DB를 사용하면 빠르다는 장점이 있지만 실제 DB 환경과 다르기 때문에 나는 실제 DB 환경에서 테스트하도록 했다.
- @Autowired: 테스트 대상인 JpaTaskManagerRepository를 주입받는다.
@Test
@DisplayName("특정 Task를 담당하는 Team ID 목록을 조회합니다.")
void searchChargeTeamIdsByTaskId() {
// given
Random random = new Random();
long taskId = (long) random.nextInt(100) + 1;
List<TaskOrganizationMappingEntity> dummyMappingEntityList = TestUtils.createDummyMappingEntityList(taskId);
taskOrganizationMappingRepository.saveAll(dummyMappingEntityList);
// when
List<Long> actualChargeTeamIdList = taskManagerRepository.searchChargeTeamIdsByTaskId(taskId);
// then
List<Long> expectedChargeTeamIdList = dummyMappingEntityList.stream()
.map(TaskOrganizationMappingEntity::getOrganization)
.map(OrganizationEntity::getId).collect(Collectors.toList());
assertThat(actualChargeTeamIdList).usingRecursiveFieldByFieldElementComparator().containsExactlyElementsOf(expectedChargeTeamIdList);
}
public class TestUtils {
public static List<TaskOrganizationMappingEntity> createDummyMappingEntityList(Long taskId){
return Arrays.asList(
TaskOrganizationMappingEntity.builder()
.task(createDummyTaskEntity(taskId))
.organization(createDummyOrganizationEntity())
.build(),
TaskOrganizationMappingEntity.builder()
.task(createDummyTaskEntity(taskId))
.organization(createDummyOrganizationEntity())
.build(),
TaskOrganizationMappingEntity.builder()
.task(createDummyTaskEntity(taskId))
.organization(createDummyOrganizationEntity())
.build()
);
}
...
}
랜덤한 TaskId를 생성해 TaskId를 가진 TaskOrganizationMappingEntity 더미 데이터들을 생성해 저장한다.
이후 해당 TaskId를 전달해 실제 조회된 팀 ID 목록과 더미데이트들의 팀 ID 목록이 일치하는지 테스트한다.
- given-when-then 패턴과 given().willReturn() 메서드는 이전 Controller 단위 테스트 글에서 다뤘기 때문에 생략한다. (참고)
- usingRecursiveFieldByFieldElementComparator(): 객체의 모든 필드를 비교한다.
- containsExactlyElementsOf(): 두 리스트가 정확히 같은 요소를 같은 순서로 포함하고 있는지 확인한다.
테스트가 무사히 통과되었다.
Controller, Service 단위 테스트가 궁금하다면 아래 링크를 통해 확인할 수 있다.
'Spring' 카테고리의 다른 글
Spring Boot - Service 단위 테스트하기(JUnit5, Mockito, AssertJ) (1) | 2024.12.16 |
---|---|
Spring Boot - Controller 단위 테스트하기(JUnit, MockMvc, Mockito) (5) | 2024.12.13 |
Spring Boot - JPA 스키마, 데이터 초기화하기 (SQL script -> ddl-auto) (1) | 2024.11.13 |
Spring Boot Test - Mockito로 Static Method Mock 만드는 방법 (2) | 2024.11.01 |
Spring Interceptor - 인터셉터로 로그인 체크하기 (0) | 2023.09.12 |