Spring Boot 환경에서 여러 협력 객체들이 존재하는 통합테스트는 빈을 호출하기 위해 @SpringBootTest를 사용하는 경우가 많다. 그리고 협력 객체들이 많을 때에는 테스트를 위한 기대 행위 전처리 작업을 mockito를 이용하여 stub을 통해 test double을 만들어주는 경우들이 존재한다.
@SpringBootTest와 함께 사용한다면 @Mock, @InjectMocks 대신 @MockBean을 주로 사용할 것이다. 그런데 테스트 코드를 작성하는 방법에 대해 공부하던 도중 @SpyBean이라는 어노테이션을 알게 됐다. 이 어노테이션은 뭐하는 애일까? 그리고 어떻게 사용하는게 좋을까?
@MockBean
개인적으로 나는 웬만해선 @MockBean 대신 실제 객체를 사용하는 방식을 사용하고 있다.
(그 이유에 대해서는 아래에 서술할 예정이다.)
다만, OAuth 테스트 코드를 작성하는 경우는 실제 계정을 넣을 순 없기 때문에 아래와 같이 @MockBean을 사용하고 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class AuthServiceTest {
@MockBean
private KakaoPlatformUserProvider kakaoPlatformUserProvider;
@Autowired
private MemberRepository memberRepository;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private AuthService authService;
@Test
@DisplayName("사용자 정보를 받아 토큰을 만들어 반환한다.")
void createToken() {
final PlatformUserDto platformUserDto = new PlatformUserDto("alex", "email@email.com", "KAKAO", "1");
when(kakaoPlatformUserProvider.getPlatformUser(anyString(), anyString())).thenReturn(platformUserDto);
final TokenRequestDto tokenRequestDto = new TokenRequestDto("authorizationCode", "https://...");
final TokenResponseDto tokenResponseDto = authService.createTokenWithKakaoOauth(tokenRequestDto);
assertThatCode(() -> jwtTokenProvider.validateAbleToken(tokenResponseDto.getAccessToken()))
.doesNotThrowAnyException();
}
}
|
cs |
Kakao Oauth를 이용할 때 사용자 정보를 받아 제대로 토큰이 반환되는지 확인하는 테스트 메서드이다.
우리가 이 테스트에서 중요한 것은, 사용자 정보를 받아 토큰이 제대로 반환되는 여부이지, 사용자 정보를 올바르게 생성하는 것은 관심 분야가 아니다. 그렇기 때문에 @MockBean으로 KakaoPlatformUserProvider 가짜 객체를 만들어서 getPlatformUser 메서드가 항상 의도한 사용자 정보 DTO를 반환하게 하였다. 이 사용자 정보를 바탕으로 토큰을 받는 createTokenWithKakaoOauth를 테스트한 것이다.
이렇듯, @MockBean을 이용하면 편리하게 test double을 생성할 수 있다.
@SpyBean
그런데 mockito에서 제공해주는 어노테이션은 @MockBean뿐만 아니라 @SpyBean도 존재한다. 이 어노테이션은 뭐하는 친구일까?
가짜 객체를 만들어서 stub을 해주는 @MockBean과 달리, @SpyBean은 given에서 stub한 메서드 외에는 그 객체의 실제 메서드를 사용할 수 있다. 마치 실제 객체의 메서드들 중에서 우리가 지정한 특정 메서드만 의도대로 작동되게 하는 것이 그 객체에 스파이를 하나 심어둔 것 같다고 생각하면 이해하기 쉽다. (이 예시는 jojoldu님 블로그 https://jojoldu.tistory.com/226 에서 봤는데 정말 기똥찼다.)
특정 객체의 일부 메서드만 stub하고, 나머지는 실제 메서드를 사용하고 싶을 때 @SpyBean은 좋은 선택지가 될 수 있다.
이처럼 @SpyBean은 실제 객체의 메서드를 사용하기 때문에 아래와 같이 @SpyBean으로 kakaoPlatformUserProvider를 등록해주고, 해당 객체의 getPlatformUser(anyString(), anyString())를 호출하여 작성하면 테스트가 실패한다.
에러나는 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class AuthServiceTest {
@SpyBean
private KakaoPlatformUserProvider kakaoPlatformUserProvider;
@Autowired
private MemberRepository memberRepository;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private AuthService authService;
@Test
@DisplayName("사용자 정보를 받아 토큰을 만들어 반환한다.")
void createToken() {
final PlatformUserDto platformUserDto = new PlatformUserDto("alex", "email@email.com", "KAKAO", "1");
when(kakaoPlatformUserProvider.getPlatformUser(anyString(), anyString())).thenReturn(platformUserDto);
final TokenRequestDto tokenRequestDto = new TokenRequestDto("authorizationCode", "https://...");
final TokenResponseDto tokenResponseDto = authService.createTokenWithKakaoOauth(tokenRequestDto);
assertThatCode(() -> jwtTokenProvider.validateAbleToken(tokenResponseDto.getAccessToken()))
.doesNotThrowAnyException();
}
}
|
cs |
실제 객체의 메서드에 argument로 이상한 값을 넘겨주니까 당연히 에러가 발생한다. 이는 메서드를 직접 호출해서 그렇다. 따라서 아래와 같이 바꿔주어야 한다.
// ERROR
when(kakaoPlatformUserProvider.getPlatformUser(anyString(), anyString()))
.thenReturn(platformUserDto);
// 이 메서드를 작동할 때는 doReturn을 반환하라
doReturn(platformUserDto).when(kakaoPlatformUserProvider)
.getPlatformUser(anyString(), anyString());
직접 getPlatformUser()를 호출하지 말고 when()으로 지정해준 SpyBean 객체의 특정 메서드가 doReturn을 반환한다고 지정해주기만 하면 된다.
변경된 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class AuthServiceTest {
@SpyBean
private KakaoPlatformUserProvider kakaoPlatformUserProvider;
@Autowired
private MemberRepository memberRepository;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private AuthService authService;
@Test
@DisplayName("사용자 정보를 받아 토큰을 만들어 반환한다.")
void createToken() {
final PlatformUserDto platformUserDto = new PlatformUserDto("alex", "email@email.com", "KAKAO", "1");
doReturn(platformUserDto).when(kakaoPlatformUserProvider).getPlatformUser(anyString(), anyString());
final TokenRequestDto tokenRequestDto = new TokenRequestDto("authorizationCode", "https://...");
final TokenResponseDto tokenResponseDto = authService.createTokenWithKakaoOauth(tokenRequestDto);
assertThatCode(() -> jwtTokenProvider.validateAbleToken(tokenResponseDto.getAccessToken()))
.doesNotThrowAnyException();
}
}
|
cs |
테스트가 통과하는 것을 확인할 수 있다.
참고로 @SpyBean을 인터페이스에 사용할 때엔 해당 인터페이스의 구현체가 반드시 스프링 컨텍스트에 등록돼있어야 한다.
개인적으로 @MockBean, @SpyBean을 지양하고 있는 이유
@MockBean, @SpyBean은 given으로 필요한 test double을 편리하게 만들 수 있다는 장점이 존재한다.
하지만 @MockBean, @SpyBean을 사용하게 되면 Spring Context를 실행해야 하므로 테스트 성능이 저하된다. 이는 피드백이 느려질 수밖에 없으므로 문제가 된다. 특히 CI/CD까지 겸하는 프로젝트라면 그 답답함은 배가 될 것이다. 매번 PR 및 merge가 될 때마다 CI/CD 툴이 build할테니 말이다.
그렇지만 이 문제 외에도 또 다른 문제가 존재한다.
바로 의존성이 많은 상태임에도 불구하고 테스트 코드 작성이 편리해져 code smell을 확인하기 어려워진다는 점이다. 이 점 역시 jojoldu님의 블로그를 보고 깨닫게 된 부분이다.
https://jojoldu.tistory.com/320
테스트 코드를 하나의 문서처럼 생각한다면 @MockBean, @SpyBean으로 감춰진 code smell은 더 큰 문제가 될 것이다. 개인적으론 테스트 코드를 통해 의존 객체가 무엇이 있는지, 그 테스트는 어떤 역할을 하는지 파악하기 쉬워야 한다고 생각한다.
따라서 나는 테스트에서 아래와 같이 @BeforeEach를 사용한 stub을 선호하는 편이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RollingpaperServiceTest {
private static final String ROLLINGPAPER_TITLE = "AlexAndKei";
private static final String MEMBER_NICKNAME = "마스터";
private static final String TEAM_NAME = "nae-pyeon";
private final TeamRequestDto teamRequestDto =
new TeamRequestDto("nae-pyeon", "테스트 모임입니다.", "testEmoji", "#123456", MEMBER_NICKNAME, true);
private Long teamId;
private Long memberId;
private Long member2Id;
private Long member3Id;
@Autowired
private MemberService memberService;
@Autowired
private TeamService teamService;
@Autowired
private RollingpaperService rollingpaperService;
@Autowired
private RollingpaperRepository rollingpaperRepository;
@BeforeEach
void setUp() {
memberId = memberService.save("member", "m@hello.com", "KAKAO", "1");
member2Id = memberService.save("writer", "w@hello.com", "KAKAO", "2");
member3Id = memberService.save("anonymous", "a@hello.com", "KAKAO", "3");
teamId = teamService.save(teamRequestDto, memberId);
teamService.joinMember(teamId, member2Id, "안뇽안뇽");
}
@Test
@DisplayName("멤버 대상 롤링페이퍼를 저장하고 id값으로 찾는다.")
void saveMemberRollingpaperAndFind() {
final Long rollingpaperId =
rollingpaperService.createMemberRollingpaper(ROLLINGPAPER_TITLE, teamId, member2Id, memberId);
final RollingpaperResponseDto rollingpaperResponse =
rollingpaperService.findById(rollingpaperId, teamId, memberId);
assertThat(rollingpaperResponse)
.extracting("title", "to", "messages")
.containsExactly(ROLLINGPAPER_TITLE, MEMBER_NICKNAME, List.of());
}
}
|
cs |
물론 이 부분은 정답이 없는 문제라 생각한다.
프로덕션 코드에서 충분히 의존성에 대해 고민하고 설계를 했고, 의존 객체가 많아 테스트 코드를 작성하기 어렵다면 @MockBean, @SpyBean을 사용하는 것은 좋은 방법이라 생각된다.
상황에 맞게 적절하게 사용하는 것이 가장 좋아보인다.
참고
- javadoc 공식문서: https://www.javadoc.io/doc/org.mockito/mockito-core/2.7.17/org/mockito/Mockito.html#spy
- @MockBean, @SpyBean jojoldu님 블로그: https://jojoldu.tistory.com/226
- @MockBean, @SpyBean cobbybb님 블로그: https://cobbybb.tistory.com/16
- mockito vs BDDMockito: https://velog.io/@lxxjn0/Mockito%EC%99%80-BDDMockito%EB%8A%94-%EB%AD%90%EA%B0%80-%EB%8B%A4%EB%A5%BC%EA%B9%8C
- @MockBean, @SpyBean 단점 jojoldu님 블로그: https://jojoldu.tistory.com/320
'JAVA > JAVA | Spring 학습기록' 카테고리의 다른 글
[Spring] @Transactional의 전파 레벨에 대해 알아보자 (4) | 2022.10.17 |
---|---|
[Spring] @SpringBootTest의 webEnvironment와 @Transactional (5) | 2022.08.23 |
[Spring] log4j2를 활용한 로깅 전략을 다룬 yml 파일을 생성하자 (2) | 2022.08.21 |
[JAVA] IoC, DI, DIP (0) | 2022.08.09 |
[Spring] 인증 토큰을 생성하는 JwtTokenProvider를 알아보고 테스트를 작성해보자 (0) | 2022.07.22 |