@InjectMocks 는 구현체여야 하는데, 구현체 타입으로 명시하고 싶지 않은 경우
@InjectMocks 를 사용하기 위해서는 구현체 타입이어야 한다.
생각해보면 당연하다. 해당 객체가 갖는 의존성들을 모킹해서 주입해주겠다는 것인데, 실제 구현 객체가 누구인지, 그리고 그 객체가 어떤 이들에 대해 의존성을 갖고 있는지도 모르고 어떻게 모킹해서 주입을 해 주겠는가.
당연한 거지만 이로인해 약간의 고민이 생겼어서 기록해 보았다.
아래와 같은 계층 구조 일 때, JoinAccountService 라는 “회원가입 책임” 을 가진 서비스를 테스트 하고 싶었다.
이 때 JoinAccountService 는 인터페이스로, AccountService 에서는 해당 인터페이스를 구현하는 구현체로서 두었다.
테스트 대상이 의미적으로 드러나는 인터페이스로서 테스트 하고 싶었기 때문에 AccountService 타입을 테스트 타겟으로 명시하고 싶지는 않았다.
하지만 JoinAccountService 타입으로 명시할 경우, 직접 new 키워드를 사용하며 AccountService 를 “직접 생성자를 사용” 하며 생성하는 것이 불가피해보였다.
그러다보니 아래와 같이 이상한 모습이 되었다.
@ExtendWith(MockitoExtension.class)
class JoinAccountServiceTest {
@Mock
AccountRepository accountRepository;
@Mock
AccountMapper mapper;
@InjectMocks
private JoinAccountService joinAccountService = new AccountService(this.accountRepository, this.accountMapper);
위의 코드에서는 NPE 가 발생했다;
흠.. @Mock annotated 필드들이 초기화 되는 시점이 @InjectMocks 가 호출된 이후 인건가?
InjectingAnnotationEngine.class process(..) 에 대한 docs 을 확인해보자
Process the fields of the test instance and create Mocks, Spies, Captors and inject them on fields annotated @InjectMocks.
`This code process the test class and the super classes.
- First create Mocks, Spies, Captors.
- Then try to inject them.`
?? 이 말대로라면, Mocks 들을 먼저 생성한 이후에 이들을 inject 해야 하니 @InjectMocks 어노테이트된 애한테 정상적으로 주입되는게 아닌가..? 했는데
생각해보니….. 정상적으로 주입되는 경우는 아래와 같았다.
@ExtendWith(MockitoExtension.class)
class JoinAccountServiceTest {
@Mock
AccountRepository accountRepository;
@Mock
AccountMapper mapper;
@InjectMocks
private AccountService joinAccountService;
보아하니 현재 @InjectMocks 를 처리 하면서 Mocks 역시 초기화해서 주입해주는 것이기 때문에 나처럼 생성자를 직접 호출하며 @Mock 필드를 직접 전달하는 코드를 작성하면 당연히 생성자에 this.accountRepository 등의 @Mock annotated field 들을 전달하는 시점엔 null 인 것이다.
그래서 나는 최종적으로 어떻게 했는가 하면 아래와 같이 했다.
@ExtendWith(MockitoExtension.class)
class JoinAccountServiceTest {
private AccountRepository accountRepository;
private AccountServiceMapper accountMapper;
private JoinAccountService joinAccountService;
@BeforeEach
void setUp() {
this.accountRepository = Mockito.mock(AccountRepository.class);
this.accountMapper = Mockito.mock(AccountServiceMapper.class);
joinAccountService = new AccountService(this.accountRepository, this.accountMapper);
}
그 이유는 @Mock, @InjectMocks 를 사용하더라도 디버깅 해보니(DefaultMockitoSession 의 injectCloseableMocks() ) , 테스트 메소드를 실행할 때 마다, InjectMocks 가 되어있는 객체에 Mocks 를 매 번 주입하는 과정이 일어나고 있었고, @BeforeEach 역시 매 테스트 메소드를 실행하기 이전에 다양한 초기화를 위해 사용 되기 때문이다.
원래는 이러한 객체 초기화는 한 번만 일어나면 되고 불필요한 중복 초기화를 하지 않도록 @BeforeAll 을 하려고 했었는데, 현재 객체의 프로퍼티를 초기화하는 부분이라 의도에 맞지 않다 생각했다(static 이 되어야해서..))
그리고 현재 유즈케이스 단위로 인터페이스를 나누는 것을 해 보고 싶기 때문에, 유즈케이스를 나타내는 타입으로 타겟 클래스를 명시하는 것을 포기하고 싶지 않아 위와 같이 작성했다.
사실 어느것이 더 좋을지는 잘 모르겠다;
Test class 이름에서 타겟 대상이 보이니, 구현체를 그냥 작성해 주는 것이 좋은 것인지..
'Spring' 카테고리의 다른 글
@MockitoBean 은 @MockBean 을 완전히 대체 할 수 있는가? (0) | 2025.03.19 |
---|---|
Controller api 가 생각처럼 출력되고 있지 않다 (2) | 2022.09.11 |
선언적 트랜잭션과 programmatic 트랜잭션 (0) | 2022.08.27 |
thymeleaf 에서 객체의 메소드 출력하기 그리고 null (0) | 2022.07.23 |
Self-invocation 과 @Transactional (0) | 2022.06.06 |