목표
📢 댓글 작성 기능을 구현해보자
접근 방법
- JPA 연관관계
- 한 유저는 여러 개의 댓글을 작성할 수 있다.
- 한개의 포스트는 여러개의 댓글을 가실 수 있다.
- 한개의 댓글은 한명의 작성자와 1개의 포스트 ID를 가질 수 있다.
- 결론적으로 댓글과 유저 혹은 포스트의 관계는 다대일의 매핑관계를 가질 수 있다.
- 댓글은 로그인을 한 유저만 작성할 수 있다.
핵심 키워드
- JPA 다대일 단방향 매핑
- Security FIlter 권한 설정
TO-DO
댓글 작성 컨트롤러 테스트 작성
<@WebMvcTest(CommentController.class)
@MockBean(JpaMetamodelMappingContext.class)
class CommentControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
CommentService commentService;
@Autowired
ObjectMapper objectMapper;
CommentResponse commentResponse = CommentResponse.builder()
.id(1L)
.comment("테스트입니다.")
.userName("홍길동")
.postId(1L)
.createdAt(LocalDateTime.now())
.build();
@Test
@DisplayName("댓글 작성 성공")
@WithMockUser
void comment_write_SUCCESS() throws Exception {
when(commentService.write(any(), any(), any()))
.thenReturn(commentResponse);
mockMvc.perform(post("/api/v1/posts/1/comments")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(new CommentRequest("테스트입니다."))))
.andDo(print())
.andExpect(status().isOk());
}
@Test
@DisplayName("댓글 작성 실패1_로그인을 하지 않은 경우")
@WithMockUser
void comment_write_FAILD_login() throws Exception {
when(commentService.write(any(), any(), any()))
.thenThrow(new AppException(ErrorCode.INVALID_PERMISSION, ErrorCode.INVALID_PERMISSION.getMessage()));
mockMvc.perform(post("/api/v1/posts/1/comments")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(new CommentRequest("테스트입니다."))))
.andDo(print())
.andExpect(status().isUnauthorized());
}
@Test
@DisplayName("댓글 작성 실패2_포스트가 없는 경우")
@WithMockUser
void comment_write_FAILD_() throws Exception {
when(commentService.write(any(), any(), any()))
.thenThrow(new AppException(ErrorCode.POST_NOT_FOUND, ErrorCode.POST_NOT_FOUND.getMessage()));
mockMvc.perform(post("/api/v1/posts/1/comments")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(new CommentRequest("테스트입니다."))))
.andDo(print())
.andExpect(status().isNotFound());
}
}
댓글 작성 서비스 테스트 구현
class CommentServiceTest {
CommentService commentService;
PostRepository postRepository = mock(PostRepository.class);
UserRepository userRepository = mock(UserRepository.class);
CommentRepository commentRepository = mock(CommentRepository.class);
User user = User.builder()
.userId(1L)
.userName("홍길동")
.password("0000")
.build();
Post post = Post.builder()
.id(1L)
.title("제목")
.body("내용입니다.")
.user(user)
.build();
Comment comment = Comment.builder()
.id(1L)
.comment("테스트입니다.")
.user(user)
.post(post)
.build();
@BeforeEach
void setUp() {
commentService = new CommentService(userRepository,postRepository,commentRepository);
}
@Test
@DisplayName("댓글 작성 성공")
void comment_write_SUCCESS() {
when(userRepository.findByUserName(any()))
.thenReturn(Optional.of(user));
when(postRepository.findById(any()))
.thenReturn(Optional.of(post));
when(commentRepository.save(any()))
.thenReturn(comment);
assertDoesNotThrow(() -> commentService.write(user.getUserName(),post.getId(),comment.getComment()));
}
@Test
@DisplayName("댓글 작성 실패1_로그인을 안한 경우")
void comment_write_FALID_login() {
when(userRepository.findByUserName(any()))
.thenReturn(Optional.empty());
when(postRepository.findById(any()))
.thenReturn(Optional.of(post));
when(commentRepository.save(any()))
.thenReturn(comment);
AppException exception = assertThrows(AppException.class, () -> commentService.write(user.getUserName(),post.getId(),comment.getComment()));
assertEquals(ErrorCode.USERNAME_NOT_FOUND, exception.getErrorCode());
}
@Test
@DisplayName("댓글 작성 실패2_포스트가 없는 경우")
void comment_write_FALID_post() {
when(userRepository.findByUserName(any()))
.thenReturn(Optional.of(user));
when(postRepository.findById(any()))
.thenReturn(Optional.empty());
when(commentRepository.save(any()))
.thenReturn(comment);
AppException exception = assertThrows(AppException.class, () -> commentService.write(user.getUserName(),post.getId(),comment.getComment()));
assertEquals(ErrorCode.POST_NOT_FOUND, exception.getErrorCode());
}
}
댓글 작성 컨트롤러 구현
@RestController
@RequestMapping("/api/v1/posts/{postId}/comments")
@RequiredArgsConstructor
public class CommentController {
private final CommentService commentService;
@PostMapping
public ResponseEntity<Response> write(Authentication authentication, @PathVariable Long postId, @RequestBody CommentRequest commentRequest) {
String userName = authentication.getName();
CommentResponse commentResponse = commentService.write(userName, postId, commentRequest.getComment());
return ResponseEntity.ok().body(Response.of("SUCCESS", commentResponse));
}
}
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class CommentRequest {
private String comment;
}
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class CommentResponse {
private Long id;
private String comment;
private String userName;
private Long postId;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd' 'HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime createdAt;
public static CommentResponse of(Comment comment) {
return CommentResponse.builder()
.id(comment.getId())
.comment(comment.getComment())
.userName(comment.getUser().getUserName())
.postId(comment.getPost().getId())
.createdAt(comment.getCreatedAt())
.build();
}
}
댓글 작성 서비스 구현
@Service
@RequiredArgsConstructor
public class CommentService {
private final UserRepository userRepository;
private final PostRepository postRepository;
private final CommentRepository commentRepository;
public CommentResponse write(String userName, Long postId, String comment) {
User findUser = AppUtil.findUser(userRepository, userName);
Post findPost = AppUtil.findPost(postRepository, postId);
Comment saveComment = Comment.of(comment, findUser, findPost);
saveComment = commentRepository.save(saveComment);
return CommentResponse.of(saveComment);
}
}
public interface CommentRepository extends JpaRepository<Comment, Long> {
}
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Getter
public class Comment extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String comment;
@ManyToOne
@JoinColumn(name = "userId")
private User user;
@ManyToOne
@JoinColumn(name = "postId")
private Post post;
public static Comment of(String comment, User user, Post post) {
return Comment.builder()
.comment(comment)
.user(user)
.post(post)
.build();
}
}
📖 회고록
- mockMvc에 관해 예외가 발생했다.
- perform함수의 content로 any()를 사용한 것이 원인이였다.
- 하지만 발생한 이유는 알지 못했다.
- mockMvc에 대해 자세한 공부가 필요하다고 느꼈다.