문제&해결

[Test] @Mock, @InjectMocks 동작 원리 및 주의 사항

mint* 2024. 4. 10. 19:50
728x90

@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");
}

 

 

728x90