[I-project] Repository 테스트 코드를 작성하며 했던 고민

Jifrozen·2024년 4월 16일
0

I-project

목록 보기
4/4

JPARepository에서 제공하는 기본적인 저장, 조회, 삭제와같은 메서드말고 Repository 메서드 테스트는 어떻게 하면 좋을까?
고민 목록
1. Repository 테스트 환경 설정
2. 도메인 생성을 위해 Fixture 사용

@Query(
            "select s from Schedule s where s.userId = :userId and (:startDate <= s.startDate and :endDate>=s.startDate) or (:startDate <= s.endDate and :endDate>=s.endDate)")
    List<Schedule> findByUserIdAndDateWithinRange(
            @Param("userId") Long userId,
            @Param("startDate") LocalDateTime startDate,
            @Param("endDate") LocalDateTime endDate);

테스트 하고자하는 코드이다.

Repository 테스트 환경 설정

Repository 테스트 환경을 세팅하기 위해 다음과 같은 결정을 내렸다.

  • h2 데이터베이스를 활용한 테스트 디비 구성
    독립성을 보장하고 실제 데이터베이스 환경가 동일하기 때문에 h2 데이터베이스를 테스트 DB로 사용하고자 하였다.
  • 반복 코드를 줄이기 위한 RepositoryTest 생성

Repository도 테스트를 해야하기때문에 반복적으로
RepositoryTest에 필요한 어노테이션과 필드를 가지고 있는 부모 클래스를 만들어준다.

@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE)
@Import(value = {TestFixtureBuilder.class, BuilderSupporter.class})
@Sql(value = {"/truncate.sql"})
public class RepositoryTest {
    @Autowired protected TestFixtureBuilder testFixtureBuilder;
}

@DataJpaTest : 단위테스트를 위해 리포지토리 관련 빈들만을 로드하고, 리포지토리들을 테스트하기 위한 트랜잭션

DataJpaTest는 @AutoConfigureTestDatabase를 통해 테스트 데이터베이스를 in-memory로 자동으로 설정되어있도록 되어있다.

@AutoConfigureTestDatabase(replace = NONE):
하지만 해당 환경의 경우 h2 DB를 통해 테스트 db를 커스텀하여 다룰 예정이여서
replace = NONE을 통해 실제 데이터베이스를 사용하도록 지정하겠다. 즉, 실제 데이터베이스를 사용하며, 테스트용으로 데이터베이스를 대체하지 않는다.

@Import(value = {TestFixtureBuilder.class, BuilderSupporter.class}) :
@DataJpaTest로 리포지토리와 관련된 빈들이 로드되었지만 테스트에 필요하여 생성한 클래스도 빈으로 등록시키기 위해 Import 어노테이션을 사용하여 수동으로 등록해준다

@Sql(value = {"/truncate.sql"}): 이 애노테이션은 테스트 전에 실행될 SQL 파일을 지정한다. 여기서는 /truncate.sql 파일을 지정하여 테스트를 위해 테이블을 초기화해야한다.

SET REFERENTIAL_INTEGRITY FALSE;
TRUNCATE TABLE users;
TRUNCATE TABLE schedule;
TRUNCATE TABLE schedule_time;
SET REFERENTIAL_INTEGRITY TRUE;

REFERENTIAL_INTEGRITY 참조 무결성 설정을 끈다. 테이블 내용을 지울떄 외래키로 무결성 제약으로 삭제가 안되기 때문에 끄고 truncate를 통해 테이블안에 내용을 삭제 후 다시 무결성 제약을 켜준다.

도메인 생성을 위해 Fixture 사용

테스트 코드를 짜다보면 반복적으로 도메인을 생성하는 메서드를 만들게 된다.
이런 반복적인 도메인을 따로 클래스로 만들어 정적 메서드를 통해 사용하면 좋을지
아님 독립성을 위해 도메인이 필요한 클래스 안에 private하게 생성하면 좋을지 고민이 되었다.

이런 고민끝에 서치를 통해 Test Fixture를 발견하였다.

Test Fixture는 특정 항목, 장치 또는 소프트웨어를 일관되게 테스트하는 데 사용되는 장치이다.

내가 해석한 Test Fixture는 일관되게 테스트를 하기 위해 테스트에 필요한 데이터를 일관되게 만들도록 하는 장치라고 해석했다. 따라서, 테스트에 필요한 도메인들을 픽처로 지정하여 일관되게 static 하게 관리하고자 하였다.

public class UserFixtures {

    public static User COMPONO_USER() {
        UserAddRequest request =
                new UserAddRequest(
                        "test@test.com",
                        "compono",
                        OauthProvider.KAKAO,
                        UUID.randomUUID().toString(),
                        true);
        return request.toEntity();
    }
}
@Component
public class TestFixtureBuilder {
    @Autowired private BuilderSupporter builderSupporter;

    public ScheduleTime buildScheduleTime(ScheduleTime scheduleTime) {
        return builderSupporter.scheduleTimeRepository().save(scheduleTime);
    }

    public List<Schedule> buildSchedule(List<Schedule> schedules) {
        return builderSupporter.scheduleRepository().saveAll(schedules);
    }

    public User buildUser(User user) {
        return builderSupporter.userRepository().save(user);
    }
}

최종적으로 밑에와 같이 코드를 마무리하였다.

class ScheduleRepositoryTest extends RepositoryTest {

    @Autowired private ScheduleRepository scheduleRepository;

    @Test
    void findByUserIdAndDateWithinRange() {
        User MOCK_USER = testFixtureBuilder.buildUser(COMPONO_USER());
        Long userId = MOCK_USER.getId();
        List<Schedule> expectedSchedules =
                testFixtureBuilder.buildSchedule(
                        List.of(TODAY_AND_TOMORROW_SCHEDULE(userId), TODAY_SCHEDULE(userId)));

        LocalDateTime todayStart = LocalDateTime.now().toLocalDate().atStartOfDay();
        LocalDateTime todayEnd = todayStart.plusDays(1).minusSeconds(1);

        List<Schedule> actualSchedules =
                scheduleRepository.findByUserIdAndDateWithinRange(userId, todayStart, todayEnd);

        Assertions.assertThat(actualSchedules)
                .usingRecursiveFieldByFieldElementComparator()
                .isEqualTo(expectedSchedules);
    }
}

0개의 댓글