기존에 우리는 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
@Repository
public class MemberRepository {
private final Map<Long, String> store = new ConcurrentHashMap<>();
private Long sequence = 0L;
public String save(Member member) {
...
}
}
MemberRepositoryTest
@SpringBootTest
public class MemberRepositoryTest {
private final MemberRepository memberRepository;
public MemberRepositoryTest(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Test
void saveTest(){
...
}
}
- 다음과 같이 MemberRepository와 MemberRepository를 테스트하는 MemberRepositoryTest가 있다고 해보겠습니다.
- 테스트 코드 내에서 생성자 주입을 통해 DI를 수행했고, 결과는 ?
org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter
🤔 왜 이런 현상이 발생하는 것일까요 ?
- 처음에는 Spring Framework가 Test 코드의 의존성 주입을 알아서 진행해주는 것으로 알고 있었습니다.
- 따라서 생성자가 하나일 때는 @Autowired를 생략해도 DI가 잘 이루어진다고 생각했습니다.
- 그러나, 스프링이 아닌 JUnit Engine(Jupiter)의 Parameter Resolver에 의하여 의존성 주입이 되는 것이었습니다.
- 이를 이해하기 위해 , JUnit의 구조를 파악해보도록 하겠습니다.
JUnit의 구조
- JUnit5는 JUnit Platform + JUnit Jupiter + JUnit Vintage 로 이루어져 있습니다.
JUnit Platform
: JVM 에서 테스트 프레임워크를 실행하는데 기초를 제공 및 TestEngine API를 제공해 테스트 프레임워크 개발 기능
JUnit Jupiter
: JUnit 5에서 테스트를 작성하고 확장하기 위한 새로운 프로그래밍 모델과 확장 모델의 조합
JUnit Vintage
: 화위 호환성 (JUnit 3, 4 버전)을 위한 테스트 엔진 제공
-> JUnit 5 부터는 생성자 및 메소드 내 파라미터를 이용할 수 있게 되었습니다. (이전 버전들은 X) 이로 인하여, 코드의 유연성 및 생성자와 메소드에 의존성 주입이 가능하게 되었죠.
Paramter Resolver는 ?
- org.junit.jupiter.api.extension에 ParamterResolver Interface가 존재한다.
그럼 도대체 왜 ? @Autowired 인데 ?
- 어댑터 패턴으로 구현된 Paramter Resolver Interface
- 그것을 구현하고 있는 SpringExtension class
- 그것을 어노테이션으로 갖고 있는 @SpringBootTest(@ExtendWith(SpringExtension.class))
- @SpringBootTest의 supportsParameter 메소드 (구현부)
[ 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 를 체크한다.
-> 따라서 생성자에 @Autowired가 붙어있지 않다면 에러가 발생하는 것이죠.
정리
- @SpringBootTest를 통해 SpringExtension.class를 가져온다.
- SpringExtension.class에는 JUnit Jupiter API의 ParameterResolver를 구현한 부분이 존재한다.
- 이 구현부를 통해 @Autowired 어노테이션 여부와 생성자 여부를 확인하고 의존성 주입을 진행한다.
< 참고 >
JUnit 5 Test가 생성자 의존성 주입을 하는 방법
JUnit 5 Test가 의존성 주입을 하는 방법
velog.io
JUnit5 완벽 가이드
시작하기전
donghyeon.dev