@InjectMocks
단위 테스트에서는 주로 `@SpringBootTest`가 아니라 `@InjectMocks`를 사용하는데
이 어노테이션의 이름만 보면 "Inject + Mocks = 목 객체들을 주입한다." 이렇게 해석된다.
`@InjectMocks`도 객체에 `Mock`을 주입하는 것인데 스프링의 의존성 주입과는 뭐가 다를까?
또한, 테스트 과정에서 인터페이스는 `@Mock`를 사용해야 했는데 왜 `@Spy`는 사용할 수 없다.
따라서, 이번 포스팅에서는 `@InjectMock`, `@Spy`, `@Mock`에 관한 정보를 담은 포스팅을 작성하려한다.
우선, 함께 볼 Service 코드는 다음과 같다.
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class AdminCommandService {
private final AdminRepository adminRepository;
private final PasswordEncoder passwordEncoder;
private final ImageUploadService imageUploadService;
private final MailService mailService;
private final RedisService redisService;
public AdminInfoResponse create(AdminCreateRequest request){
validateEmailAndNickNameAndLoginId(request.getEmail(), request.getNickName(),
request.getLoginId());
request.modifyPassword(passwordEncoder.encode(request.getPassword()));
String urlName = RandomUtils.generateAlphaNumericRandomCode();
Admin newAdmin = Admin.builder()
.email(request.getEmail())
.nickName(request.getNickName())
.name(request.getName())
.profileUrl(request.getProfileUrl())
.roleType(RoleType.PORT)
.password(request.getPassword())
.loginId(request.getLoginId())
.urlName(urlName)
.build();
newAdmin = adminRepository.save(newAdmin);
return AdminInfoResponse.from(newAdmin);
}
...
}
간단한 설명으로, 회원가입 예제로 해당 유저의 메일, 닉네임, 로그인ID가 이미 있는지를 검증한다.
그 후, `passwordEncoder`를 통해 기존 비밀번호를 암호화한다.
그 후, 도메인을 DB에 저장한 뒤 반환한다.
하단은 테스트 코드이다.
@ExtendWith(MockitoExtension.class)
class AdminCommandServiceTest {
@InjectMocks
private AdminCommandService adminCommandService;
@Mock
private AdminRepository adminRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private ImageUploadService imageUploadService;
@Mock
private MailService mailService;
@Mock
private RedisService redisService;
private Admin admin;
private AdminCreateRequest createRequest;
private AdminModifyPasswordRequest modifyPasswordRequest;
@BeforeEach
void setUp(){
admin = AdminFixture.createAdmin();
createRequest = AdminCreateRequest.builder()
.loginId("test")
.email("test@naver.com")
.profileUrl("https://test.com")
.password("test")
.nickName("test")
.name("test")
.build();
modifyPasswordRequest = AdminModifyPasswordRequest.builder()
.adminId(1L)
.oldPassword("oldPassword")
.newPassword("newPassword")
.build();
}
@Test
public void create_성공_테스트() throws Exception{
//given
given(adminRepository
.existsByEmailAndLoginIdAndNickName(anyString(), anyString(), anyString()))
.willReturn(false);
given(adminRepository.save(any(Admin.class))).willReturn(admin);
MockedStatic<RandomUtils> mockedStatic = mockStatic(RandomUtils.class);
mockedStatic.when(RandomUtils::generateAlphaNumericRandomCode).thenReturn("test");
//when
AdminInfoResponse response = adminCommandService.create(createRequest);
//then
assertNotNull(response);
assertEquals(1L, response.getAdminId());
assertEquals(admin.getName(), response.getName());
assertEquals(admin.getNickName(), response.getNickName());
assertEquals("test", response.getUrlName());
assertEquals(admin.getEmail(), response.getEmail());
assertEquals(admin.getProfileUrl(), response.getProfileUrl());
verify(passwordEncoder, times(1)).encode(anyString());
mockedStatic.close();
}
...
}
테스트 클래스 최상단을 확인해보자.
클래스 최상단에 `@ExtendWith(MockitoExtension.class)`를 적어준다.
`@ExtendWith(MockitoExtension.class)`는 JUnit 5에서 `Mockito`를 확장하여 사용할 수 있도록 해주는 어노테이션이다.
이 어노테이션을 통해, Mockito의 `@Mock`, `@InjectMocks`, `@Spy`와 같은 기능들이 JUnit 테스트에서 자동으로 동작하게 된다.
@ExtendWith(MockitoExtension.class)
class AdminCommandServiceTest {
}
테스트 클래스 내부에 선언된 멤버 변수를 확인해 보자.
- 서비스 클래스의 필드에 선언된 클래스들은 테스트 클래스 내부에도 그대로 작성되어 있다.
- 다른 점은 테스트 클래스 필드에 선언된 클래스들은 `@Mock` 어노테이션을 사용한다.
- 제일 중요한 테스트 메서드를 가진 클래스는 `@InjectMocks` 어노테이션을 사용하고 있다.
@ExtendWith(MockitoExtension.class)
class AdminCommandServiceTest {
@InjectMocks
private AdminCommandService adminCommandService;
@Mock
private AdminRepository adminRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private MailService mailService;
@Mock
private RedisService redisService;
}
그래서 @InjectMocks, @Mock이 뭘까?
@InjectMocks
- `@InjectMocks`는 `Mockito`가 자동으로 모킹 된 객체(`@Mock`으로 선언된 객체)를 주입해주는 어노테이션이다.
- 이로 인해 테스트하려는 객체(예: 서비스 클래스)에서 필요로 하는 의존성 필드들을 자동으로 주입해준다.
- 위 어노테이션을 사용하면, 테스트하려는 객체의 모든 의존성이 자동으로 주입되므로 수동으로 의존성을 설정할 필요가 없다.
@Mock
- `@Mock`은 `Mockito`가 모킹 한 객체를 생성하기 위해 사용되는 어노테이션이다.
- 모킹 된 객체는 실제 객체의 동작을 시뮬레이션할 수 있으며, 이를 통해 테스트 시 특정 객체의 메서드를 원하는 방식으로 동작하도록 제어할 수 있다.(given, when 등)
- 이로 인해 외부 의존성(데이터베이스, 네트워크, 파일 시스템 등)에 대한 접근 없이 테스트를 수행할 수 있다.
위의 테스트 코드를 다시 봐보자.
- 클래스 최상단에 `@ExtendWith`를 작성해서 `@Mock`, `@InjectMocks`, `@Spy`와 같은 기능들이 동작한다.
- 하단의 `@InjectMocks`를 적어준 클래스는 실제 테스트를 하려는 `AdminCommandService` 클래스이고 이 클래스는 `@InjectMocks`를 통해 `@Mock`, `@Spy`로 선언된 필드를 자동으로 주입받는다.
- 나머지 `@Mock`를 사용하는 클래스들은 `AdminCommandService` 클래스에서 필요로 하는 의존성 클래스 들이다.
이 `@Mock`을 적어줘야만 `@InjectMocks`에서 이 클래스들을 의존성 주입해준다.
@ExtendWith(MockitoExtension.class)
class AdminCommandServiceTest {
@InjectMocks
private AdminCommandService adminCommandService;
@Mock
private AdminRepository adminRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private MailService mailService;
@Mock
private RedisService redisService;
}
만약, 테스트 하려는 실제 클래스가 필요로 하지 않는 의존성 객체들을 @Mock으로 선언하면 어떻게 될까?
- `@Mock`을 사용해서 `AdminCommandService`에서는 주입받지 않는 클래스 2개를 선언해봤다.
- 이후 테스트 코드를 실행했더니 문제없이 성공한다.
- 확인해보니 `@InjectMocks`가 주입을 시도하는 `Mock` 객체가 주입될 클래스(테스트할 메서드를 가진 클래스)의 필드에 없으면, 그 `@Mock` 객체는 그냥 무시된다.
- 따라서, 필요하지 않은 의존성을 `@Mock`으로 선언했다고 해서 테스트가 실패하지는 않는다.
- 그래도 가독성을 위해서는 정말 테스트에 필요한 의존성 객체를 `@Mock`으로 선언하는 것이 좋다.
@ExtendWith(MockitoExtension.class)
class AdminCommandServiceTest {
@InjectMocks
private AdminCommandService adminCommandService;
@Mock
private AdminRepository adminRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private MailService mailService;
@Mock
private RedisService redisService;
// 아래 2가지 객체는 실제 서비스에는 없다.
@Mock
private ImageUploadService imageUploadService;
@Mock
private JwtTokenProvider jwtTokenProvider;
}
`@InjectMocks`에 `@Spy`를 쓰면 뭐가 다를까?
@Mock과 @Spy의 차이점
- `@InjectMocks`는 `Mockito`에서 주입할 객체를 자동으로 생성하고, 주입할 클래스의 필드에 `@Mock` 또는 `@Spy`로 선언된 객체들을 주입한다.
- 그러나, `@Mock`과 `@Spy`의 차이는 객체의 동작 방식에 있다.
- 위를 이해하면 `@InjectMocks`와 함께 `@Spy`를 사용할 때 어떤 차이가 발생하는지 알 수 있다.
@Mock
- 인터페이스에도 적용 가능
- 객체의 모든 메서드를 모킹(기본적으로 아무 동작 X)
- 명시적으로 동작을 설정해야만 동작함
@Spy
- 구체적인 구현체(클래스)에만 적용 가능
- 객체의 실제 메서드가 호출되며, 필요시 특정 메서드만 모킹 가능
- 인터페이스에는 적용할 수 없음
즉, `@Spy`는 인터페이스에는 사용할 수 없으며, `Stub`을 하지 않으면 실제 메서드 동작이 수행된다.
(`Stub`을 해주면 `Stub` 동작이 일어난다.)
인터페이스에는 `@Spy`가 아닌 `@Mock`을 적용하는 이유
메서드 시그니처 기반
- 인터페이스는 메서드 시그니처(메서드의 이름, 반환 타입, 파라미터 목록 등)를 정의한다.
- 따라서, 인터페이스에 `@Mock`을 적용하면, `Mockito`는 이 인터페이스의 모든 메서드에 대해 기본 동작을 설정할 수 있다.
프록시 객체 생성
- `Mockito`는 인터페이스에 대한 프록시 객체를 생성한다.
- 이 프록시 객체는 인터페이스에 정의된 메서드를 호출할 수 있지만, 실제로는 구현이 없기 때문에, 기본적으로 아무 동작도 하지 않고, 반환 타입에 따른 기본값을 반환한다.
구현체와의 차이
- 구현체에 `@Mock`을 사용하면, 해당 클래스의 실제 메서드 구현이 무시되고, 모킹 된 결과를 반환한다.
- 하지만, 인터페이스는 애초에 구현체가 없기 때문에, 메서드 시그니처에 따라 기본값을 반환한다.
결과 값의 기본값
- boolean: false
- int: 0
- Object: null
- Collection: 빈 컬렉션 등
`@Spy`는 실제 동작을 기반으로 한다.
- `@Spy`는 특정 메서드를 모킹 하기 전에는 기본적으로 실제 객체의 메서드가 호출되도록 한다.
- 인터페이스는 메서드의 구현이 없으므로, `@Spy`가 수핼할 실제 동작이 존재하지 않는다.
- 따라서, `@Spy`는 인터페이스가 아닌, 실제 구현체가 있는 클래스에만 사용할 수 있다.
최종 정리
- @Mock
- 인터페이스에 `@Mock`을 적용하면, 그 인터페이스의 모든 메서드를 호출할 수 있음
- 하지만, 기본적으로 아무런 동작도 수행하지 않으며, 반환 타입에 맞는 기본값을 반환
- 이는 인터페이스의 메서드 시그니처가 존재하기 때문에 가능하며,
이 메서드 시그니처를 기반으로 `@Mockito`가 프록시 객체를 생성하기 때문이다.
- 인터페이스에 `@Mock`을 적용하면, 그 인터페이스의 모든 메서드를 호출할 수 있음
- @Spy
- 실제 객체를 사용하고, 그 객체의 메서드를 호출할 때 실제로 동작하도록 한다.
- 그렇기에 필요한 경우 특정 메서드만 모킹한다.
- 인터페이스는 단순히 메서드의 시그너처만 정의할 뿐, 실제 구현체가 없기에 `@Spy`로 감싸려면 구체적인 클래스(즉, 실제 객체)가 필요하다.
- 따라서, `@Spy`는 구체적인 동작을 포함하는 실제 객체가 있어야 하기에 인터페이스에는 사용할 수 없다.
- 대신 클래스에 적용하여 부분 모킹을 사용할 수 있다.
- 실제 객체를 사용하고, 그 객체의 메서드를 호출할 때 실제로 동작하도록 한다.
@InjectMocks는 실제 테스트할 객체가 필요로 하는 모든 의존성을 주입받아야 할까?
앞서, 아래 경우를 알아보았다.
"만약, 테스트 하려는 실제 클래스가 필요로 하지 않는 의존성 객체들을 @Mock으로 선언하면 어떻게 될까?"
무시되는 것을 알 수 있었다.
반대로, 그렇다면 모든 의존성을 주입받아야 할까?
위의 예시에서 2개의 주입을 제외시켰다.
@ExtendWith(MockitoExtension.class)
class AdminCommandServiceTest {
@InjectMocks
private AdminCommandService adminCommandService;
@Mock
private AdminRepository adminRepository;
@Mock
private PasswordEncoder passwordEncoder;
// @Mock
// private MailService mailService;
// @Mock
// private RedisService redisService;
}
스프링의 의존성 주입과 비슷하게 동작하여 예외가 발생하지 않을까?
- 4개가 다 주입되어야 동작할 것이라고 생각하고 테스트를 진행해 봤다.
- 결과는 테스트 메서드에서 필요로 하는 2개의 의존성만 @Mock으로 주입해줘도 테스트에 성공한다.
- 만약, 해당 빈을 사용하는 곳이 있다면 제외시키면 안된다.
- 테스트 코드 중 `create()` 메서드만 다뤘기 때문에 통과가 되었다.
스프링과 Mockito의 의존성 주입의 차이점
스프링의 의존성 주입(DI)
- 스프링 DI는 애플리케이션의 전반적인 의존성을 관리하고 주입하는 방법이다.
- 이를 통해 애플리케이션이 실행될 때 필요한 모든 의존성이 주입된다.
- 스프링의 생성자 주입 방식에서는 모든 final 필드가 반드시 초기화되어야 한다.
- 따라서, 스프링은 애플리케이션 실행 시 의존성을 자동으로 주입한다.
- 만약 어떤 의존성이 주입되지 않으면, 스프링은 오류를 발생시키며 애플리케이션이 정상적으로 실행되지 않는다.
- 스프링에서 null이 주입되는 경우, 이는 의존성 미주입과 동일하게 간주되어, 런타임에서 오류를 발생시킨다.
Mockito의 의존성 주입
- `Mockito`는 주로 단위 테스트에서 객체 간의 의존성을 모킹하고 주입하는 데 사용된다.
- 이로 인해 실제 의존성을 모킹하여 독립적으로 테스트할 수 있다.
- 주입할 클래스의 필드가 final로 선언된 경우 `@InjectMocks`는 클래스의 생성자를 통해 필요한 의존성들을 주입한다.
- 만약 생성자에서 요구하는 필드가 `@Mock`으로 선언되지 않았거나 주입할 객체가 없다면, 그 필드는 null로 전달된다.
- 즉, 생성자를 호출할 때, 주입 가능한 `@Mock` 객체는 주입되고, 나머지는 null로 주입되어 객체가 생성된다.
결론
- 스프링 DI는 애플리케이션 실행 시 모든 의존성이 주입되어야 하며, 주입되지 않은 의존성에 대해 오류를 발생시킨다.
- Mockito는 `@InjectMocks`를 사용하면 사용자가 모킹한 의존성 필드만 주입하고, 모킹하지 않는 의존성 필드는 자동으로 null로 주입해 객체를 생성한다.
- 즉, 스프링 DI는 달리 오류를 발생시키지 않으므로, 단위 테스트를 돕는다.
'Spring > Test' 카테고리의 다른 글
[테스트 코드] 스프링에서의 Redis 테스트 환경 구축 (0) | 2024.11.29 |
---|---|
[테스트 코드] @DataJpaTest 사용 (0) | 2024.11.24 |
[테스트 코드] 테스트 코드에서 static method를 사용하는 법 (0) | 2024.11.24 |
[테스트 코드] 테스트 코드 이해하기 (0) | 2024.11.22 |
[테스트 코드] MockMvc, MockBean (0) | 2024.11.21 |