회원등록을 하는 메서드의 테스트코드를 작성하는데 mock설정을 제대로 해줬음에도 불구하고 User객체에서 NPE에러가 발생했다.
assert result != null
을 통해 user객체가 null인지 확인했다.급하게 에러를 해결하고싶은 분들을 위해 결론부터 말하면, mocking과 호출 횟수 확인을 결합하면 mock이 호출되지 않기 때문에 문제가 있다. spock공식문서 참고
Mocking과 Stubbing은 동일한 메소드 호출에 대해 동시에 이루어져야 한다. 특히, 아래와 같이 Stubbing과 Mocking을 두 개의 별도 문장으로 분리하는 것은 작동하지 않는다.
given:
subscriber.receive("message1") >> "ok"
when:
publisher.send("message1")
then:
1 * subscriber.receive("message1")
'Where to Declare Interactions'에서 설명한 바와 같이, receive호출은 먼저 then: 블록의 상호작용과 매칭된다. 이 상호작용은 응답을 지정하지 않으므로 메소드의 반환 유형에 대한 기본 값(null)이 반환된다. 이것은 Spock의 유연한 접근 방식의 또 다른 측면이다. 따라서, given: 블록의 상호작용은 매칭될 기회를 얻지 못한다.
기존의 코드 중 문제가 되었던 부분이 이 코드인데,
then:
result.uuid == "550e8400-e29b-41d4-a716-446655440000"
1 * userRepository.save(user)
위의 코드를, 아래의 코드로 수정하면 정상적으로 테스트코드가 통과되는 것을 볼 수 있다.
then:
assert result != null
result.uuid == "550e8400-e29b-41d4-a716-446655440000"
result instanceof SignUpResponseDto
1 * userRepository.save(user) >> user
1 * mapper.userPostToSignUpResponse(_) >> signupResponseDto
}
@RequiredArgsConstructor
@Service
@Transactional
public class UserSignupService implements UserSignupServiceInterface{
private final UserServiceUtilsInterface userServiceUtilsInterface;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserAuthorityUtils authorityUtils;
private final UserMapper mapper;
@Override
public SignUpResponseDto createUser(UserPostDto userPostDto) {
User user = mapper.userPostDtoToUser(userPostDto);
String encryptedPassword = passwordEncoder.encode(user.getPassword());
user.setPassword(encryptedPassword);
List<String> roles = authorityUtils.createRoles(user.getEmail());
user.setRoles(roles);
User createdUser = userRepository.save(user);
return mapper.userPostToSignUpResponse(createdUser);
}
createUser 메서드는 다음 단계로 구성되어 있다.
- UserPostDto에서 User 객체로의 변환 (mapper.userPostDtoToUser).
- 비밀번호 암호화 (passwordEncoder.encode).
- 사용자 역할 설정 (authorityUtils.createRoles).
- 사용자 저장 (userRepository.save).
- User 객체에서 SignUpResponseDto로의 변환 (mapper.userPostToSignUpResponse).
class UserSignupServiceTestSpec extends Specification {
UserServiceUtilsInterface userServiceUtilsInterface = Mock(UserServiceUtilsInterface)
UserRepository userRepository = Mock(UserRepository)
PasswordEncoder passwordEncoder = Mock(PasswordEncoder)
UserAuthorityUtils authorityUtils = Mock(UserAuthorityUtils)
UserMapper mapper = Mock(UserMapper)
UserSignupService userSignupService
def setup() {
userSignupService = new UserSignupService(
userServiceUtilsInterface,
userRepository,
passwordEncoder,
authorityUtils,
mapper
)
}
def "createUser 회원가입 메서드"() {
given:
// UserPostDto의 예시 데이터
def userPostDto = new UserPostDto("홍길동", "hgd@gmail.com", "Apple1234!", "010-1111-2222")
// User 객체 초기화
def user = new User()
// 암호화된 비밀번호의 예시
def encryptedPassword = "encryptedPassword"
// Mock 객체들의 행동 정의
mapper.userPostDtoToUser(userPostDto) >> user
passwordEncoder.encode(user.getPassword()) >> encryptedPassword
authorityUtils.createRoles(user.getEmail()) >> ["ROLE_USER"]
userRepository.save(user) >> user
// SignUpResponseDto의 예시
def signupResponseDto = new SignUpResponseDto()
signupResponseDto.uuid = "550e8400-e29b-41d4-a716-446655440000"
mapper.userPostToSignUpResponse(user) >> signupResponseDto
when:
def result = userSignupService.createUser(userPostDto)
then:
result.userId == 1L
1 * userRepository.save(user)
}
}
class UserSignupServiceTestSpec extends Specification {
.
.
. //생략
then:
assert result != null
result.uuid == "550e8400-e29b-41d4-a716-446655440000"
result instanceof SignUpResponseDto
1 * userRepository.save(user) >> user
1 * mapper.userPostToSignUpResponse(_) >> signupResponseDto
}
계속되는 에러로 지쳐서 '이거 대체 나 왜하고있지?' 란 생각이 들었다. 그래서 리마인드를 목적으로 되새겨보는 쿠키공부..?
버그를 감소하고 코드를 리팩토링하는 경우, 테스트코드를 통해 변경된 코드가 있어도 기존 기능이 올바르게 작동하는지 쉽게 확인할 수 있다.
그래서 테스트코드를 작성하기 위해 Junit5와 spock테스트 프레임워크 중 고민하다가 프로젝트의 멘토님께서 Spock을 추천해주셔서 도전해보기로했다.
spock은 내장된 모킹과 스텁기능을 제공하고 데이터 주도적인 테스트가 가능하다 특히 where블록을 사용하면 파라미터화된 테스트를 쉽게 작성할 수 있다는 장점이 있다.
결론: 일단 도전!!! 안되면 될 때까지 🔥
then:
assert result != null
result.uuid == "550e8400-e29b-41d4-a716-446655440000"
result instanceof SignUpResponseDto
1 * userRepository.save(user) >> user
1 * mapper.userPostToSignUpResponse(_) >> signupResponseDto
}