코드 그라데이션

20230210 테스트 코드 추가 설명 (1) 회원 정보 업데이트 성공 테스트 본문

Spring/Test Code

20230210 테스트 코드 추가 설명 (1) 회원 정보 업데이트 성공 테스트

완벽한 장면 2023. 2. 15. 00:48

<TestCode>

우선 Test의 의미 자체만 생각해보자!

 

무엇을 테스트해야 하는가?

"그 기능에 대한 값을 넣었을 때, 그 기능이 성공적으로 작동하느냐" 를 테스트해봐야지

내가 작성한 updateProfile() 기능을 테스트한다면 무엇을 테스트해봐야 할까.
1. 업데이트가 잘 되는지 확인.
- 잘 된다는 건, 정상적인 값이 들어오고, 실제로 데이터베이스의 값이 바뀐다거나 그런 것들을 확인해야 하고,
- 업데이트가 안 되는 상황도 확인해야 한다

여기서는 두 가지를 확인해봐야지
1) 사용자가 이미 있어서 업데이트가 완료되었다는 말이 나오거나 
2) 사용자가 없어서 예외 메시지가 터진다거나

 

일단 UserService 인터페이스와 UserServiceImpl 클래스의 테스트 할 내 부분 원본 코드를 가져와보자.

 UserService

  public interface UserService {
  
   String updateProfile(Long profileId, ProfileRequestDto request);

}

UserServiceImpl 

@Service
@RequiredArgsConstructor

public class UserServiceImpl implements UserService { 

  private final UserRepository userRepository;
  private final JwtUtil jwtUtil;
  private final PasswordEncoder passwordEncoder;
  
    @Override
  public String updateProfile(Long userId, ProfileRequestDto request) {
    Profile profileSaved = userRepository.findById(userId)
        .orElseThrow(() -> new IllegalArgumentException("회원 없음")).getProfile();
    profileSaved.update(request.getNickname(), request.getImg_url());
    return "해당 프로필이 업데이트 완료되었습니다";
  }

  @Override
  public ProfileResponseDto showProfile(Long userId) {
    Profile profile = userRepository.findById(userId)
        .orElseThrow(() -> new IllegalArgumentException("회원 없음")).getProfile();
    return new ProfileResponseDto(profile);
  }

 

실제 코드 짜보기  : (1) 회원가입 성공 테스트

먼저, 테스트 어노테이션을 붙이고, 메서드 명을 붙여주면 된다.
그런데 메서드명은 사실 별 의미는 없다.
@DisplayName("")은 테스트의 의미를 더 명확하게 해주기 위해 붙여주는 사족 정도로 생각하면 된다.
그래도 최대한 써준다.

우리가 이 메서드를 테스트 하기 위해서는 본 코드의 Service단을 가져와야 하잖아.
그런데 Service 단의 @Service는 스프링이 관리하는 빈이야.

지금 여기서는 UserServiceImple만 테스트 해보고 싶은 것이잖아.
그래서 UserServiceImpl만 받은 다음에
@InjectMocks
UserServiceImple userService;

UserRepository는 흉내만 내주면 된다.
@Mock
UserRepository userRepository;

그리고 지금 테스트 코드에서는 userRepository는 가짜거든요.
그래서 UserRepository를 호출하면 에러가 나요.

지금 진짜 서비스 단에서는 
userRepository.findById(userId)라고 메서드가 구성되어 있는데, 
테스트 단에서는 "findById가 호출이 되면, 그냥 이걸 그대로 줘라" 라고 하드코딩 하면 된다.

이게 무슨 말이냐면, 
UserService에서 signup() 메서드를 보면,
// 회원 중복 확인 에서
userRepository.existsUsername(username); 이라는 메서드를 쓰고 있죠.
얘는 실제로 데이터베이스에 가서 확인을 하고 쿼리를 날리는 메서드잖아요.

그럼 테스트 서비스단에서 보면,
given(userRepository.existsByUsername(username).willReturn(false);
유저레포지토리에서 existByUsername이라는 메서드가 불리는 상황이 오면, false를 리턴하겠다는 뜻.

무슨 말이냐면, 얘가 호출이 되면, 실제로 동작하라는 게 아니라 그냥 false를 줘라 라는 뜻.
"나는 껍데기 뿐이라 동작을 못하니까 정해진 값을 주겠다"라는 것이지

근데 지금은 signup() 처럼 userRepository는 껍데기만 만들어놓고 Service 단만 테스트를 해보자.
그래서 우리도 틀을 만들면 돼,
지금 서비스 단에서 userRepository에서 findById(userId)가 호출이 되면, 원래는 실제로 돌아갈 거란 말이죠

findById를 하면 User가 만들어져요. 프로필이 아니라.
User()안에는 username, password, phoneNumber, region이 들어가야지.
유저아이디도 하나 설정해줘야지

똑같이 만들어보자.
void updateProfile() {
	Long userId = 임의값;
	//Profile profile() = new Profile(); 이게 아니고
    User user = new User();
    
	given(userRepository.findById(무언가)).willReturn(무언가);
    // given(userRepository.findById(userId).willReturn(Optional.of(user));
    // userRepository에서 findById를 하면 그냥 내가 지정해주는 값을 반환해! 지금 옵셔널로 받고 있잖아.

}



이제 그러면, userService에서 updateProfile(userId를 받아서)을 수행하면 
어떤 것을 수행하는지 콤마 뒤에 적어준다(userId,   )
그런데, 요청이 어떻게 만들어지는지 보면(ctrl 클릭), 
ProfileRequestDto에서 nickName과 img_url을 받아서 돌려주는 것을 확인할 수 있다.
(즉, 프로필을 구성하는 것이 nickName과 img_url이다.)
그래서 ProfileRequestDto에서 값을 가져오는 식을 하나 더 추가해줘야해
여기 ProfileRequestDto에는 @builder어노테이션이 붙어있기 때문에, .으로 연결해서 불러온다.

"빌더를 왜 쓰죠?"
setter를 쓰지 말래서, 대용으로 쓰는 것.

값을 넣어줄 때, username password phoneNumber region 모두 String 타입이기 때문에
순서가 바뀌어도 에러가 나지 않음.
그런데 사실상 이건 잘못된 거잖아.
그런데 builder를 써 주면 순서가 중요하기 때문에 섞일 우려가 없어져.

void updateProfile() {
	Long userId = 임의값;
	//Profile profile() = new Profile(); 이게 아니고
    User user = new User();
    
    ProfileRequestDto.builder().nickName(무언가).img_url(무언가).build();
    
	given(userRepository.findById(무언가)).willReturn(무언가);
    // given(userRepository.findById(userId).willReturn(Optional.of(user));
    // userRepository에서 findById를 하면 그냥 내가 지정해주는 값을 반환해!
	
    userService.updateProfile(userId, .....  )
}

그럼 userService에서 userId를 받아 updateProfile을 실행했을 때, 어떤 값을 만들어주냐
: RequestDto에 들 값을 만들어주지. ...에는 requestDto가 들어가야함.
void updateProfile() {
	Long userId = 임의값;
	//Profile profile() = new Profile(); 이게 아니고
    User user = new User();
    
    ProfileRequestDto requestDto = ProfileRequestDto.builder().nickName(무언가).img_url(무언가).build();
    
	given(userRepository.findById(무언가)).willReturn(무언가);
    // given(userRepository.findById(userId).willReturn(Optional.of(user));
    // userRepository에서 findById를 하면 그냥 내가 지정해주는 값을 반환해!
	
    userService.updateProfile(userId, requestDto);
}

그런데 프로필도 빌더가 들어가있으니까,
그냥 user도 빌더로 만들어버리자. 프로필은 생성자가 있으니까 new로 만들어준다.

즉, 유저를 바꾸는 게 아니라 유저의 프로필을 바꾸는 것이니까, 
프로필을 가진 유저를 다시 만들어줬고, 이게 바뀌었다고 하려면 처음에는 별명이 banana였고 이미지url이 before였는데
바뀐 게 apple, after이 되면 성공인 거야.
void updateProfile() {
	Long userId = 임의값;
	
    User user = new User("testuser", "1234", new Profile("banana(별명)","before(이미지url)")); 
            
    
    ProfileRequestDto requestDto = ProfileRequestDto.builder().nickname("apple").img_url("after").build();
    
	given(userRepository.findById(무언가)).willReturn(무언가);
    // given(userRepository.findById(userId).willReturn(Optional.of(user));
    // userRepository에서 findById를 하면 그냥 내가 지정해주는 값을 반환해!
	
    userService.updateProfile(userId, requestDto);
}

이제 마지막으로, 테스트가 잘 수행되었는지 확인하기 위해 결과를 반환하는 데서드를 써줘야지.
문법은 updateProfile에서 getNickname을 했을 때, 이것이 requestDto에서 getNickname을 해온 값과 같은지를 확인해주면 된다.
void updateProfile() {
	Long userId = 임의값;
	
    User user = new User("testuser", "1234", new Profile("banana(별명)","before(이미지url)")); 
            
    
    ProfileRequestDto requestDto = ProfileRequestDto.builder().nickname("apple").img_url("after").build();
    
	given(userRepository.findById(무언가)).willReturn(무언가);
    // given(userRepository.findById(userId).willReturn(Optional.of(user));
    // userRepository에서 findById를 하면 그냥 내가 지정해주는 값을 반환해!
	
    userService.updateProfile(userId, requestDto);
    assertThat(.....) // if문 쓰지 않는다. 문법임.
}

 

[실제 함께 작성한 테스트 코드 : (1) 회원 정보 업데이트 성공 테스트]

@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {

@Mock
UserRepository userRepository;

@InjectMocks
UserServiceImple userService;

 @Test
 @DisplayName("회원정보 업데이트 성공 테스트")
 void updateProfile() {
 
 //given
  Long userId = 1L; // 데이터베이스에 저장되어 있는 사용자의 primary key
  
  // User user = new User("testuser", "1234", "01012345678", "서울"); 이게 아니고,
  
  // 데이터베이스에 저장되어 있는 사용자의 역할
  User user = new User("testUser", "1234", new Profile("banana", "before"));
  
  // 업데이트할 프로필의 정보 : 업데이트가 제대로 일어났다면 실행했을 때 닉네임과 imgurl이 바뀌어 있겠지
  ProfileRequestDto requestDto = ProfileRequestDto.builder().nickName("Apple").img_url("after").build();
  
  given(userRepository.findById(userId).willReturn(Optional.of(user));
  
  //when
  userService.updateProfile(userId, requestDto);
  
  //then
  Profile updatedProfile = user.getProfile();
  assertThat(updatedProfile.getNickname()).isEqualTo(requestDto.getNickname());
  assertThat(updatedProfile.getImg_url()).isEqualTo(requestDto.getImg_url());
 }
}

근데 이렇게 만들면 실패해.

왜? 메시지를 보면 expected 값은 "apple"인데,

"banana" 가 들어왔다고 나와.

 

왜일까?

우리가 테스트코드 짤 때 user을 데이터베이스에서 준 것이 아니라 직접 하드코딩을 했지(직접 우리가 만든 값을 준 거고).

ProfileSaved.update(...) 메서드를 실행했을 때, 실제로 UserServiceImple에서는 문제 없이 업데이트가 잘 될 거예요.

(데이터베이스와 연동이 되어 있기 때문에...)

근데 우리는 지금 테스트코드에서 DB를 Mocking 했죠.

그런데 변화는 실제  데이터베이스에서 일어나는 코드란 말이예요.

따라서 지금 코드는 바뀌었는지 확인하려면 직접 데이터베이스를 까보는 수밖에 없다.

 

그래서 보통은 return을 하면 리턴한 결과를 직접 줘요. 메시지를 주지 않고.

근데 지금 내가 커밋해서 올려놓은 코드에는 메시지 값으로 주는 것으로 되어 있잖아요.

 

그래서 우리가 메서드의 반환값을 현재 String으로 적어놨지만, Profile을 반환한다고 바꿔보자.

그럼 확인이 가능해진다.

아까는 오직 변경된 값을 데이터베이스 내부에서만 확인이 가능한 구조였는데, 

지금은 메서드의 리턴 자체를 변경된 값을 준다고 바꿨지

 

혹은 User를 반환값으로 주겠다고 해도 됨.

왜냐면 프로필은 사실 User가 가진 값이니까.

 

하지만 Profile을 반환하게 만들었다.

그러면, UserService 인터페이스의 메서드에도 반환값을 바꿔줘야지

 

여기까지 바꾼 내용(위랑 비교해서 보자)

  public interface UserService {
  
   Profile updateProfile(Long profileId, ProfileRequestDto request);

}
@Service
@RequiredArgsConstructor

public class UserServiceImpl implements UserService { 

  private final UserRepository userRepository;
  private final JwtUtil jwtUtil;
  private final PasswordEncoder passwordEncoder;
  
    @Override
  public Profile updateProfile(Long userId, ProfileRequestDto request) {
    Profile profileSaved = userRepository.findById(userId)
        .orElseThrow(() -> new IllegalArgumentException("회원 없음")).getProfile();
    profileSaved.update(request.getNickname(), request.getImg_url());
    return ProfileSaved;
  }

 

그러면 테스트코드도 조금의 수정을 해주자.

@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {

@Mock
UserRepository userRepository;

@InjectMocks
UserServiceImple userService;

 @Test
 @DisplayName("회원정보 업데이트 성공 테스트")
 void updateProfile() {
 
 //given
  Long userId = 1L; // 데이터베이스에 저장되어 있는 사용자의 primary key
  
  // User user = new User("testuser", "1234", "01012345678", "서울"); 이게 아니고,
  
  // 데이터베이스에 저장되어 있는 사용자의 역할
  User user = new User("testUser", "1234", new Profile("banana", "before"));
  
  // 업데이트할 프로필의 정보 : 업데이트가 제대로 일어났다면 실행했을 때 닉네임과 imgurl이 바뀌어 있겠지
  ProfileRequestDto requestDto = ProfileRequestDto.builder().nickName("Apple").img_url("after").build();
  
  given(userRepository.findById(userId).willReturn(Optional.of(user));
  
  //when
  // 이거 삭제 userService.updateProfile(userId, requestDto);
  Profile updatedProfile = userServece.updateProfile(userId, requestDto);
  // 이렇게 되면 우리가 업데이트된 프로필 정보를 받아서 확인할 수 있게 된다.
  
  
  //then
  Profile updatedProfile = user.getProfile();
  assertThat(updatedProfile.getNickname()).isEqualTo(requestDto.getNickname());
  assertThat(updatedProfile.getImg_url()).isEqualTo(requestDto.getImg_url());
 }
}

여기서 변수 대소문자만 잘 구분하자.

지금 nickname이랑 nickName이랑 혼재되어있어서 테스트가 안 돌아갈 수도 있기 때문에,

그냥 일괄적으로 nickName으로 맞춰준다. (-51분) 끝!

 

 

 

728x90
Comments