[Test] @Mock, @InjectMocks 동작 원리 및 주의 사항
@Mock과 @InjectMocks
@Mock
Mock 객체(가짜 객체)를 생성한다.
@InjectMocks
Mock 객체를 해당 필드에 주입한다.
@Mock 어노테이션이 붙은 필드와 일치하는 생성자를 찾아 객체를 자동으로 생성해준다.
@InjectMocks 동작 원리
@Mock
private MusicRepository musicRepository;
@InjectMocks
private MusicService musicService;
@InjectMocks는 테스트 대상 클래스의 인스턴스를 생성하고, @Mock 필드를 생성자나 setter 메서드를 통해 주입한다.
musicRepository라는 mock 객체를 musicService 생성자에 주입한다.
추가 매개변수가 있는 생성자
하지만 만약 @InjectMocks 클래스의 생성자에 @Mock 필드 외에 다른 매개변수가 있다면, @InjectMocks는 해당 객체를 자동으로 생성하지 않는다.
public class MusicService {
private final MusicRepository musicRepository;
private final String genre;
public MusicService(MusicRepository musicRepository, String genre) {
this.musicRepository = musicRepository;
this.genre = genre;
}
// ...
}
위 코드에서는 genre라는 매개변수가 있고 @InjectMocks는 MusicService 객체 생성시 genre값을 임의로 넣어줄 수 없다!
즉, @InjectMocks는 생성자를 호출할 수 없어 MusicService 객체를 생성할 수 없다.
에러 메세지
@InjectMocks를 이용해 객체를 초기화(생성)할 수 없습니다.
@InjectMocks 필드에 인스턴스가 할당되지 않아서, 제가 생성하려고했는데 기본 생성자가 없어서 불가능해요~
기본 생성자를 만들어주시거나 필드에 인스턴스를 할당해주세요~~라고 친절하게 말해준다.
해결 방법
1. 기본 생성자 만들기
@InjectMocks가 해당 어노테이션이 붙은 클래스의 인스턴스를 생성해 줄 수 있도록 기본 생성자를 만든다.
public class MusicService {
private final MusicRepository musicRepository;
private final String genre;
public MusicService() {
this(new MusicRepository(), "any");
}
public MusicService(MusicRepository musicRepository, String genre) {
this.musicRepository = musicRepository;
this.genre = genre;
}
// ...
}
그러면 인스턴스가 잘 생성이 될 것이고, 만약 genre를 따로 지정하고 싶다면 setUp에서 원하는 인스턴스로 바꿔주면 된다.
genre로 jazz를 원할 경우
@Mock
private MusicRepository musicRepository;
@InjectMocks
private MusicService musicService;
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
musicService = new MusicService(musicRepository, "jazz");
}
2. 매개변수를 포함한 생성자를 사용하여 @InjectMocks 필드에 인스턴스 할당하기
@Mock
private MusicRepository musicRepository;
@InjectMocks
private MusicService musicService = new MusicService(musicRepository, "any");
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
musicService = new MusicService(musicRepository, "jazz");
}
실행 순서 : @Mock -> @InjectMocks -> @BeforeEach
@InjectMocks 필드에 매개변수를 포함한 생성자를 그대로 사용하여 생성한다.
그리고 setUp 메서드에서 initMocks를 호출하여 Mock으로 생성된 musicRepository를 정상적으로 주입받는다.
주입받은 mock객체를 이용해 musicService 인스턴스를 생성하여 재할당한다.
MockitoAnnotations.initMocks(this)를 호출하면
@Mock으로 어노테이션된 musicRepository가 Mock 객체로 초기화된다.
여기서 @InjectMocks의 musicService와 setUp에서의 musicService는 다른 객체이다.
- 전자는 @BeforeEach에서 initMocks()로 mock 객체가 초기화되지도 않은 상태에서 생성된 거라 제대로 동작하지 않는 빈 껍데기이다.
- 후자는 정상적으로 mock 객체가 초기화된 후 생성된 것으로 실제 테스트 코드에서 사용한다.
장단점
1. 기본 생성자를 추가할 경우 코드의 변경이 필요하지만 @InjectMocks를 이용해 mock 객체를 주입받을 수 있다.
2. setUp에서 재할당할 경우 따로 기본 생성자를 추가할 필요는 없지만 재할당해야하므로 @InjectMocks 기능을 우회하여 사용한다 (복잡성 증가)
mock() 이용해서 의존성 주입하기
추가 매개변수가 있는 생성자의 경우 @InjectMocks를 사용하기 위해 기본 생성자를 추가하거나 setUp 메서드에서 인스턴스를 할당하는 번거로움이 있다.
mock()을 사용해서 의존성을 직접 주입하는 경우 추가 코드나 설정이 필요하지 않아 간단하다.
private MusicRepository musicRepository;
private MusicService musicService;
@BeforeEach
void setUp() {
musicRepository = mock(MusicRepository.class);
musicService = new MusicService(musicRepository, "jazz");
}