반응형
오늘 스프링 부트 개인 프로젝트를 진행하던 도중 서비스 레이어를 테스트하다 만난 오류다.
1. 오류 확인
1-1. 에러 로그
- 아래와 같이 log에 남았고 이게 뭔지 찾아보니 나는 예외를 던지도록 bad case를 테스트했는데 예외가 터지지 않은것이었다.
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
java.lang.AssertionError:
Expecting code to raise a throwable.
at com.jinan.profile.service.board.BoardServiceTest.validUserException(BoardServiceTest.java:230)
- 아래의 예외가 터진 서비스코드와 테스트 코드를 살펴보자
1-2. 서비스 코드
/**
* 게시글을 수정하기 전에 게시글을 작성한 사람과 수정하려는 사람이 동일한 사람인지 검증한다.
*/
public boolean validUser(Long boardId, String loginId) {
BoardDto boardDto = boardRepository.findById(boardId)
.map(BoardDto::fromEntity)
.orElseThrow(() -> new ProfileApplicationException(ErrorCode.USER_NOT_FOUND));
UserDto userDto = userRepository.findByLoginId(boardDto.userDto().loginId())
.map(UserDto::fromEntity)
.orElseThrow(() -> new ProfileApplicationException(ErrorCode.USER_NOT_FOUND));
// 시큐리티에 담긴 로그인id가 게시글 안에있던 유저정보에서 꺼내온 유저의 id와 다르다면 검증 실패다.
if (!loginId.equals(userDto.loginId())) {
return false;
}
return true;
}
1-3. 테스트 코드
@DisplayName("[bad]-게시글을 작성한 유저가 수정하고자하는 유저와 다르다면 검증은 실패한다.")
@Test
void validUserException() {
//given
User user = createUser("dig04058");
User savedUser = userRepository.save(user);
Board board = createBoard(savedUser, "test");
Board savedBoard = boardRepository.save(board);
//when & then
assertThatThrownBy(() -> boardService.validUser(savedBoard.getId(), "annonymousId"))
.isInstanceOf(ProfileApplicationException.class);
}
private User createUser(String loginId) {
return User.of(
loginId,
"wlsdks12",
"wlsdks",
"wlsdks12@naver.com",
RoleType.ADMIN,
UserStatus.Y
);
}
private Board createBoard(User user) {
return Board.of(
"테스트 게시글",
"테스트",
10,
20,
user
);
}
코드 설명
- createUser(), createBoard()는 테스트코드를 동작시키기 위해 유저, 게시글 엔티티를 만드는 factory method를 사용해서 미리 테스트용 데이터를 만든것이다.
- user, board 엔티티를 테스트코드 메서드 내부의 Transaction에 영속화 시켜서 사용하기 위해 repository의 save() 메서드를 호출해서 영속화 시킨다. (참고로 테스트 코드 클래스 상단에는 @Transactional을 적어줬다. -> 테스트가 끝나면 알아서 rollback)
- 테스트의 when&then파트 구성은 assertThatThrownBy() 메서드로 예외처리 검증 로직을 작성했다. 이 검증 메서드는 람다식으로 boardService안의 validUser()라는 메서드를 실행해서 던지는 예외를 검증하는 방식이다.
- 이때 validUser()의 첫번째 인자로는 위에서 세팅한 board 엔티티에서 id값(pk)을 꺼내와서 넣어준다. 두번째 인자로는 db에 없는 String값을 내맘대로 "annonymousId" 라는 값으로 세팅해줬다.
- 이제 테스트코드 작성을 완료했으니 실행했는데 맨위에 적어놓은 에러로그가 나왔다. -> 대체 왜 예외가 없이 코드가 성공했지....? 문명 id는 db에 존재하지 않는 값인데?
2. 원인 파악
- 에러의 원인을 파악하기 위해 Service 코드에 Debug를 찍어봤다.
/**
* 게시글을 수정하기 전에 게시글을 작성한 사람과 수정하려는 사람이 동일한 사람인지 검증한다.
*/
public boolean validUser(Long boardId, String loginId) {
BoardDto boardDto = boardRepository.findById(boardId)
.map(BoardDto::fromEntity)
.orElseThrow(() -> new ProfileApplicationException(ErrorCode.USER_NOT_FOUND));
UserDto userDto = userRepository.findByLoginId(boardDto.userDto().loginId())
.map(UserDto::fromEntity)
.orElseThrow(() -> new ProfileApplicationException(ErrorCode.USER_NOT_FOUND));
// 시큐리티에 담긴 로그인id가 게시글 안에있던 유저정보에서 꺼내온 유저의 id와 다르다면 검증 실패다.
if (!loginId.equals(userDto.loginId())) {
return false;
}
return true;
}
아닛..!??! 문제의 지점은 중간의 findByLoginId() 메서드의 매게변수였다.
- 왜냐하면 로직을 보면 지금 findByLoginId() 메서드에 주입되는 매게변수를 보면 boardDto.userDto().loginId()이고 이건 Board값에 세팅된 user의 loginId이다.
- 문제가 없어 보이지만 이러면 if문에 도달하기 전에 예외가 안터진다. 순서대로 확인해보자
- boarId는 1L 이라고 가정한다.
- boardRepository.findById(1L); 이걸로 board 값을 받아와서 dto로 변환해 줬다.
- 1L의 board_id를 가진 board엔티티는 그 안에 무조건 저장된 user엔티티가 있을것이다. -> 왜냐하면 user가 없으면 board 엔티티가 db에 저장이 안되도록 생성 로직을 만들어놓고 테스트로 검증까지 했기 때문이다.
- 그럼 이제 1L의 board_id를 가진 board엔티티 내부에 있는 user엔티티에서 loginId 값을 꺼낸다. -> 문제없다. 여기까지가 실제 게시글을 작성한 board에서 유저를 들고온 것이다.
- 자 그럼 지금까지 예외가 터지지 않았다. -> 왜냐? 당연히 board엔티티에 대한 저장에는 문제가 생길만한것이 없었기 때문에 잘 저장된 board안에서 잘 저장된 userId 값을 가져온 것이다.
- 그럼 다음 로직인 userRepository.findByLoginId()로 간다. -> 이제 여기에 인자로 받는 loginId가 문제가 된다.
- findByLoginId() 이 메서드에서 매게변수로 사용하는 loginId는 board_Id가 "1L"인 board엔티티의 내부에 세팅된 user엔티티의 loginId다. 즉, 무조건 존재하는 값이다. (예외 발생이 불가능하다. 왜냐? user엔티티가 db에 없을시에는 board 저장시에 예외가 터지도록 설정해놨으니 저건 무조건 존재한다.)
- 그럼 이제 이 findByLoginId()로 받아온 user엔티티는 당연히 board엔티티안에 세팅된 user 엔티티일 것이다.
- 그럼 맨 아래의 if문을 보자 여기서는 문제없이 로직이 동작해서 true, false가 반환될 것이다. 그럼 뭐가 잘못된거냐??
- 지금까지 "annonymous"라는 내가 세팅해준 loginId값은 단 한군데에서만 사용되었다. 바로 마지막 if문 안이다. 그럼 무슨의미가 있는가? 왜냐하면 나는 "annonymous"라는 값으로 findByLoginId()를 해서 "annonymous"라는 loginId를 가진 user엔티티를 받아오도록 만들었어야 하는데 그렇지 않았으니 board엔티티가 존재하기만 하면 무조건 user엔티티를 가져올수 있도록 로직을 작성했으니 실제 존재하지 않는 loginId를 매게변수로 줘도 예외가 발생할 일이 없었던 것이다.
- boarId는 1L 이라고 가정한다.
- 원인 파악을 완료했으니 수정을 진행하자
3. 코드 수정 및 검증
- 서비스코드 수정
/**
* 게시글을 수정하기 전에 게시글을 작성한 사람과 수정하려는 사람이 동일한 사람인지 검증한다.
*/
public boolean validUser(Long boardId, String loginId) {
BoardDto boardDto = boardRepository.findById(boardId)
.map(BoardDto::fromEntity)
.orElseThrow(() -> new ProfileApplicationException(ErrorCode.USER_NOT_FOUND));
// user정보를 board에서 꺼내는데 만약 board안에 user가 없으면 예외처리를 한다.
String boardLoginId = Optional.of(boardDto.userDto())
.orElseThrow(() -> new ProfileApplicationException(ErrorCode.USER_NOT_FOUND)).loginId();
if (!boardLoginId.equals(loginId)) {
return false;
}
return true;
}
- 테스트코드 수정
- 기존에는 예외를 던지면 통과하던 테스트 검증 방식에서 서비스 코드를 간략화시키고 최종적으로 true, false로 검증을 확인했다. 수정한 테스트에서는 검증이 실패해야하니 false를 반환해주면 되는것이다. (이건 예외처리 관련 오류 검증 테스트는 아니게 되었다.)
@DisplayName("[bad]-게시글을 작성한 유저가 수정하고자하는 유저와 다르다면 검증은 실패한다.")
@Test
void validUserException() {
//given
User user = createUser("dig04058");
User savedUser = userRepository.save(user);
Board board = createBoard(savedUser, "test");
Board savedBoard = boardRepository.save(board);
//when
boolean validUser = boardService.validUser(savedBoard.getId(), "annonymousId");
//then
assertThat(validUser).isFalse();
}
- 만약 예외처리를 통한 검증을 하고싶다면 위의 새롭게 작성한 서비스, 테스트 코드를 사용하지 말고 기존의 테스트 코드를 사용하면서 다음과 같이 서비스 코드만 수정해보자
- 아래의 코드처럼 수정하면 annonymousId는 db에 없으니 findByLoginId()에서 예외가 터질것이다. 이때 테스트코드는 위의 새로운 코드로 바꾸지말고 이전의 예외테스트 코드를 사용해야 한다.
- 아래처럼 if문의 내용도 바꿔주면 된다. -> board안에 들어있는 user의 loginId와 파라미터로 보낸 annonymousId로 받아온 user의 loginId값을 비교해주면 된다. 그럼 valid도 동작하고 board, user가 없을시에는 예외처리도 동작할 것이다.
- 아래의 코드처럼 수정하면 annonymousId는 db에 없으니 findByLoginId()에서 예외가 터질것이다. 이때 테스트코드는 위의 새로운 코드로 바꾸지말고 이전의 예외테스트 코드를 사용해야 한다.
/**
* 게시글을 수정하기 전에 게시글을 작성한 사람과 수정하려는 사람이 동일한 사람인지 검증한다.
*/
public boolean validUser(Long boardId, String loginId) {
BoardDto boardDto = boardRepository.findById(boardId)
.map(BoardDto::fromEntity)
.orElseThrow(() -> new ProfileApplicationException(ErrorCode.USER_NOT_FOUND));
UserDto userDto = userRepository.findByLoginId(loginId)
.map(UserDto::fromEntity)
.orElseThrow(() -> new ProfileApplicationException(ErrorCode.USER_NOT_FOUND));
// 게시글dto안에 세팅된 유저의 loginId와 파라미터로 지금 로그인된 유저의 loginId값을 받아서 세팅한 userDto안의 loginId값을 비교한다.
if (!boardDto.getUserDto.getLoginId().equals(userDto.loginId())) {
return false;
}
return true;
}
4. 느낀점
테스트 코드를 작성하다보니 확인하기 힘든 오류를 빠르게 찾고 검증할수가 있었다.
만약 테스트를 작성하지 않았다면 훨씬 늦게 알았을것이고 엄청 해매었을것이다.
앞으로도 테스트 코드를 작성하는것을 중요하게 생각해야겠다. 그리고 이왕이면 tdd방식으로 미리 테스트를 짜고 서비스코드를 만들었으면 이런 오류도 안생겼을거라는 생각이 크게 들었다.
2023.08.09 - [Spring 테스트코드/기초 지식] - Spring(스프링) - 테스트 코드의 기초(5) [CRUD 테스트]
반응형
'Spring > 테스트 코드' 카테고리의 다른 글
[스프링, 스프링부트] Spring test - 스프링 시큐리티의 authentication객체를 어떻게 사용해야 할까? (0) | 2023.08.22 |
---|---|
Spring 서비스 테스트 중 발견한 NPE 해결기 (0) | 2023.08.17 |
[스프링, 스프링 부트] Spring test - Mock에 대한 이해 (0) | 2023.08.09 |
[스프링, 스프링부트] Spring test - 테스트 코드의 기초(5) [CRUD 테스트] (0) | 2023.08.09 |
[스프링, 스프링 부트] Spring test - 테스트 코드의 기초(4) [mock 테스트] (0) | 2023.08.09 |