Spock 프레임워크에서 Mocking과 Stubbing이 함께 사용되지 않으면 작동하지 않음. <결과 : NPE에러 발생>

박두팔이·2023년 11월 12일
0

🚨 에러상황


회원등록을 하는 메서드의 테스트코드를 작성하는데 mock설정을 제대로 해줬음에도 불구하고 User객체에서 NPE에러가 발생했다.

  • 이를 해결하기 위해 service클래스 내 createUser();가 제대로 동작하지 않는지 한번 더 확인 해보았으나 역시나 문제없이 잘 실행되었다.
  • return타입을 재 확인하고 SignUpResponseDto객체가 반환되도록 테스트코드가 작성되었는지 확인했다.
  • 처음엔 단순히 user의 uuid값을 가져오지 못하는 것이라고 생각했으나 에러를 다시 살펴보니 객체 자체가 null값이란 것을 발견했다.
  • 실제로, assert result != null을 통해 user객체가 null인지 확인했다.

💡 해결방법

급하게 에러를 해결하고싶은 분들을 위해 결론부터 말하면, mocking과 호출 횟수 확인을 결합하면 mock이 호출되지 않기 때문에 문제가 있다. spock공식문서 참고

Spock 공식 문서의 일부에 대한 번역

Mocking과 Stubbing은 함께 사용된다:

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
    }

👩🏻‍💻 자세한 설명:)

테스트하고자 하는 코드: createUser();

@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 메서드 분석

createUser 메서드는 다음 단계로 구성되어 있다.

  1. UserPostDto에서 User 객체로의 변환 (mapper.userPostDtoToUser).
  2. 비밀번호 암호화 (passwordEncoder.encode).
  3. 사용자 역할 설정 (authorityUtils.createRoles).
  4. 사용자 저장 (userRepository.save).
  5. User 객체에서 SignUpResponseDto로의 변환 (mapper.userPostToSignUpResponse).

groovy언어로 작성한 spock테스트코드

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)
    }
}

변경한 코드 (than 주목)

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 VS ✅ Spock (선택함)

그래서 테스트코드를 작성하기 위해 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
}
profile
기억을 위한 기록 :>

0개의 댓글