TIL

[231211] Spring 심화 개인 과제 피드백

진진리 2023. 12. 11. 19:41
728x90
이번 심화 개인 과제로 테스트 코드를 작성하는 과제가 나왔다.
테스트 과제를 진행하면서 궁금했던 점들을 깃허브 이슈에 남겨놓았고,
이를 토대로 튜터님께서 답변을 남겨주셨다.

 

1. 테스트 시 기존 코드에 없는 생성자가 필요한 경우

예를 들어 reqeustDto 같은 경우 테스트를 위해서 값을 넣어줘야 했는데, 
기존의 코드에서는 Getter만 있었기 때문에 생성자를 추가해주었다.
테스트로 인해서 기존 코드의 변경이 있는 것은 좋지 않다는 생각이 들어서 이에 대해 질문을 남겼다.

 

답변:

이런 경우 생성자는 NoArgsConstructor의 AccessLevel을 PROTECTED로 정의하고
mockBuilder를 사용해서 명확하게 객체를 만들 때 mockBuilder를 사용하는 방법이 있습니다.
// ex.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class UserRequestDto {

    @Size(min=4, max=10)
    @Pattern(regexp = "^[a-z0-9]*$")
    private String username;

    @Size(min=8, max=15)
    @Pattern(regexp = "^[a-zA-Z0-9]*$")
    private String password;

    @Builder(builderClassName = "mockBuilder", builderMethodName = "mockBuilder")
    public UserRequestDto(String username, String password) {
        this.username = username;
        this.password = password;
    }
}

 

이렇게 정의하면 new UserRequestDto(name, pw) <- 이런 형태로 사용할 수 없고 UserRequestDto.mockBuilder().username('name').password('pw').build(); 이런식으로 사용합니다.

_x0008_테스트를 할 경우 객체를 꼭 생성해야 할 경우 이런식으로 많이 사용하곤 합니다.

 

  • @NoArgsConstructor의 access
    • PUBLIC(또는 NONE): 모든 곳에서 접근 가능
    • MODULE(또는 PACKAGE): 같은 패키지 내에서 접근 가능
    • PROTECTED: 같은 패키지 또는 자식 클래스에서 접근 가능
    • PRIVATE: 내부 클래스에서만 사용 가능
  • @NoArgsConstructor(access = AccessLevel.PROTECTED)를 사용하는 이유
    • 모든 필드를 다 가져야 하는 클래스의 경우, 무분별한 객체 생성을 막아줌
  • @Builder와 함께 사용
    • 모든 멤버 변수를 받는 생성자에 @Builder를 설정
    • 생성자로 바로 객체를 생성하는 것을 막고 mockBuilder로 명시해주어야만 객체를 생성할 수 있도록 함
    • 테스트를 하는 경우 객체를 mockBuilder로 생성하여 사용

 

코드

@DisplayName("성공")
@Test
void createUserRequestDto_success() {
    // given
    UserRequestDto requestDto = new UserRequestDto.mockBuilder().username("username").password("password").build();

    // when
    Set<ConstraintViolation<UserRequestDto>> violations = validate(requestDto);

    // then
    assertThat(violations).isEmpty();
}
  • mockBuilder()를 사용할 때 오류가 발생했는데 UserRequestDto가 있는 위치와 테스트 코드의 위치가 달라서 생긴 오류였다.

2. 서비스 단위 테스트에서 NotFound~Exception 상황을 테스트하고자 할 때

서비스 단위 테스트에서는 레포지토리를 mock 객체로 사용하는데 이때 객체를 찾을 수 없는 경우를 테스트하고자 할 때 어떻게 해야 하는지 몰라서 질문을 남겼다.

 

답변:

given(cardRepository.findById(cardId)).willReturn(Optional.empty());
Optional.empty()를 리턴하게 하면 원하는 NotFound 에러를 터트릴 수 있습니다.

 

 

코드

        @Test
        @DisplayName("카드를 찾을 수 없음")
        void test6() {
            //given
            given(cardRepository.findById(1L)).willReturn(Optional.empty());

            //when
            EntityNotFoundException exception = assertThrows(EntityNotFoundException.class, () ->
                cardService.getCardEntity(1L)
            );

            //then
            assertEquals("해당 카드를 찾을 수 없습니다.", exception.getMessage());
        }
  • 테스트하고자 했던 EntityNotFoundException에 대해 테스트를 했다.

3. 단위 테스트에서 테스트 대상의 리턴값이 없는 경우

서비스 단위 테스트를 진행하면서 서비스의 메소드를 하나씩 테스트해보았다.
주로 메소드의 반환값을 가지고 잘 반환되었는지를 확인하였는데
반환값이 없는 경우에는 어떻게 테스트하면 좋을지 몰라서 중간에 발생할 수 있는 예외들을 테스트하고
이에 대해 질문하게 되었다.

 

답변:

void같은 메소드를 테스트할 땐 중간에 발생할 수 있는 예외 상황에 대해서 처리도 하지만
mocking한 코드가 잘 호출 됐는지를 테스트할 수 있습니다.

지금 _x0008_signup 코드를 기준으로 설명해보면 import static org.mockito.Mockito.verify;를 import 해주고 verify(passwordEncoder).encode(내가 넣은 패스워드);
verify(userRepository).findByUsername(내가 넣은 유저 이름);
verify(userRepository).save(any(User.class));
이렇게 내가 모킹한 클래스의 메소드가 잘 호출 되었는지를 확인할 수 있습니다.

또는 ArgumentCaptor를 사용해서 내가 모킹한 메소드의 결과를 가져와서 검증할 수 있습니다.
설명하자면 길어져서 관련 링크 하나 첨부해 드릴게요! 혹시나 추가 설명이 필요하면 면담 때 알려드릴게요. https://www.baeldung.com/mockito-argumentcaptor

 

코드

    //테스트 클래스에 선언
    @Captor
    ArgumentCaptor<User> argumentCaptor;
    
    
    @Test
    @DisplayName("회원가입 ")
    void test2() {
        // given
        UserRequestDto userRequestDto = new UserRequestDto("lucy", "11111111");
        User user = new User("lucy", "encodedPassword");

        given(userRepository.findByUsername("lucy")).willReturn(Optional.empty());

        // when
        userService.signup(userRequestDto);


        // then
        verify(passwordEncoder).encode("11111111");
        verify(userRepository).findByUsername("lucy");
        verify(userRepository).save(any(User.class));

        verify(userRepository).save(argumentCaptor.capture());
        assertEquals(user.getUsername(), argumentCaptor.getValue().getUsername());
    }

 

  • signup 메소드는 반환값이 없어서 중간에 생성된 user에 대해 테스트하지 못하였는데 ArgumentCaptor를 사용해 user 값을 가져올 수 있어서 신기했다.
  • 그 외에도 encode와 findByUsername 등이 실행되었는지 verify를 통해 테스트할 수 있었다.
    • verify(userRepository).save(any(User.class)); 에서 오류가 발생했는데 임포트가 잘못되어 있었다.
import static org.mockito.ArgumentMatchers.any;  // ArgumentMatchers.any를 임포트해서 오류 해결

4. 통합 테스트에서 환경 변수를 찾지 못함

답변:

application-test.properties 파일을 테스트 코드 디렉토리의 resources에 넣어주시고 환경 변수가 필요한 테스트 코드에 @ActiveProfiles("test")를 붙여주면 됩니다!

 

  • 추가적으로 이번에 팀과제를 하면서 테스트에서 따로 환경변수를 설정해줘야 한다는 것을 알게 되었다.