📕 목차
1. 개요
2. @InjectMocks
3. 결론
1. 개요
📌 기존 서비스 계층 단위 테스트 방식
@ExtendWith(MockitoExtension.class)
public FooServiceTest {
private FooService fooService;
@Mock
private BarServcie barService;
@BeforeEach
void setUp() {
fooService = new FooService(barService);
}
...
}
테스트 타겟이 FooService 뿐이라면, BarService를 Mock 객체로 등록하고, FooService를 테스트마다 생성한다.
BarService의 로직이 호출되어야 하는 경우엔 BDDMockito의 given 메서드를 사용해 응답값을 조작하면 된다.
🤔 정말 이 방법이 옳은가?
물론 테스트를 하면서 문제가 됐던 건 아니지만, 혹시나 테스트 도구를 제대로 다루지 못하고 있는 거라면?
위의 기능을 도와주는 도구가 없을 거라고는 생각이 들지 않았다.
그래서 mockito 라이브러리의 어떤 어노테이션을 사용하면, 위와 동일한 동작을 수행할 수 있는 지 알아보았다.
2. @InjectMocks
📌 InjectMocks
@ExtendWith(MockitoExtension.class)
public FooServiceTest {
@Mock
private BarServcie barService;
@InjectMocks
private FooService fooService;
...
}
mockito에는 InjectMocks이라는 어노테이션이 존재했고, 타겟 서비스에 선언해주는 것만으로도 기존과 완전히 동일하게 사용할 수 있다.
@Mock, @Spy로 정의해둔 껍데기들을 @InjectMocks으로 선언한 클래스에 알아서 주입해줌으로써, 비지니스 로직을 검사할 수 있게 도와준다.
🤔 FooService도 Mock인데, 어떻게 비지니스 로직 검사가 가능한 거지?
이게 정말 의문이었는데, 'InjectMocks으로 선언한 클래스도 다른 Mock 객체를 주입받은 Mock 객체가 아닌가?'였다.
그런데 어떻게 비지니스 로직 테스트가 가능한지 의문이었는데, @InjectMocks으로 선언한 클래스는 Mock이 아니라 실제 인스턴스를 생성한다고 한다.
@InjectMocks 어노테이션의 주석만 살펴봐도 실제 인스턴스를 생성하기 위해 생성자를 가장 우선적으로 고려하고, 그 다음으로 setter, 그래도 안 되면 필드 주입으로 의존성을 주입하여 실제 인스턴스를 생성한다고 한다.
📌 내부 동작 파헤쳐보기
사실 이거 하고 싶어서 포스팅 함. ㅎㅎ
처음에 decompiled 클래스가 없어서 AOP가 어디서 동작하는 지 찾을 수 없었는데, 갑자기 IDE에서 소스 다운할 거냐는 메시지가 뜨길래 ok 눌렀더니 소스 코드 확인이 가능해졌다.
처음에 InjectMocks 어노테이션을 스캔하는 클래스를 찾으니 InjectMocksScanner가 나왔고, 다시 InjectMocksScanner를 사용하는 클래스를 검색하여 InjectAnnotationEngine이라는 클래스를 찾아냈다.
그리고 열자마자 "첫 번째로 Mocks, Spies, Captors를 생성하고, 그들을 주입한다."라는 주석을 읽었다.
아아, 너구나?
@Override
public AutoCloseable process(Class<?> clazz, Object testInstance) {
List<AutoCloseable> closeables = new ArrayList<>();
closeables.addAll(processIndependentAnnotations(testInstance.getClass(), testInstance));
closeables.add(injectCloseableMocks(testInstance));
return () -> {
for (AutoCloseable closeable : closeables) {
closeable.close();
}
};
}
유일한 공개 메서드인 process()에서 closeables 리스트를 추가하고 있다.
여기서 내가 궁금한 건 injectCloseableMocks() 쪽이므로, 해당 메서드를 따라가봤다.
/**
* Initializes mock/spies dependencies for objects annotated with
* @InjectMocks for given testClassInstance.
* <p>
* See examples in javadoc for {@link MockitoAnnotations} class.
*
* @param testClassInstance
* Test class, usually <code>this</code>
*/
private AutoCloseable injectCloseableMocks(final Object testClassInstance) {
Class<?> clazz = testClassInstance.getClass();
Set<Field> mockDependentFields = new HashSet<>();
Set<Object> mocks = newMockSafeHashSet();
while (clazz != Object.class) {
new InjectMocksScanner(clazz).addTo(mockDependentFields);
new MockScanner(testClassInstance, clazz).addPreparedMocks(mocks);
onInjection(testClassInstance, clazz, mockDependentFields, mocks);
clazz = clazz.getSuperclass();
}
new DefaultInjectionEngine()
.injectMocksOnFields(mockDependentFields, mocks, testClassInstance);
return () -> {
for (Object mock : mocks) {
if (mock instanceof ScopedMock) {
((ScopedMock) mock).closeOnDemand();
}
}
};
}
protected void onInjection(
Object testClassInstance,
Class<?> clazz,
Set<Field> mockDependentFields,
Set<Object> mocks) {}
코드를 잘 써놔서 이해하는데 그리 어렵지 않다.
- 클래스와 필드 집합 초기화
- 테스트 클래스의 런타입 정보를 .getClass()로 획득
- Mock 객체 주입이 필요한 필드를 저장하기 위해 mockDependentFields 인스턴스 생성
- 생성된 Mock 객체를 저장할 mocks 인스턴스 생성
- 클래스 계층 구조 순회하면서 필드와 Mock 객체 스캔 (while 문)
- 테스트 클래스 내의 @InjectMocks 주석이 달린 필드를 mockDependentFields HashSet에 추가
- 테스트 클래스 내의 Mock 객체들을 mocks HashSet에 추가
- onInjection() : 왜 있는 거지...? 일단 지금 당장은 아무런 역할도 안 한다. Injection 관계가 올바른지 유효성 검사라도 하려고 했던 거 같기도.
- Object 클래스가 나올 때까지 슈퍼 클래스로 이동하여 (2)번 반복
- Mock 객체 주입
- Mock 객체를 수집된 필드에 주입한다. (생성자 먼저 주입한다고 해놓고 왜 Field 주입이냐 싶었는데, 내부 구현을 보면 그렇지 않음을 알 수 있다. 아래에서 언급)
- AutoCloseable 반환
- mocks 집합의 scopedMock 객체의 closeOnDemand 메서드 호출
그럼 여기서 InjectMocksScanner, MockScanner, DefaultInjectionEngine의 역할만 알면, 내부적으로 어떻게 동작하는 지 확실히 알 수 있다.
📌 InjectMocksScanner
정직하게 InjectMocks 어노테이션이 붙은 클래스를 탐색한다.
private Set<Field> scan() {
Set<Field> mockDependentFields = new HashSet<>();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (null != field.getAnnotation(InjectMocks.class)) {
assertNoAnnotations(field, Mock.class, Captor.class);
mockDependentFields.add(field);
}
}
return mockDependentFields;
}
private static void assertNoAnnotations(
Field field, Class<? extends Annotation>... annotations) {
for (Class<? extends Annotation> annotation : annotations) {
if (field.isAnnotationPresent(annotation)) {
throw unsupportedCombinationOfAnnotations(
annotation.getSimpleName(), InjectMocks.class.getSimpleName());
}
}
}
뭐..그냥 여기까진 뻔한데, 한 가지 재밌는 점이 있다.
assertNoAnnotations는 @InjectMocks와 같이 있을 수 없는 어노테이션 조합에 대해 예외처리를 하고 있다.
그런데 @Spy에 대해서는 조합을 허용하고 있다.
아마도 @Spy 또한 실제 객체를 사용해서 모의 객체를 사용하기 때문에 이를 허용해주는 듯 한데..근데 타겟팅 대상인 클래스에 Spy를 허용해봐야 무슨 득이 있을까.
진짜 모르겠다.
📌 MockScanner
MockScanner 또한 예상했던 그대로의 동작을 하고 있기 때문.
다만 여기서 한 가지 주의해야 할 점이 있는데, mockName을 재설정하는 메서드가 존재한다는 점.
private Set<Object> scan() {
Set<Object> mocks = newMockSafeHashSet();
for (Field field : clazz.getDeclaredFields()) {
// mock or spies only
FieldReader fieldReader = new FieldReader(instance, field);
Object mockInstance = preparedMock(fieldReader.read(), field);
if (mockInstance != null) {
mocks.add(mockInstance);
}
}
return mocks;
}
private Object preparedMock(Object instance, Field field) {
if (isAnnotatedByMockOrSpy(field)) {
return instance;
}
if (isMockOrSpy(instance)) {
MockUtil.maybeRedefineMockName(instance, field.getName());
return instance;
}
return null;
}
preparedMock() 두 번째 조건문의 MockUtil.maybeRedefineMockName()을 호출해서 Mock의 이름을 설정하는데
솔직히 뭔 소린지 모르겠다. ㅋㅋㅋㅋ
내부 코드가 엄청나게 길고 주석도 없다.
이해해보려면 할 수는 있겠지만, 이 이상은 너무 오바하는 감이 있어서 패스.
그나마 추측해보자면 @Mock 어노테이션은 name을 설정해줄 수 있는데, 이게 없으면 기본값으로 mock을 등록하는 것 같다.
이게 왜 중요하냐?
같은 타입의 Mock이 있을 경우 @InjectMocks에 제대로 주입이 되지 않을 수 있는데, 이 때 name을 지정해서 필드명과 맞춰주어야 하기 때문이다.
📌 DefaultInjectionEngine
처음에 메서드 명(injectMocksOnFields)만 보고, 왜 갑자기 필드 주입만 수행하는 건가 싶었다.
public class DefaultInjectionEngine {
public void injectMocksOnFields(
Set<Field> needingInjection, Set<Object> mocks, Object testClassInstance) {
MockInjection.onFields(needingInjection, testClassInstance)
.withMocks(mocks)
.tryConstructorInjection()
.tryPropertyOrFieldInjection()
.handleSpyAnnotation()
.apply();
}
}
그런데 그게 아니고 그냥 필드에 주입한다는 의미의 메서드 명이고, 내부적으로 가장 처음에 생성자 주입을 우선하고 있음을 알 수 있었다.
3. 결론
📌 뭘 사용할까?
@InjectMocks 어노테이션을 사용하면 굉장히 편리해보이긴 하지만, 이걸 열렬히 반대하는 글을 읽게 되었다.
@InjectMocks를 사용할 때는 생성자 주입 방식이 아니면 굉장히 위험하다.
심지어 해당 라이브러리를 개발한 프로그래머조차 인정했으며, 하지만 수정할 수 없다고 한다. (이게 만들어질 때랑 지금이랑 관심사가 달랐기 때문인..듯?)
여튼 둘 중 편한 방식을 고르면 되지 않을까라는 생각이 든다. (@InjectMocks의 위험성을 분명히 인식한 후에)
그렇다고 기존에 new 키워드로 타겟 클래스의 인스턴스를 생성하는 방식이 잘못되진 않은 것 같아서 만족.