간단하게 토이프로젝트를 하던 중 LocalDateTime.now()에 관련된 테스트코드를 작성해야 할 일이 생겼습니다.
직면한 문제는
1. LocalDateTime.now() 는 테스트 할 때마다 달라지는 값이며,
2. 객체에 now()가 주입되는 시점의 값과, 테스트시점의 now()의 값이 달라질 수 있다.
였습니다.
회원 삭제로직은 아래와 같습니다.
- 회원삭제요청
- 회원객체의
deleted를 true로,deletedAt을 LocalDateTime.now()로 변경- 변경된 객체 저장
Service
// 회원를 삭제한다.
@Transactional
override fun deleteUser(username: String): UserInformation {
// user를 가져오고, 없으면 예외를 발생시킨다.
val foundUser: UserEntity = userRepository.findByUsername(username) ?: throw ApplicationException(
ErrorCode.USER_NOT_FOUND, ErrorCode.USER_NOT_FOUND.message
)
// delete() 메서드를 통하여 상태를 변경한다.
val deleteUser: UserEntity = foundUser.delete()
// 전 더티체킹 사용안하고 명시적으로 저장해주는게 더 나은거 같더라구요...()
userRepository.save(deltedUser)
// dto의 형태로 return 해준다.
return UserInformation(deleteUser)
}
Domain
fun delete(): UserEntity = copy(
deleted = true,
deletedAt = LocalDateTime.now()
)
Domain코드가 조금은 독특한데,
엔티티를 불변객체로 지정하였기 때문에
파라미터의 값만 변경해서 새로운 객체를 생성하여 반환 해주는 방식을 사용했습니다.
ServiceTest
@Test
fun 회원삭제에_성공한다() {
//given
val user: UserEntity = UserEntity.fixture()
val deletedUser: UserEntity = user.copy(
deleted = true,
deletedAt = LocalDateTime.now()
)
given(userRepository.findByUsername(user.username)).willReturn(user)
given(userRepository.save(deletedUser)).willReturn(deletedUser)
//when
val result: UserInformation = userService.deleteUser(user.username)
//then
then(userRepository).should().findByUsername(user.username)
then(userRepository).should().save(deletedUser)
assertThat(result.username).isEqualTo(deletedUser.username)
assertThat(result.deleted).isEqualTo(deletedUser.deleted)
assertThat(result.deletedAt).isEqualTo(deletedUser.deletedAt)
}
위 테스트 코드는 deletedAt필드에 들어가는
TestCode에서의 시간값과
실제 Service안에서의 시간값이 다르기 때문에
테스트에 실패하게 됩니다.
서칭한 결과 여러 방식이 있었지만
나름 최대한 간단하게 값을 Mocking하는 방식을 택했습니다.
이미 많은 분들이 고민을 하시고, 해결방법을 제시해주신 방법인
파라미터로 Clock 객체를 사용하는 방법을 택했습니다.
* 공식문서에도 LocalDateTime 테스트시에 Clock 인자를 사용하라고 명시가 되어있었기에 믿고 사용했습니다.

@Configuration
class ClockConfig {
fun getClock(): Clock = Clock.systemDefaultZone()
}
SpringBean으로 등록했기 때문에 앞으로 테스트코드에서 손쉽게 모킹이 가능해집니다.
Service
class UserServiceImpl(
private val userRepository: UserRepository,
private val clockConfig: ClockConfig, // 의존관계 주입
) : UserService {
// 회원를 삭제한다.
@Transactional
override fun deleteUser(username: String): UserInformation {
val foundUser: UserEntity = userRepository.findByUsername(username) ?: throw ApplicationException(
ErrorCode.USER_NOT_FOUND, ErrorCode.USER_NOT_FOUND.message
)
// delete() 메서드에 clockConfig.getClock() 파라미터 추가
val deleteUser = foundUser.delete(clockConfig.getClock())
userRepository.save(deleteUser)
return UserInformation()
}
}
domain
fun delete(clock: Clock): UserEntity = copy(
deleted = true,
deletedAt = LocalDateTime.now(clock)
)
@Test
fun 회원삭제에_성공한다() {
//given
// clockConfig.getClock() 호출시 지정한 시간을 반환하도록 mocking
given(clockConfig.getClock()).willReturn(Clock.fixed(Instant.parse("2025-04-02T12:30:30Z"), ZoneId.systemDefault()))
val user: UserEntity = UserEntity.fixture()
val deletedUser: UserEntity = user.copy(
deleted = true,
deletedAt = LocalDateTime.now(clockConfig.getClock())
)
given(userRepository.findByUsername(user.username)).willReturn(user)
given(userRepository.save(deletedUser)).willReturn(deletedUser)
//when
val result: UserInformation = userService.deleteUser(user.username)
//then
then(userRepository).should().findByUsername(user.username)
then(userRepository).should().save(deletedUser)
assertThat(result.username).isEqualTo(deletedUser.username)
assertThat(result.deleted).isEqualTo(deletedUser.deleted)
assertThat(result.deletedAt).isEqualTo(deletedUser.deletedAt)
}
Clock객체를 사용하고, 값을 mocking하여 테스트 코드에 성공할 수 있었습니다!
