👩🏻‍💻 Programming/SpringBoot

@PostConsturct와 테스트 코드

한국의 메타몽 2024. 11. 23. 19:51
@Component  
@RequiredArgsConstructor  
public class ChangeMoneyUseCaseFactory {  

  private final List<ChangeMoneyUseCase> changeMoneyUseCases;  

  private EnumMap<LevelType, ChangeMoneyUseCase> changeMoneyUseCaseMap;  

  @PostConstruct  
  private void init() {  
    changeMoneyUseCaseMap = changeMoneyUseCases.stream()  
        .collect(Collectors.toMap(ChangeMoneyUseCase::getLevelType, Function.identity(), (existing, replacement) -> existing,  
            () -> new EnumMap<>(LevelType.class)));  
  }  

  public ChangeMoneyUseCase changeMoneyUseCaseOf(LevelType levelType) {  
   return changeMoneyUseCaseMap.getOrDefault(levelType, null);  
  }  

}

위와 같은 factory 클래스가 있었다.
보다시피 해당 factory 클래스의 init() 메서드는 @PostConstruct을 통해 초기화가 이루어지고 있다.
그러다보니 테스트 코드 작성 중 아래의 이슈들과 맞닥뜨렸다.

 

  • @PostConstructSpring Context에서만 동작
  • 따라서 Mockito를 사용하여 @InjectMocks로 의존성을 주입하고 순수 자바 객체로 테스트를 진행할 때 @PostConstruct으로 선언된 로직은 자동으로 실행되지 못함

 

그래서 일반적인 @InjectMocks <-> @Mock을 활용한 테스트 코드를 작성했을때 changeMoneyUseCaseOf 메서드를 호출해도 changeMoneyUseCaseMap 자체가 제대로 초기화되지 못한 현상이 발생했다.

그래도 테스트 코드는 작성해야 성에 찰 것 같다.
방법은 크게 2가지가 떠올랐다.

 

  1. init() 초기화 메서드의 접근제어자를 public으로 변경하여 테스트 코드에서 init()을 호출하기
  2. Reflection으로 init() 메서드에 접근하기

 

아무라봐도 테스트 코드를 위해 비즈니스 로직의 접근제어자를 private -> public으로 바꾸는건 아닌 것 같다.
한편으로는 private으로 선언된 메서드로 어떻게든 테스트 코드를 작성하는게 맞는지 의문이 들기도 하지만,
그래도 테스트 코드는 작성해야 마음 놓고 PR을 올릴 수 있을것 같으니, 우선 비즈니스 로직을 변경하지 않는 2안으로 진행하기로 결정했다.

@BeforeEach  
void setUp() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {  
  // ChangeMoneyUseCase를 상속받는 각 service 레이어로 값 초기화
  given(vipChangeMoneyService.getLevelType()).willReturn(LevelType.VIP); 
  // .. 다른 멤버십 서비스도 동일하게 작성

  List<ChangeMoneyUseCase> useCases = List.of(vipChangeMoneyService, ...);  

  target = new ChangeMoneyUseCaseFactory(useCases);  

  // Reflection으로 private 메서드 접근  
  Method init = ChangeMoneyUseCaseFactory.class.getDeclaredMethod("init");  
  init.setAccessible(true);  
  init.invoke(target);  
}