JUnit 5 Test가 생성자 의존성 주입을 하는 방법

nathan·2022년 5월 13일
8

Spring

목록 보기
4/4
post-custom-banner

도입부

  • 원래는 우리가 Spring Framework에서 의존성 주입(DI)을 하게 될 때 생성자가 하나라면, @Autowired를 생략해도 됐는데, JUnit으로 @SpringBootTest 테스트 코드를 작성할 때 같은 방식으로 하면 다음과 같은 에러가 발생한다.
org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter [nathan.test.repository.MemberRepository memberRepository] in constructor [public nathan.test.MemberRepositoryTest(nathan.test.repository.MemberRepository)].

예시

  • MemberRepository

    @Slf4j
    @Repository
    public class MemberRepository {
    
    	private final Map<Long, String> store = new ConcurrentHashMap<>();
    	private Long sequence = 0L;
    
    	public String save(Member member) {
    		member.setId(sequence++);
    		store.put(member.getId(), member.getName());
    		log.info("[Repository] member save: {} - {}", member.getId(), member.getName());
    		return member.getName();
    	}
    }
  • MemberRepositoryTest

    @SpringBootTest
    public class MemberRepositoryTest {
    
    	private final MemberRepository memberRepository;
    
    	@Autowired
    	public MemberRepositoryTest(MemberRepository memberRepository) {
    		this.memberRepository = memberRepository;
    	}
    
    	@Test
    	void saveTest(){
    	    //given
    		Member member = new Member();
    		member.setName("nathan");
    
    	    //when
    		String savedMemberName = memberRepository.save(member);
    
    	    //then
    		Assertions.assertThat(savedMemberName).isEqualTo("nathan");
    	}
  • 여기에서 @Autowired를 빼면 맨위에서 설명했던 org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter 에러가 발생한다.


왜 이런 현상이 발생하는 것일까?


JUnit 구조

  • JUnit5는 다음과 같이 이루어져 있다.

    JUnit Platform + JUnit Jupiter + JUnit Vintage

    • JUnit Platform : JVM에서 테스트 프레임워크를 실행하는데 기초를 제공 및 TestEngine API를 제공하여 테스트 프레임워크 개발 가능
    • JUnit Jupiter : JUnit 5에서 테스트를 작성하고 확장하기 위한 새로운 프로그래밍 모델과 확장 모델의 조합
    • JUnit Vintage : 하위 호환성(JUnit 3, 4 버전)을 위한 테스트 엔진 제공
  • JUnit5부터는 생성자 및 메소드 내 파라미터를 이용할 수 있게 되었다.(이전 버전들은 안됐다.)
    • 이로 인하여, 코드의 유연성 및 생성자와 메소드에 의존성 주입이 가능케 되었다.

구조는 대강 알겠다. 근데, Parameter Resolver는?

  • Parameter Resolver의 위치
    • org.junit.jupiter.api.extension에 ParameterResolver Interface가 존재한다.
    • Jupiter Engine은 해당 ParameterResolver를 3가지로 구현해놓았다.

아니 Parameter Resolver가 중요하다며?

  • 어댑터 패턴으로 구현된 Parameter Resolver Interface

  • 그것을 구현하고 있는 SpringExtension class

  • 그것을 애노테이션으로 갖고 있는 @SpringBootTest (@ExtendWith({SpringExtension.class}))

  • @SpringBootTest의 supportsParameter 메서드 (구현부)

     public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
         Parameter parameter = parameterContext.getParameter();
         Executable executable = parameter.getDeclaringExecutable();
         Class<?> testClass = extensionContext.getRequiredTestClass();
         PropertyProvider junitPropertyProvider = (propertyName) -> {
             return (String)extensionContext.getConfigurationParameter(propertyName).orElse((Object)null);
         };
         return TestConstructorUtils.isAutowirableConstructor(executable, testClass, junitPropertyProvider) || ApplicationContext.class.isAssignableFrom(parameter.getType()) || this.supportsApplicationEvents(parameterContext) || ParameterResolutionDelegate.isAutowirable(parameter, parameterContext.getIndex());
     }
    • TestConstructorUtils.isAutowirableConstructor를 통하여 @Autowired를 체크한 뒤 의존성 주입을 한다.

          public static boolean isAutowirableConstructor(Executable executable, Class<?> testClass, @Nullable PropertyProvider fallbackPropertyProvider) {
        return executable instanceof Constructor && isAutowirableConstructor((Constructor)executable, testClass, fallbackPropertyProvider);
      }
    • 이 글의 핵심!

          public static boolean isAutowirableConstructor(Constructor<?> constructor, Class<?> testClass, @Nullable PropertyProvider fallbackPropertyProvider) {
        if (AnnotatedElementUtils.hasAnnotation(constructor, Autowired.class)) {
            return true;
        } else {
            AutowireMode autowireMode = null;
            TestConstructor testConstructor = (TestConstructor)TestContextAnnotationUtils.findMergedAnnotation(testClass, TestConstructor.class);
            if (testConstructor != null) {
                autowireMode = testConstructor.autowireMode();
            } else {
                String value = SpringProperties.getProperty("spring.test.constructor.autowire.mode");
                autowireMode = AutowireMode.from(value);
                if (autowireMode == null && fallbackPropertyProvider != null) {
                    value = fallbackPropertyProvider.get("spring.test.constructor.autowire.mode");
                    autowireMode = AutowireMode.from(value);
                }
            }
      
            return autowireMode == AutowireMode.ALL;
        }
      }
      • 맨 위의 hasAnnotation을 통하여 @Autowired를 체크한다.

흐름 정리

    1. @SpringBootTest를 통해 SpringExtension.class를 가져온다.
    1. SpringExtension.class에는 JUnit Jupiter API의 ParameterResolver를 구현한 부분이 존재한다.
    1. 이 구현부를 통하여 @Autowired 애노테이션 여부와 생성자 여부를 확인한 후 의존성 주입을 진행한다.

Reference

profile
나는 날마다 모든 면에서 점점 더 나아지고 있다.
post-custom-banner

1개의 댓글

comment-user-thumbnail
2023년 10월 31일

JUnit 5 동작 원리에 대해 공부중이었는 데, 잘 보고 갑니다 :)

답글 달기