LocalDateTime 조롱하기

pood-tech·2023년 2월 8일
0

안녕하세요 푸드 테크팀 백엔드 개발자 박형민입니다 :)

👏🏻 최근 프로젝트에서 특정일자와 오늘날짜의 차이를 구하는 로직을 구현해야했었는데, 이 날짜 로직에 LocalDate.now() 가 포함되어있어 오늘만 통과하는 테스트코드를 작성하게되어 곤란함이 있었습니다.


그래서 이번 포스팅에서는 제가 mocking 할 수 없는, LocalDateTime.now( ) 메소드를 어떻게 mocking 했는지에 대해 작성해보고자 합니다.


1) LocalDate.now() 테스트하기

  • 먼저 성공하는 테스트코드를 작성해 보았습니다.
  • 포스팅 작성일자 기준, 오늘은 2023-2-8일 이기 때문에, 아래 코드는 now() 메소드가 성공하는 테스트 코드입니다.
  • 📌 하지만 이렇게 작성한다면, 내일은 실패하는 테스트 코드가 되고 맙니다.
    → 테스트코드는 항상 성공해야한다는 법칙이 깨지는 것이죠

LocalDate Mocking

하지만 저희에게는 mocking 이라는 훌룡한 조롱수단이 있습니다.
LocalDate 객체를 mocking 하고 now() 메소드에 원하는 값을 반환시켜주면 항상 성공하는 테스트 케이스가 작성되겠죠

하지만 실패합니다.

  • 그 이유는, LocalDate 객체는 final 로 선언된 객체이며, now() 메소드 또한 정적 메소드이기 때문입니다.



2) 그럼에도 불구하고 LocalDate 를 조롱하는 방법들

그럼에도 불구하고 LocalDate 의 정적 메소드인 now()를 mocking 할 수 있는 몇가지 방법들이 존재합니다.


1. Power Mockito

가장 쉬운 방법입니다.

수많은 블로그나, 레펀런스를 보면 보통 Mockito-inline 을 dependecy 에 추가해주어야한다고 이야기합니다.

  1. mockito 3.4.0 전에는 powermock 라이브러리를 따로 추가해주어야했지만, 그 이후 버전부터는 mockito 에 추가되었습니다.
  2. maven 혹은 gradle 에 mockito-inline 이나 mockito-core를 종속성 추가해주면 Mockito.mockStatic() 메소드를 사용할 수 있게됩니다.
  3. 이름 그대로 정적 메소드를 mocking하기 위해 만든 메소드입니다.
//이런식으로 mocking 이 가능해집니다.
MockedStatic<LocalDate> localDateMockedStatic = Mockito.mockStatic(LocalDate.class);
    
localDateMockedStatic.when(LocalDate::now).thenReturn(date);

🔥 하지만, PowerMock을 지양해야하는 몆가지 이유가 존재합니다.

  1. 테스트코드 작성 방식이 복잡하고 try문으로 감싸거나 명시적으로 Mocking 정적객체를 close() 하지 않으면 계속해서 해당 스레드에서는 정적객체 mocking 활성상태가 유지됩니다.
  2. 특정 라이브러리를 의존해야하고 이 과정에서 일부 라이브러리와 호환되지 않을 수 도 있습니다.
  3. static mock은 안티패턴입니다.
    • static method를 모킹하기 위해 PowerMock이나 mockito-inline 라이브러리를 설치하면 private 메서드, final 클래스 등 테스트 가능한 상태가 됩니다.
    • http://shoulditestprivatemethods.com/ 라는 사이트가 있을 정도로 private method의 테스트 불필요성에 대해선 많은 자료가 있으니 허용하지 않는 상태로 두는 것이 좋습니다.



2. Class 로 감싸기

두번째 방법은 정적 메소드를 커스텀한 class 로 감싸는 방법입니다.

final 클래스라 mocking 이 불가능하고, 정적 메소드라 mocking 이 불가능하다면 새로운 객체에 해당 기능만을 담아 만들어 mocking 하는 방법도 있습니다.


👏🏻 간단하게 LocalDate.now() 를 감싼 객체를 만들었습니다.

public class DateUtil {

    public LocalDate now() {
        return LocalDate.now();
    }
}
  1. 이제 이 객체를 mocking 하여 LocalDate.now() 가 아닌 DateUtil.now() 를 mocking 합니다.

  1. 결과를 보시면 내부적으로 LocalDate.now() 를 호출하여 현재 날짜를 반환하게 하는 DateUtil.now() 가 27년 뒤를 반환하고 있습니다.

👍 Power Mock 을 사용하지도 않았고, 외부 라이브러리에 종속적이지도 않습니다 :)


🙅‍♂️ 하지만 이 방법은 저만을 위한 객체가 될 수 있기 때문에, 협업을 하는 상황에서 좋지 않다고 생각했습니다.

  • 이 객체를 모르는 팀원은 또 다시 power mock 이든 새로운 객체를 생성하든 할 수 있기때문에, 조금더 범용적인 방법을 고민했습니다.


3. LocalDate.now(Clock clock) 사용하기

✔️ 마지막 방법이고 채택한 방법입니다.

진짜 오랫동안 LocalDate.now() 메소드를 째려보니(?) 1가지 묘한 점을 발견할 수 있었습니다.

static 메소드인 LocalDate.now() 메소드가 내부적으로 LocalDate.now(Clock clock) 메소드를 호출하면서 Clock 객체를 파라미터로 주입하고있었고

Clock 객체는 final class 도 아니면, now() 에서 호출하고잇는 instance() 메소드도 static 메소드가 아니였습니다.

👏🏻 즉! Clock 객체를 mocking 할 수 있다!

코드로 적용해본다면

  1. 항상 Clock.systemDefaultZone() 을 매번 주입하기는 힘들고, 위험성이 존재하니 Clock을 Bean으로 올려 관리하도록하고,
  2. 호출하는 부분에서 DI 로 주입한다면 종속성도 끊어내 테스트 코드 작성에도 유용해집니다.

테스트 코드


  • Clock 객체를 mocking 하기 위해서는 결국 호출하는 프로젝션 코드를 변경할 수 밖에 없습니다.

  • 하지만, Oracle에서 테스트를 위해 Clock을 받아 사용하는 목적으로 사용하라고 명시되어있어서 쿨하게 넘어가기로 했습니다.

This will query the specified clock to obtain the current date - today. Using this method allows the use of an alternate clock for testing. The alternate clock may be introduced using dependency injection.



이렇게 정적 메소드인 LocalDate.now() 메소드를 mocking 하는 방법에 대해서 알아보았습니다.


더 좋은 방법과 질문이 있으시다면 댓글 언제나 환영이고
✨ 긴글 읽어주셔서 감사합니다! ✨

0개의 댓글