스프링 doReturn과 thenReturn에 대해 학습하며 작성된 글입니다. 학습 과정에서 작성된 글이기 때문에 잘못된 내용이 있을 수 있으며, 이에 대한 지적이나 피드백은 언제든 환영입니다.
1. doReturn과 thenReturn
두 메서드 모두 Mockito 프레임워크가 제공하는 메서드로, Stub으로 테스트 대역을 지정하고 Mock과 Spy의 특정 메서드를 호출하면 설정한 값을 반환 합니다. 이 둘은 유사하지만, 타입 세이프(Type Safety), 사이드 이펙트(Side-Effect)에 대해 차이점이 있는데 이에 대해 살펴보겠습니다. 우선 이를 위해 사용자의 이름을 받아 저장하는 간단한 예제를 만들겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequiredArgsConstructor
public class UserCommandController {
private final UserCommandService userCommandService;
@PostMapping
public UserResponse save(@RequestBody UserSignupRequest request) {
User newUser = new User(request.getName());
User savedUser = userCommandService.save(newUser);
return new UserResponse(savedUser);
}
}
1
2
3
4
5
6
7
8
9
10
11
@Service
@RequiredArgsConstructor
public class UserCommandService {
private final UserJpaRepository userJpaRepository;
@Transactional
public User save(User user){
return userJpaRepository.save(user);
}
}
1
2
public interface UserJpaRepository extends JpaRepository<User, Long> {
}
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
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
private String name;
protected User() {
}
public User(String name) {
this.name = name;
}
public User(Long userId, String name) {
this.userId = userId;
this.name = name;
}
public Long getUserId() {
return userId;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User user)) return false;
return getUserId().equals(user.getUserId());
}
@Override
public int hashCode() {
return Objects.hash(getUserId());
}
@Override
public String toString() {
return userId.toString();
}
}
1-1. 타입 세이프(Type Safety)
doReturn 메서드는 컴파일 시점에 타입 체크를 하지 않습니다. 아래 코드를 보면 UserCommandService의 반환 타입은 User인데 doReturn의 기댓값으로 1L을 넣어도 오류가 발생하지 않습니다.
1
2
3
4
5
6
7
8
9
10
11
@Service
@RequiredArgsConstructor
public class UserCommandService {
private final UserJpaRepository userJpaRepository;
@Transactional
public User save(User user){
return userJpaRepository.save(user);
}
}
하지만 테스트를 실행시켜보면 아래와 같이 오류가 발생하게 됩니다.
반면 thenReturn의 경우 컴파일 시점에 타입 체크를 하며 타입 세이프한 테스트를 진행할 수 있습니다. 이를 정리해보면 thenReturn은 컴파일 시점에 오류를 알 수 있지만 doReturn은 알 수 없다는 차이가 있습니다.
1-2. 사이드 이펙트(Side-Effect)
thenReturn은 실제 메서드를 호출하며 doReturn은 실제 메서드를 호출하지 않고 최종 결과만 반환합니다. 따라서 thenReturn은 사이드 이펙트가 존재하며, doReturn은 존재하지 않는 차이가 있습니다. 이를 확인하기 위해 UserCommandService에 아래와 같이 로그를 볼 수 있도록 어노테이션을 추가했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@Service
@RequiredArgsConstructor
public class UserCommandService {
private final UserJpaRepository userJpaRepository;
@Transactional
public User save(User user) {
log.info("UserCommandService save method call");
return userJpaRepository.save(user);
}
}
thenReturn 테스트를 실행시키면 로그가 남는 것을 확인할 수 있는데, 즉 실제 메서드를 호출한 것입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SpyMethodCallTest extends AbstractTestConfiguration {
@SpyBean
private UserCommandService userCommandService;
@SpyBean
private UserCommandController userCommandController;
@Test
@DisplayName("스파이는 실제 메서드를 호출하기 때문에 메서드 호출 횟수가 1이다.")
void thenReturn_실제_메서드_호출_테스트() {
// given
UserCommandService spy = spy(userCommandService);
User newUser = new User("devjun10");
User expected = new User(1L, "devjun10");
when(spy.save(newUser)).thenReturn(expected);
// when
spy.save(newUser);
// then
verify(spy, times(1)).save(newUser);
}
}
반면 doReturn의 경우 로그가 남지 않는 것을 볼 수 있습니다. 즉 thenReturn은 실제 메서드를 호출하기 때문에 그 과정에서 상태를 바꾸는 사이드 이펙트를 발생시키지만, 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
class SpyMethodCallTest extends AbstractTestConfiguration {
@SpyBean
private UserCommandService userCommandService;
@SpyBean
private UserCommandController userCommandController;
@Test
@DisplayName("스파이는 실제 메서드를 호출하기 때문에 메서드 호출 횟수가 1이다.")
void thenReturn_실제_메서드_호출_테스트() {
// given
UserCommandService spy = spy(userCommandService);
User newUser = new User("devjun10");
User expected = new User(1L, "devjun10");
doReturn(expected).when(spy.save(newUser));
// when
spy.save(newUser);
// then
verify(spy, times(1)).save(newUser);
}
}
2. 정리
doReturn와 thenReturn의 차이점은 타입 세이프와 사이드 이펙트의 유무입니다. 단순히 결과적으로만 볼 땐 doReturn이 안 좋아 보일 수 있지만 사이드 이펙트가 없으므로 메서드 호출 시간이 길거나 외부 API를 호출하는 테스트 혹은 문서 작업 등에서는 유용하게 사용될 수 있습니다.