Service Mockito Unit Test Method
- @ExtendWith(MockitoExtension.class) : 테스트 클래스가 Mockito를 사용함을 의미한다.
- @Mock : 실제 구현된 객체 대신에 Mock 객체를 사용하게 될 클래스를 의미한다. 테스트 런타임 시 해당 객체 대신 Mock 객체가 주입되어 Unit Test가 처리된다.
- @InjectMocks : Mock 객체가 주입된 클래스를 사용하게 될 클래스를 의미합니다. 테스트 런타임 시 클래스 내부에 선언된 멤버 변수들 중에서 @Mock으로 등록된 클래스의 변수에 실제 객체 대신 Mock 객체가 주입되어 Unit Test가 처리된다.
@ExtendWith(MockitoExtension.class)
class PostServiceTest {
@InjectMocks
PostService postService;
@Mock
PostRepository postRepository ;
@Mock
UserRepository userRepository ;
@Mock
LikeRepository likeRepository;
}
위에서는 PostService 단을 테스트 하기위해서 클래스 상단에 @ExtendWith(MockitoExtension.class) 를 붙여주었고 @InjectMocks 어노테이션을 PostService에 적용하여 Mock객체가 주입 된 클래스를 사용하게 될 클래스를 명시해주었다. 그리고 마지막으로 @Mock 어노테이션이 붙은 Respository 클래스들은 테스트 런타임시에 PostService 단에 Mock객체가 주입된다.
TDD 적용
- Given: 테스트를 위한 준비 과정이다. 변수를 선언하고, Mock 객체에 대한 정의도 함께 작성한다.
- When: 테스트를 실행하는 과정이다. 테스트하고자 하는 내용을 작서한다.
- Then: 테스트를 검증하는 과정이다. 예상한 값과 결괏값이 일치하는 지 확인한다.
의역하자면, 테스트 실행을 위해 베이스라인으로서 사용되는 객체들의 고정된 상태이다. 테스트 픽스처의 목적은 결과를 반복가능할 수 있도록 알 수 있고, 고정된 환경에서 테스트할 수 있음을 보장하기 위함이다. 중복 발생되는 무언가(행위)를 고정시켜 한곳에 관리하도록 하겠다는 개념.
나는 postService단에서 많이 사용되는 객체들을 test 패키지 하위에 만들어줄 것이다.
serviceTest 클래스에서 다음과 같이 각각의 필드들의 정보를 임의로 값을 설정해 둔 객체
@Data public static class PostAndUser{ private Long postId; private Long userId; private String userName; private String password; private String title; private String body; //PostAndUser.getDto()라는 메소드를 사용하면 바로 값을 가져올 수 있다. public static PostAndUser getDto() { PostAndUser p = new PostAndUser(); p.setPostId(1L); p.setUserId(1L); p.setUserName("test"); p.setPassword("1234"); p.setTitle("테스트 제목"); p.setBody("테스트 내용"); return p; } }
public class UserEntityFixture {
public static User get(String userName,String password) {
return User.builder()
.id(1l)
.userName(userName)
.password(password)
.registeredAt(String.valueOf(Timestamp.from(Instant.now())))
.role(UserRole.USER)
.build();
}
}
public class PostEntityFixture {
public static Post get(String userName, String password) {
Post post = Post.builder()
.id(1L)
.user(UserEntityFixture.get(userName, password))
.title("title")
.body("body")
.build();
return post;
}
public static Post get(User user) {
Post post = Post.builder()
.id(1L)
.user(user)
.title("title")
.body("body")
.build();
return post;
}
}
@Test
@DisplayName("조회 성공")
void 글_조회_단건() {
//postAndUser의 객체
PostAndUser postAndUser = new PostAndUser();
PostAndUser dto = postAndUser.getDto();
//User 픽스처로 생성된 user엔티티(매개변수는 postAndUser픽스처의 값)
User user = UserEntityFixture.get(dto.getUserName(), dto.getPassword());
//Post 픽스처로 생성된 user엔티티(매개변수는 User픽스처의 값)
Post post = PostEntityFixture.get(user);
//postRepository에서 조회 메서드를 사용하면 post엔티티를 반환한다는 가정
when(postRepository.findById(dto.getPostId())).thenReturn(Optional.of(post));
//postService에서 포스트 단건조회
PostSelectResponse postSelectResponse = postService.getPost(dto.getPostId());
//픽스처의 값과 service 반환 값의 테스트
assertEquals(dto.getUserName(), postSelectResponse.getUserName());
}
@Nested
@DisplayName("포스트 등록")
class PostAdd {
@Test
@DisplayName("글 등록 성공 테스트")
void 등록성공() {
PostAndUser dto = PostAndUser.getDto();
//User 픽스처로 생성된 user엔티티(매개변수는 postAndUser픽스처의 값)
User user = UserEntityFixture.get(dto.getUserName(), dto.getPassword());
//Post 픽스처로 생성된 user엔티티(매개변수는 User픽스처의 값)
Post post = PostEntityFixture.get(user);
//postservice의 작성에서 우선 회원을 찾을 것이다. userRepository.findOptionalByUserName
when(userRepository.findOptionalByUserName(any())).thenReturn(Optional.of(user));
//그 후 postRepository.save()를 적용
when(postRepository.save(any())).thenReturn(post);
//postService에 들어갈 변수
PostAddRequest postAddRequest = new PostAddRequest(dto.getTitle(), dto.getBody());
PostAddResponse postAddResponse = postService.addPost(postAddRequest, dto.getUserName());
Assertions.assertEquals(postAddResponse.getPostId(), dto.getPostId());
//assertDoesNotThrow() 이상이 없다면 통과
Assertions.assertDoesNotThrow(() -> postAddResponse);
}
//포스트생성시 유저가 존재하지 않을 때 에러
@Test
@WithMockUser
@DisplayName("등록 실패 : 유저 존재 하지 않음")
void 등록_실패() {
PostAndUser dto = PostAndUser.getDto();
User user = UserEntityFixture.get(dto.getUserName(), dto.getPassword());
Post post = PostEntityFixture.get(user);
// 유저 존재 하지 않은 상황 가정
when(userRepository.findOptionalByUserName(any())).thenReturn(Optional.empty());
when(postRepository.save(any())).thenReturn(mock(Post.class));
PostAddRequest postAddRequest = new PostAddRequest(dto.getTitle(), dto.getBody());
UserException userException
= assertThrows(UserException.class, () -> postService.addPost(postAddRequest, dto.getUserName()));
Assertions.assertEquals(userException.getErrorCode(), ErrorCode.USERNAME_NOT_FOUND);
Assertions.assertEquals(userException.getMessage(),"회원가입 후 작성해주세요");
}
}
@Nested
@DisplayName("포스트 수정")
class PostUpdate{
@Test
@DisplayName("수정 실패 : 작성자!=유저")
void 수정_실패_작성자_불일치() {
PostAndUser dto = PostAndUser.getDto();
User user1 = UserEntityFixture.get("박지성", "password");
User user2 = UserEntityFixture.get("손흥민", "password2");
//user2가 작성한 post
Post postByUser2 = PostEntityFixture.get(user2);
//수정 request
PostUpdateRequest postUpdateRequest = new PostUpdateRequest("수정될 제목", "수정될 내용");
// post를 작성한 user는 user1
when(postRepository.findById(dto.getPostId()))
.thenReturn(Optional.of(Post.of("title","body",user1)));
// post를 수정 시도한 user는 user2
when(userRepository.findOptionalByUserName(any()))
.thenReturn(Optional.of(User.of(user2.getUsername(), user2.getPassword())));
UserException userException = assertThrows(UserException.class,
() -> postService.updatePost(dto.getPostId(), postUpdateRequest, dto.getUserName()));
//ErrorCode 검사
Assertions.assertEquals(userException.getErrorCode(),ErrorCode.INVALID_PERMISSION);
}
@Test
@DisplayName("수정 실패 : 포스트 존재하지 않음")
@WithMockUser
void 수정_실패_포스트_존재하지않음() {
PostAndUser postAndUser = PostAndUser.getDto();
//수정 request
PostUpdateRequest postUpdateRequest = new PostUpdateRequest("수정될 제목", "수정될 내용");
//해당 post존재하지 않음 Optional.of(mock(Post.class)) <- 이것을 넣으면 에러가 발생하면 안됨
when(postRepository.findById(any())).thenReturn(Optional.empty());
when(userRepository.findOptionalByUserName(any())).thenReturn(Optional.of(mock(User.class)));
PostException postException =
assertThrows(PostException.class,
() -> postService.updatePost(postAndUser.getPostId(), postUpdateRequest, postAndUser.getUserName()));
Assertions.assertEquals(postException.getErrorCode(),ErrorCode.POST_NOT_FOUND);
}
@Test
@DisplayName("수정 실패 : 회원 존재하지 않음")
@WithMockUser
void 수정_실패_회원_존재하지않음() {
PostAndUser postAndUser = PostAndUser.getDto();
//수정 request
PostUpdateRequest postUpdateRequest = new PostUpdateRequest("수정될 제목", "수정될 내용");
//해당 user존재 하지않는 상황가정
when(postRepository.findById(any())).thenReturn(Optional.of(mock(Post.class)));
when(userRepository.findOptionalByUserName(any())).thenReturn(Optional.empty());
UserException userException =
assertThrows(UserException.class,
() -> postService.updatePost(postAndUser.getPostId(), postUpdateRequest, postAndUser.getUserName()));
Assertions.assertEquals(userException.getErrorCode(),ErrorCode.USERNAME_NOT_FOUND);
}
@Test
@DisplayName("포스트 수정 성공")
void postUpdateSuccess() {
PostAndUser postAndUser = PostAndUser.getDto();
PostUpdateRequest postUpdateRequest = new PostUpdateRequest("수정될 제목", "수정될 내용");
//수정 성공 로직
when(userRepository.findOptionalByUserName(any())).thenReturn(Optional.of(mock(User.class)));
when(postRepository.findById(any())).thenReturn(Optional.of(mock(Post.class)));
PostUpdateResponse postUpdateResponse
= postService.updatePost(postAndUser.getPostId(), postUpdateRequest, postAndUser.getUserName());
Assertions.assertEquals(postUpdateResponse.getMessage(),"포스트 수정 완료");
}
}
@Nested
@DisplayName("포스트 삭제")
class PostDelete{
@Test
@WithMockUser
@DisplayName("포스트 삭제 성공")
void 포스트_삭제_성공() {
PostAndUser postAndUser = PostAndUser.getDto();
User user = UserEntityFixture.get(postAndUser.getUserName(), postAndUser.getPassword());
//수정 성공 로직
when(userRepository.findOptionalByUserName(user.getUsername())).thenReturn(Optional.of(user));
when(postRepository.findById(any())).thenReturn(Optional.of(PostEntityFixture.get(user)));
PostDeleteResponse postDeleteResponse
= postService.deletePost(postAndUser.getPostId(), postAndUser.getUserName());
Assertions.assertEquals(postDeleteResponse.getMessage(),"포스트 삭제 완료");
Assertions.assertEquals(postDeleteResponse.getPostId(),1L);
}
@Test
@DisplayName("포스트 삭제 작성자 불일치")
void 삭제_실패_작성자_불일치() throws Exception {
PostAndUser dto = PostAndUser.getDto();
//유저1 (글 작성한 회원)
User 유저1 = UserEntityFixture.get("유저1", "1234");
//유저2 (글 삭제 시도 회원)
User 유저2 = new User(2l, "유저2", "1234");
when(postRepository.findById(any())).thenReturn(Optional.of(PostEntityFixture.get(유저1)));
when(userRepository.findOptionalByUserName(any())).thenReturn(Optional.of(유저2));
UserException userException =
assertThrows(UserException.class, () -> postService.deletePost(dto.getPostId(), dto.getUserName()));
//에러 코드 확인
Assertions.assertEquals(userException.getErrorCode(),ErrorCode.INVALID_PERMISSION);
}
@Test
@WithMockUser
@DisplayName("삭제 실패 : 포스트 존재 하지 않음")
void 삭제_실패_포스트_존재x() throws Exception {
PostAndUser dto = PostAndUser.getDto();
//포스트 존재하지 않는 상황 가정
when(postRepository.findById(any())).thenReturn(Optional.empty());
PostException postException =
assertThrows(PostException.class, () -> postService.deletePost(dto.getPostId(), dto.getUserName()));
Assertions.assertEquals(postException.getErrorCode(),ErrorCode.POST_NOT_FOUND);
Assertions.assertEquals(postException.getMessage(),"해당 글은 존재하지 않아서 삭제할 수 없습니다.");
}
}