구현 코드 내부에서 LocalDate.now()
를 호출하여 사용 하는 로직이 있었다.
@Embeddable
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class StartingDate {
@Column(name = "starting_date", nullable = false)
private LocalDate startingDate;
public StartingDate(final LocalDate startingDate) {
validateDate(startingDate);
this.startingDate = startingDate;
}
private void validateDate(final LocalDate startingDate) {
if (**LocalDate.now().isAfter(startingDate)**) {
throw new IllegalArgumentException("이미 지난 날짜는 활동 시작일로 할 수 없습니다.");
}
}
public boolean isPassed() {
return startingDate.isBefore(LocalDate.now());
}
테스트 코드는 다음과 같이 작성 하였다.
@ParameterizedTest
@MethodSource("correctStartingDateProvider")
@DisplayName("활동 시작일 입력받아 객체를 생성한다. 활동 시작일은 과거의 날짜가 될 수 없다.")
void create(final LocalDate startingDate) {
//when, then
assertThatCode(() -> new StartingDate(startingDate)).doesNotThrowAnyException();
}
private static Stream<Arguments> correctStartingDateProvider() {
return Stream.of(
Arguments.of(LocalDate.of(2023, 2, 16)),
Arguments.of(LocalDate.of(2023, 2, 17)));
}
이렇게 되니 문제점이 하나 발생 했다.
작성일 기준 현재 날짜
는 2023년 2월 16일
이다. 현재는 해당 테스트코드가 문제 없이 통과
한다.
하지만 내일인 2023년 2월 17일
이 된다면 startingDate
가 2023년 2월 16일
인 경우 테스트는 실패한다.
하루가 더 지나면 모든 경우가 다 실패한다.
테스트를 돌릴 때마다 결과가 다르면 신뢰도가 전혀 없는 테스트라고 생각하기 때문에, 해당 테스트 코드가 제대로 된 역할을 한다고 말할수가 없었다.
그래서 개선 해보기로 하였다.
현재 StartingDate 클래스 내부에서 LocalDate.now() 를 사용하기 때문에, 테스트 코드에서는 해당 로직에 개입을 할 수가 없었다.
따라서 외부에서 now 를 주입 받아서 사용을 하면 어떨까?
라는 생각으로 접근을 해보았다.
@Embeddable
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class StartingDate {
@Column(name = "starting_date", nullable = false)
private LocalDate startingDate;
public StartingDate(final LocalDate **currentDate**, final LocalDate startingDate) {
validateDate(currentDate, startingDate);
this.startingDate = startingDate;
}
private void validateDate(final LocalDate **currentDate**, final LocalDate startingDate) {
if (currentDate.isAfter(startingDate)) {
throw new IllegalArgumentException("이미 지난 날짜는 활동 시작일로 할 수 없습니다.");
}
}
...
}
StartingDateTest
private final LocalDate current = LocalDate.of(2023, 2, 16);
@Test
@DisplayName("과거의 활동 시작일 입력 시, 예외를 반환한다.")
void validateDate() {
//given
LocalDate pastStartingDate = LocalDate.of(2023, 2, 15);
//when,then
assertThatThrownBy(() -> new StartingDate(current, pastStartingDate)).isInstanceOf(IllegalArgumentException.class)
.hasMessage("이미 지난 날짜는 활동 시작일로 할 수 없습니다.");
}
이처럼 LocalDate.now()
에 해당하는 값을 current
로 빼주고, 테스트 코드에서 명시한 날짜로 테스트를 하면 해당 테스트 코드는 시간이 지나도 무조건 통과를 한다.
하지만 StartingDate
의 구현 코드에서 currentDate
에 해당하는 인자를 받기위해 Board
에서는 LocalDate.now()
를 호출해주어야 했다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Board {
//...
@Embedded
private StartingDate startingDate;
//...
public Board(final Member member, final LocalDate startingDate, final String activityCategory, final String title, final String content) {
this.member = member;
**this.startingDate = new StartingDate(LocalDate.now(), startingDate);**
this.activityCategory = ActivityCategory.from(activityCategory);
this.title = new Title(title);
this.content = new Content(content);
}
이렇게 되면 BoardTest
에서도 LocalDate.now()
때문에 테스트에 대한 신뢰성이 떨어지는 문제가 동일하게 발생하게된다.
LocalDate.now()
에 대한 값을 주입을 받더라도 결국 현재 시간을 받기위해서 LocalDate.now()
를 사용을 해야하니 테스트를 할수 없는 부분이 발생하게 되었다.
그래서 테스트 코드에서 LocalDate.now() 를 모킹하면 되지 않을까? 라고 생각하였고 모킹을 해보려 했으나,,,,
LocalDate.now() 는 스태틱 메서드이기 때문에 일반적인 방법으로 모킹 할 수가 없었다.
LocalDate 를 목 객체로 만들어도 스태틱인 now 메서드를 꺼내쓸 수 없기 때문!
검색을 좀 해보니 LocalDate 를 static mock 으로 모킹을 할 수 있다고 한다. 하지만 그러면 관련 라이브러리를 주입을 해줘야했다. 무엇보다 static mock 을 사용하면 테스트 코드 작성 방식이 복잡해지고, try 문으로 예외를 잡아야 하기 때문에 복잡해진다고 생각을 했다.
그래서 좀 더 방법을 찾아 보던중 Clock
빈을 등록해 모킹하는 방법이 있었다.
Clock 을 왜? 모킹하고 Clock 이 뭔지 몰라 더 찾아보았다.
LocalDate 의 now()를 살펴보면
//LocalDate
public final class LocalDate implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable {
//...
public static LocalDate now() {
return now(Clock.systemDefaultZone());
}
public static LocalDate now(ZoneId zone) {
return now(Clock.system(zone));
}
public static LocalDate now(Clock clock) {
Objects.requireNonNull(clock, "clock");
final Instant now = clock.instant(); // called once
return ofInstant(now, clock.getZone());
}
}
//...
}
위와 같이 LocalDate 에서는 Clock.systemDefaultZone
을 이용해 현재 날짜를 반환한다.
또한 public static LocalDate now(Clock clock)
을 보면 Clock
객체를 받아 그에 해당하는 LocalDate
를 반환한다.
따라서 Clock
을 빈으로 등록하고, 해당 Clock
을 구현 코드와 테스트 코드에서 각각 사용하여 , LocalDate.now()
를 사용하던 부분을 LocalDate.now(Clock clock)
로 변경을 해준다면 시간을 컨트롤 할 수 있을 것이다.
@Configuration
public class TimeConfig {
@Bean
public Clock clock() {
return Clock.systemDefaultZone();
}
}
이후 LocalDate.now() 를 사용하는 로직에서는 public static LocalDate now(Clock clock)
를 사용하고, 테스트 코드 시 시간에 대한 컨트롤이 필요하다면 clock 를 모킹하여 사용 할 수 있다.
우선 서비스 레이어에 빈으로 등록한 Clock 에 대한 의존성을 주입하였고, Board 생성 시, 해당 시간을 기준으로 LocalDate.now(Clock)를 호출 하도록 해줬다.
정리하자면 BoardService 레이어에는 Clock 을 주입받아 LocalDate.now(clock) 형태로 현재 날짜를 생성하고, 이 현재 날짜가 필요한 도메인는 BoardService 에서 넘겨준 LocalDate 를 사용하게 했다.
코드로 보자..
public class BoardService {
**private final Clock clock;**
private final MemberService memberService;
private final BoardRepository boardRepository;
@Transactional
public void write(final Long memberId, final BoardCreateRequest boardCreateRequest) {
Member member = memberService.findByMemberId(memberId);
**LocalDate now = LocalDate.now(clock);**
Board board = new Board(member, boardCreateRequest.getStartDate(), boardCreateRequest.getActivityCategory(),
boardCreateRequest.getTitle(), boardCreateRequest.getContent(), **now**);
boardRepository.save(board);
}
public class Board {
//...
@Embedded
private StartingDate startingDate;
public Board(final Member member, final LocalDate startingDate, final String activityCategory, final String title, final String content, final LocalDate now) {
this.member = member;
this.startingDate = new StartingDate(now, startingDate);
this.activityCategory = ActivityCategory.from(activityCategory);
this.title = new Title(title);
this.content = new Content(content);
}
//...
}
public class StartingDate {
@Column(name = "starting_date", nullable = false)
private LocalDate startingDate;
public StartingDate(final LocalDate currentDate, final LocalDate startingDate) {
validateDate(currentDate, startingDate);
this.startingDate = startingDate;
}
private void validateDate(final LocalDate currentDate, final LocalDate startingDate) {
if (currentDate.isAfter(startingDate)) {
throw new IllegalArgumentException("이미 지난 날짜는 활동 시작일로 할 수 없습니다.");
}
}
public boolean isPassed(final LocalDate now) {
return startingDate.isBefore(now);
}
이에 따라서 각각의 테스트 코드를 수정해주었다.
서비스 레이어 에서는 LocalDate.now()
를 사용하는 메서드 대한 테스트를 해야한다면 주입하는 Clock
을 모킹하면된다.
도메인 레벨에서는 외부에서 생성한 LocalDate
를 주입받기 때문에, //given
으로 LocalDate
를 생성해서 넣어주면 될 것이다.
LocalDate.now 을 Board 엔티티 내부에서 사용하는 것이 아니기 때문에 외부에서 지정한 시간을 넣어주어 테스트를 할 수 있다.
LocalDate now = LocalDate.of(2023, 11, 12);
처럼 현재 시간을 임의로 지정해줄 수 있다.
class BoardTest {
@Test
@DisplayName("게시판의 member(작성자), title, content 를 반환한다.")
void getter() {
//given
SoftAssertions softly = new SoftAssertions();
LocalDate now = LocalDate.of(2023, 11, 12);
Board board = new Board(testMember, LocalDate.of(2025, 2, 11),
"달리기", "게시판 제목", "게시판 내용 작성 테스트", now);
//when
Title title = board.getTitle();
Content content = board.getContent();
ActivityCategory activityCategory = board.getActivityCategory();
StartingDate startingDate = board.getStartingDate();
//then
softly.assertThat(activityCategory).isSameAs(ActivityCategory.RUNNING);
softly.assertThat(startingDate).isEqualTo(new StartingDate(LocalDate.of(2023, 11, 11),
LocalDate.of(2025, 2, 11)));
softly.assertThat(title).isEqualTo(new Title("게시판 제목"));
softly.assertThat(content).isEqualTo(new Content("게시판 내용 작성 테스트"));
softly.assertAll();
}
}
private final LocalDate current = LocalDate.of(2023, 2, 16);
@ParameterizedTest
@MethodSource("correctStartingDateProvider")
@DisplayName("활동 시작일 입력받아 객체를 생성한다. 활동 시작일은 과거의 날짜가 될 수 없다.")
void create(final LocalDate startingDate) {
//when, then
assertThatCode(() -> new StartingDate(current, startingDate)).doesNotThrowAnyException();
}
private static Stream<Arguments> correctStartingDateProvider() {
return Stream.of(
Arguments.of(LocalDate.of(2023, 2, 16)),
Arguments.of(LocalDate.of(2023, 2, 17)));
}
이제 테스트 코드에서도 now() 에 대한 컨트롤을 할 수 있고 테스트의 신뢰도를 올릴 수 있게되었다!