테스트 코드에서 LocalDate.now() 제어하기

최지환·2023년 5월 23일
0

졸업작품-동네줍깅

목록 보기
3/11
post-thumbnail

구현 코드 내부에서 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일이 된다면 startingDate2023년 2월 16일 인 경우 테스트는 실패한다.

하루가 더 지나면 모든 경우가 다 실패한다.

테스트를 돌릴 때마다 결과가 다르면 신뢰도가 전혀 없는 테스트라고 생각하기 때문에, 해당 테스트 코드가 제대로 된 역할을 한다고 말할수가 없었다.

그래서 개선 해보기로 하였다.

1차 시도 - now 에 대한 로직을 다른 객체에 위임.

현재 StartingDate 클래스 내부에서 LocalDate.now() 를 사용하기 때문에, 테스트 코드에서는 해당 로직에 개입을 할 수가 없었다.

따라서 외부에서 now 를 주입 받아서 사용을 하면 어떨까? 라는 생각으로 접근을 해보았다.

StartingDate

@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() 를 사용을 해야하니 테스트를 할수 없는 부분이 발생하게 되었다.


두번째 시도 - mocking 하기

그래서 테스트 코드에서 LocalDate.now() 를 모킹하면 되지 않을까? 라고 생각하였고 모킹을 해보려 했으나,,,,

LocalDate.now() 는 스태틱 메서드이기 때문에 일반적인 방법으로 모킹 할 수가 없었다.

LocalDate 를 목 객체로 만들어도 스태틱인 now 메서드를 꺼내쓸 수 없기 때문!

검색을 좀 해보니 LocalDate 를 static mock 으로 모킹을 할 수 있다고 한다. 하지만 그러면 관련 라이브러리를 주입을 해줘야했다. 무엇보다 static mock 을 사용하면 테스트 코드 작성 방식이 복잡해지고, try 문으로 예외를 잡아야 하기 때문에 복잡해진다고 생각을 했다.

그래서 좀 더 방법을 찾아 보던중 Clock 빈을 등록해 모킹하는 방법이 있었다.

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) 로 변경을 해준다면 시간을 컨트롤 할 수 있을 것이다.

Clock 모킹하기

clock을 Bean 으로 하기

@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 를 사용하게 했다.

코드로 보자..

BoardService

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);
    }

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);
    }
		//...
}

StartingDate - 기존과 동일하다.

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 를 생성해서 넣어주면 될 것이다.

BoardTest

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();
    }
}

StartingDateTest - 기존과 동일

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() 에 대한 컨트롤을 할 수 있고 테스트의 신뢰도를 올릴 수 있게되었다!

0개의 댓글