테스트 코드를 작성하다 보면 “어제는 성공했는데 오늘은 실패한다?” 수정한 게 하나도 없는데 이런 일이 벌어질 때가 있습니다.
이번 글에서는 제가 실제로 겪은 LocalDateTime.now() 때문에 테스트가 간헐적으로 실패한 경험과 이를 해결한 방법을 정리해 보았습니다.
이슈 발생
제가 회사에서 맡고있는 소개팅 앱의 기능에는 이성의 카드를 받고 좋아요 또는 호감 메시지 보내기 기능이 있습니다.
이런 기능을 액션을 했다고 표현하는데 액션을 하게 되면 카드의 D-DAY를 갱신해주게 됩니다.
D-DAY 갱신 방식으로는 기존 D-DAY가 7인 카드는 액션 시 D-7로 갱신하고, D-DAY가 1~6인 카드는 D-6으로 갱신해줍니다.
- 갱신 시에는 카드 데이터의 생성시간을 직접 수정
- D-DAY는 별도의 컬럼으로 관리하지 않고 생성일자와 현재시간을 기준으로 계산
위의 로직과 관련해서 액션한 이후의 D-DAY 값을 검증하는 테스트 코드를 작성했는데
테스트코드를 실행하는 시간에 따라 성공/실패가 갈리는 이슈가 발생했습니다.
원인 분석
해당 이슈를 계속 테스트하며 원인을 분석한 결과, 12시 30분을 기준으로 이전에는 테스트가 실패하고 이후에는 성공하는 패턴을 발견했습니다.
이에 따라 D-DAY를 갱신하는 로직과 D-DAY를 계산하는 로직을 살펴보았고, 원인을 정확히 파악할 수 있었습니다.
서비스 정책상 D-DAY가 12시 30분을 기준으로 변경되는 경우가 있는데, 테스트 코드에서는 이를 고려하지 않고 특정 D-DAY 값을 기대값으로 설정했기 때문에 발생한 문제였습니다.
즉, D-DAY가 D-5인 카드가 액션한 이후 D-6으로 D-DAY가 갱신되었는지 테스트를 작성했는데 아래와 같이 D-DAY가 갱신 & 계산 하기 때문에, 실행 시점이 12시 30분 이전인 경우 D-7로 12시 30분 이후에는 D-6으로 결과값이 나오는 문제였습니다.
즉, D-DAY가 D-5인 카드가 액션된 후 D-6으로 갱신되는지 확인하는 테스트를 작성했는데,
D-DAY가 갱신되고 계산되는 방식 때문에 실행 시점이 12시 30분 이전이면 D-7로 나오고 이후면 D-6으로 결과가 달라지게 되었습니다.
D-DAY 갱신 & 계산 방식
- 카드 액션한 뒤 현재 D-DAY가 7이면 D-DAY를 오늘 12시 30분으로 갱신하고, 현재 D-DAY가 6 이하인 경우 하루 전 12시 30분으로 카드 생성일자를 갱신한다.
- 카드 D-DAY 계산 할 때는 카드 생성일자가 현재 시간으로 부터 24시간이 지나지 않았다면 D-7로 지났다면 D-6로 판단한다.
해결 방안
서비스 정책상 12시 30분을 기준으로 카드 D-DAY가 변경되는데, 테스트에서는 실행 시점을 통제하지 못해 문제가 발생했습니다.
이에 따라 테스트 코드 내에서 시간을 통제하는 방법으로 두 가지 방안을 고려했습니다.
1. 테스트 코드 내에서 현재 시간이 12시 30분 이전/이후인지에 따라 결과값을 다르게 세팅한다.
2. 테스트 코드 내의 LocalDatetime.now()를 stubbing 해서 12시 30분 이전/이후에 대한 테스트코드를 각각 생성
첫 번째 방법은 구현이 비교적 간단하지만, 테스트 코드에 분기문이 포함되므로 다른 팀원이 읽을 때 이해하기 어려울 수 있다는 단점이 있습니다.
반면 두 번째 방법은 static 메서드를 모킹 처리하므로, 적절히 해제하지 않으면 다른 테스트에서 LocalDateTime.now()가 스터빙된 값으로 실행될 수 있어 테스트가 깨질 위험이 있습니다.
하지만 테스트 코드가 가독성이 1번보다 명확하고, 테스트에 사용되는 값을 확실하게 통제할 수 있다는 장점이 있어 저는 이 방법을 선택했습니다.
아래는 두 번째 방법으로 수정한 테스트 코드 일부입니다.
LocalDateTime fixedDate = LocalDateTime.of(2025, 4, 23, 12, 29);
LocalTime fixedTime = LocalTime.of(12, 29);
MockedStatic<LocalDateTime> mockLocalDateTime = mockStatic(LocalDateTime.class, CALLS_REAL_METHODS); // close 필수
MockedStatic<LocalTime> mockLocalTime = mockStatic(LocalTime.class, CALLS_REAL_METHODS); // close 필수
mockLocalDateTime.when(LocalDateTime::now).thenReturn(fixedDate);
mockLocalTime.when(LocalTime::now).thenReturn(fixedTime);
LocalDate today = LocalDate.now();
LocalDateTime now = LocalDateTime.now(); // 테스트 끝나면 반드시 close
...
mockLocalDateTime.close();
mockLocalTime.close();
- CALLS_REAL_METHODS: LocalDateTime.now()/LocalTime.now()만 Mocking하고 나머지 메서드는 실제 실행
- close() 필수: 테스트 종료 후 static mock을 해제하지 않으면 다른 테스트에 영향 가능성 있음
@TestFactory를 쓸 때는 onClose()로 처리할 수도 있습니다.
마무리
이번 이슈를 해결하며 완벽한 테스트 코드를 위해서는 시간을 확실하게 통제하고 있는지를 확실히 체크 해야겠다라는 생각을 했습니다.
그리고 이번 이슈에는 해당되지는 않았지만 프로덕션 코드에서도 LocalDateTime.now()와 같은 코드를 repository layer에서 직접 사용하지 말고, 적절한 위치에서 주입 해주는게 좋겠다는 생각이 들었습니다.
'Trouble Shooting > Java' 카테고리의 다른 글
| [Java] UnsupportedOperationException 에러 해결 (0) | 2022.07.20 |
|---|---|
| [Java] java.math.BigDecimal cannot be cast to java.lang.Integer 에러 해결 (0) | 2022.07.15 |