공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.
이번 글에서는 댓글 관리 페이지의 기능을 테스트하는 방법에 대해 다룬다.
기본적인 흐름은 이전에 진행한 게시글 관리 페이지 테스트와 동일한 구조로, 서비스 계층과 컨트롤러를 분리하여 테스트를 진행한다.
1. 서비스 계층 정의
먼저 ArticleCommentManagementService를 통해 댓글을 조회하고 삭제하는 기능을 정의한다.
이 서비스 계층은 실제 비즈니스 로직을 처리하며, 컨트롤러에서 호출되는 핵심 역할을 담당한다.
package org.example.projectboardadmin.service;
import lombok.RequiredArgsConstructor;
import org.example.projectboardadmin.dto.ArticleCommentDto;
import org.springframework.stereotype.Service;
import java.util.List;
@RequiredArgsConstructor
@Service
public class ArticleCommentManagementService {
public List<ArticleCommentDto> getArticleComments() {
return List.of();
}
public ArticleCommentDto getArticleComment(Long articleCommentId){
return null;
}
public void deleteArticleComment(Long articleCommentId) {
}
}
- getArticleComments()
- 댓글 목록을 가져오는 메서드로, 현재는 빈 리스트를 반환하고 있다.
- getArticleComment(Long articleCommentId)
- 특정 댓글을 ID로 조회하는 메서드로, 현재는 null을 반환하고 있다.
- deleteArticleComment(Long articleCommentId)
- 특정 댓글을 삭제하는 메서드로, 현재는 아무 동작도 수행하지 않는다.
2. DTO 정의
댓글과 관련된 데이터를 주고받기 위한 ArticleCommentDto와 API 응답을 처리하기 위한 ArticleCommentClientResponse를 정의한다.
package org.example.projectboardadmin.dto;
import java.time.LocalDateTime;
public record ArticleCommentDto(
Long id,
Long articleId,
UserAccountDto userAccount,
Long parentCommentId,
String content,
LocalDateTime createdAt,
String createdBy,
LocalDateTime modifiedAt,
String modifiedBy
) {
public static ArticleCommentDto of(Long id, Long articleId, UserAccountDto userAccount, Long parentCommentId, String content, LocalDateTime createdAt, String createdBy, LocalDateTime modifiedAt, String modifiedBy) {
return new ArticleCommentDto(id, articleId, userAccount, parentCommentId, content, createdAt, createdBy, modifiedAt, modifiedBy);
}
}
- of() 메서드
- 정적 팩토리 메서드로, ArticleCommentDto 객체를 쉽게 생성하기 위해 사용한다.
package org.example.projectboardadmin.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.example.projectboardadmin.dto.ArticleCommentDto;
import java.util.List;
public record ArticleCommentClientResponse(
@JsonProperty("_embedded") Embedded embedded,
@JsonProperty("page") Page page
) {
public static ArticleCommentClientResponse empty() {
return new ArticleCommentClientResponse(
new Embedded(List.of()),
new Page(1, 0, 1, 0)
);
}
public static ArticleCommentClientResponse of(List<ArticleCommentDto> articleComments) {
return new ArticleCommentClientResponse(
new Embedded(articleComments),
new Page(articleComments.size(), articleComments.size(), 1, 0)
);
}
public List<ArticleCommentDto> articleComments() { return this.embedded().articleComments(); }
public record Embedded(List<ArticleCommentDto> articleComments) {}
public record Page(
int size,
long totalElements,
int totalPages,
int number
) {}
}
- 구조
- Embedded
- 댓글 목록을 포함한다. _embedded 필드에 매핑되며, 내부에 댓글 리스트가 포함된다.
- Page
- 페이지네이션 정보를 포함한다. 페이지의 크기(size), 총 요소 개수(totalElements), 총 페이지 수(totalPages), 현재 페이지 번호(number)가 포함된다.
- Embedded
- empty() 메서드
- 빈 응답을 생성한다. 주로 기본값이나 초기 상태를 나타낼 때 사용된다.
- of() 메서드
- 주어진 댓글 리스트를 포함한 응답 객체를 생성한다.
- articleComments() 메서드
- 댓글 리스트를 쉽게 가져올 수 있도록 제공된 헬퍼 메서드이다.
3. 컨트롤러 테스트
다음으로, 댓글 관리 페이지를 테스트하기 위한 컨트롤러 테스트를 작성한다.
이 테스트는 Spring MVC Test를 활용하여 웹 계층의 동작을 검증한다.
package org.example.projectboardadmin.controller;
import org.example.projectboardadmin.config.SecurityConfig;
import org.example.projectboardadmin.domain.constant.RoleType;
import org.example.projectboardadmin.dto.ArticleCommentDto;
import org.example.projectboardadmin.dto.UserAccountDto;
import org.example.projectboardadmin.service.ArticleCommentManagementService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import static org.mockito.BDDMockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@DisplayName("컨트롤러 - 댓글 관리")
@Import(SecurityConfig.class)
@WebMvcTest(ArticleCommentManagementController.class)
class ArticleCommentManagementControllerTest {
private final MockMvc mvc;
@MockBean private ArticleCommentManagementService articleCommentManagementService;
public ArticleCommentManagementControllerTest(@Autowired MockMvc mvc) {
this.mvc = mvc;
}
@DisplayName("[view][GET] 댓글 관리 페이지 - 정상 호출")
@Test
void givenNothing_whenRequestingArticleCommentManagementView_thenReturnsArticleCommentManagementView() throws Exception {
// Given
given(articleCommentManagementService.getArticleComments()).willReturn(List.of());
// When & Then
mvc.perform(get("/management/article-comments"))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
.andExpect(view().name("management/article-comments"))
.andExpect(model().attribute("comments", List.of()));
then(articleCommentManagementService).should().getArticleComments();
}
@DisplayName("[data][GET] 댓글 1개 - 정상 호출")
@Test
void givenCommentId_whenRequestingArticleComment_thenReturnsArticleComment() throws Exception {
// Given
Long articleCommentId = 1L;
ArticleCommentDto articleCommentDto = createArticleCommentDto("comment");
given(articleCommentManagementService.getArticleComment(articleCommentId)).willReturn(articleCommentDto);
// When & Then
mvc.perform(get("/management/article-comments/" + articleCommentId))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id").value(articleCommentId))
.andExpect(jsonPath("$.content").value(articleCommentDto.content()))
.andExpect(jsonPath("$.userAccount.nickname").value(articleCommentDto.userAccount().nickname()));
then(articleCommentManagementService).should().getArticleComment(articleCommentId);
}
@DisplayName("[view][POST] 댓글 삭제 - 정상 호출")
@Test
void givenCommentId_whenRequestingDeletion_thenRedirectsToArticleCommentManagementView() throws Exception {
// Given
Long articleCommentId = 1L;
willDoNothing().given(articleCommentManagementService).deleteArticleComment(articleCommentId);
// When & Then
mvc.perform(
post("/management/article-comments/" + articleCommentId)
.with(csrf())
)
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/management/article-comments"))
.andExpect(redirectedUrl("/management/article-comments"));
then(articleCommentManagementService).should().deleteArticleComment(articleCommentId);
}
private ArticleCommentDto createArticleCommentDto(String content) {
return ArticleCommentDto.of(
1L,
1L,
createUserAccountDto(),
null,
content,
LocalDateTime.now(),
"Eunchan",
LocalDateTime.now(),
"Eunchan"
);
}
private UserAccountDto createUserAccountDto() {
return UserAccountDto.of(
"ecTest",
"pw",
Set.of(RoleType.ADMIN),
"eunchan-test@email.com",
"eunchan-test",
"test memo"
);
}
}
- 클래스 설정
- @WebMvcTest(ArticleCommentManagementController.class)를 통해 댓글 관리 컨트롤러만을 테스트 대상으로 지정한다.
- @Import(SecurityConfig.class)는 테스트에서 필요한 보안 설정을 로드한다.
- MockMvc를 사용해 실제 서버를 실행하지 않고도 HTTP 요청과 응답을 테스트한다.
- @MockBean을 사용해 ArticleCommentManagementService를 모킹 하여, 실제 서비스 로직 대신 모의 객체로 테스트한다.
- 댓글 관리 페이지 테스트
- givenNothing_whenRequestingArticleCommentManagementView_thenReturnsArticleCommentManagementView 테스트는 댓글 관리 페이지가 정상적으로 호출되는지 확인한다.
- articleCommentManagementService.getArticleComments()가 빈 리스트를 반환하도록 설정한 뒤, GET 요청을 /management/article-comments로 보낸다.
- 요청에 대해 HTTP 200 상태와 management/article-comments 뷰가 반환되는지 검증한다. 또한, 모델에 비어 있는 comments 리스트가 포함되는지 확인한다.
- 댓글 상세 조회 테스트:
- givenCommentId_whenRequestingArticleComment_thenReturnsArticleComment 테스트는 특정 댓글 ID로 댓글을 조회할 때 JSON 응답이 올바르게 반환되는지 확인한다.
- 댓글 ID가 1인 댓글을 모킹한 후, GET 요청을 /management/article-comments/1로 보낸다.
- HTTP 200 상태와 함께 JSON 응답으로 댓글 ID, 내용, 작성자의 닉네임이 올바르게 반환되는지 확인한다.
- 댓글 삭제 테스트:
- givenCommentId_whenRequestingDeletion_thenRedirectsToArticleCommentManagementView 테스트는 댓글을 삭제하는 POST 요청이 정상적으로 처리되고, 댓글 관리 페이지로 리디렉션 되는지 확인한다.
- 댓글 삭제 요청 시, CSRF 토큰을 포함한 POST 요청을 /management/article-comments/1로 보낸다.
- 요청 후, 리디렉션이 발생하고 redirect:/management/article-comments 뷰로 이동하는지 확인한다.
- 테스트 데이터 생성 헬퍼 메서드:
- createArticleCommentDto와 createUserAccountDto 메서드는 테스트에 필요한 ArticleCommentDto와 UserAccountDto 객체를 생성한다. 이는 중복된 테스트 데이터를 효율적으로 생성하기 위해 사용된다.
4. 서비스 테스트
마지막으로, 서비스의 동작을 검증하는 테스트를 작성한다. 실제 API 호출과 API mocking을 이용해 댓글 관리 기능이 올바르게 동작하는지 검증한다.
package org.example.projectboardadmin.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.projectboardadmin.domain.constant.RoleType;
import org.example.projectboardadmin.dto.ArticleCommentDto;
import org.example.projectboardadmin.dto.UserAccountDto;
import org.example.projectboardadmin.dto.properties.ProjectProperties;
import org.example.projectboardadmin.dto.response.ArticleCommentClientResponse;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.client.MockRestServiceServer;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
@ActiveProfiles("test")
@DisplayName("비즈니스 로직 - 댓글 관리")
class ArticleCommentManagementServiceTest {
// @Disabled("실제 API 호출 결과 관찰용이므로 평상시엔 비활성화")
@DisplayName("실제 API 호출 테스트")
@SpringBootTest
@Nested
class RealApiTest {
private final ArticleCommentManagementService sut;
@Autowired
public RealApiTest(ArticleCommentManagementService sut) {
this.sut = sut;
}
@DisplayName("댓글 API를 호출하면, 댓글을 가져온다.")
@Test
void givenNothing_whenCallingCommentApi_thenReturnsCommentList() {
// Given
// When
List<ArticleCommentDto> result = sut.getArticleComments();
// Then
System.out.println(result.stream().findFirst());
assertThat(result).isNotNull();
}
}
@DisplayName("API mocking 테스트")
@EnableConfigurationProperties(ProjectProperties.class)
@AutoConfigureWebClient(registerRestTemplate = true)
@RestClientTest(ArticleCommentManagementService.class)
@Nested
class RestTemplateTest {
private final ArticleCommentManagementService sut;
private final ProjectProperties projectProperties;
private final MockRestServiceServer server;
private final ObjectMapper mapper;
@Autowired
public RestTemplateTest(
ArticleCommentManagementService sut,
ProjectProperties projectProperties,
MockRestServiceServer server,
ObjectMapper mapper
) {
this.sut = sut;
this.projectProperties = projectProperties;
this.server = server;
this.mapper = mapper;
}
@DisplayName("댓글 목록 API을 호출하면, 댓글들을 가져온다.")
@Test
void givenNothing_whenCallingCommentsApi_thenReturnsCommentList() throws Exception {
// Given
ArticleCommentDto expectedComment = createArticleCommentDto("댓글");
ArticleCommentClientResponse expectedResponse = ArticleCommentClientResponse.of(List.of(expectedComment));
server
.expect(requestTo(projectProperties.board().url() + "/api/articleComments?size=10000"))
.andRespond(withSuccess(
mapper.writeValueAsString(expectedResponse),
MediaType.APPLICATION_JSON
));
// When
List<ArticleCommentDto> result = sut.getArticleComments();
// Then
assertThat(result).first()
.hasFieldOrPropertyWithValue("id", expectedComment.id())
.hasFieldOrPropertyWithValue("content", expectedComment.content())
.hasFieldOrPropertyWithValue("userAccount.nickname", expectedComment.userAccount().nickname());
server.verify();
}
@DisplayName("댓글 ID와 함께 댓글 API을 호출하면, 댓글을 가져온다.")
@Test
void givenCommentId_whenCallingCommentApi_thenReturnsComment() throws Exception {
// Given
Long articleCommentId = 1L;
ArticleCommentDto expectedComment = createArticleCommentDto("댓글");
server
.expect(requestTo(projectProperties.board().url() + "/api/articleComments/" + articleCommentId))
.andRespond(withSuccess(
mapper.writeValueAsString(expectedComment),
MediaType.APPLICATION_JSON
));
// When
ArticleCommentDto result = sut.getArticleComment(articleCommentId);
// Then
assertThat(result)
.hasFieldOrPropertyWithValue("id", expectedComment.id())
.hasFieldOrPropertyWithValue("content", expectedComment.content())
.hasFieldOrPropertyWithValue("userAccount.nickname", expectedComment.userAccount().nickname());
server.verify();
}
@DisplayName("댓글 ID와 함께 댓글 삭제 API을 호출하면, 댓글을 삭제한다.")
@Test
void givenCommentId_whenCallingDeleteCommentApi_thenDeletesComment() throws Exception {
// Given
Long articleCommentId = 1L;
server
.expect(requestTo(projectProperties.board().url() + "/api/articleComments/" + articleCommentId))
.andExpect(method(HttpMethod.DELETE))
.andRespond(withSuccess());
// When
sut.deleteArticleComment(articleCommentId);
// Then
server.verify();
}
}
private ArticleCommentDto createArticleCommentDto(String content) {
return ArticleCommentDto.of(
1L,
1L,
createUserAccountDto(),
null,
content,
LocalDateTime.now(),
"Euncha",
LocalDateTime.now(),
"Eunchan"
);
}
private UserAccountDto createUserAccountDto() {
return UserAccountDto.of(
"ecTest",
"pw",
Set.of(RoleType.ADMIN),
"eunchan-test@email.com",
"eunchan-test",
"test memo"
);
}
}
- 테스트 클래스 설정
- 이 클래스는 댓글 관리 서비스(ArticleCommentManagementService)의 비즈니스 로직을 테스트한다.
- @ActiveProfiles("test")를 통해 테스트 환경에서 특정 프로파일을 활성화한다.
- @DisplayName("비즈니스 로직 - 댓글 관리")는 테스트 클래스의 이름을 지정해 가독성을 높인다.
- 실제 API 호출 테스트
- RealApiTest는 실제 댓글 API를 호출해 결과를 검증한다. 이 테스트는 외부 API를 호출하여 실제 데이터를 확인할 때 유용하다.
- @SpringBootTest를 사용하여 애플리케이션 전체를 로드하고, 실제 API를 호출한다.
- givenNothing_whenCallingCommentApi_thenReturnsCommentList 테스트는 sut.getArticleComments()를 호출하여 댓글 목록을 가져온다. 결과로 반환된 리스트가 null이 아닌지 확인한다.
- API mocking 테스트
- RestTemplateTest는 외부 API 호출을 실제로 하지 않고 모의(Mock) 서버를 이용해 테스트한다. Spring의 MockRestServiceServer와 RestClientTest를 사용한다.
- @RestClientTest(ArticleCommentManagementService.class)는 특정 서비스에 대한 REST 클라이언트 테스트를 설정한다.
- @AutoConfigureWebClient(registerRestTemplate = true)는 RestTemplate을 테스트에 자동으로 설정한다.
- 댓글 목록 조회 테스트
- givenNothing_whenCallingCommentsApi_thenReturnsCommentList 테스트는 댓글 목록을 조회하는 API 호출을 모킹 하여 테스트한다.
- 서버에서 댓글 리스트를 응답으로 반환하도록 설정한 뒤, sut.getArticleComments()를 호출한다.
- 응답 결과로 얻어진 첫 번째 댓글이 예상한 속성 값(id, content, userAccount.nickname)을 가지는지 검증한다.
- 댓글 상세 조회 테스트
- givenCommentId_whenCallingCommentApi_thenReturnsComment 테스트는 특정 댓글 ID로 API를 호출해 댓글을 가져오는 시나리오를 검증한다.
- 서버에서 해당 ID의 댓글을 응답으로 반환하도록 설정한 뒤, sut.getArticleComment(articleCommentId)를 호출한다.
- 응답 결과로 얻어진 댓글이 예상한 속성 값을 가지는지 확인한다.
- 댓글 삭제 테스트
- givenCommentId_whenCallingDeleteCommentApi_thenDeletesComment 테스트는 특정 댓글을 삭제하는 API를 모킹하여 검증한다.
- 서버에서 해당 댓글 ID로 DELETE 요청을 받으면 성공 응답을 반환하도록 설정한 뒤, sut.deleteArticleComment(articleCommentId)를 호출한다.
- 서버가 예상대로 DELETE 요청을 처리했는지 확인한다.
- 테스트 데이터 생성 헬퍼 메서드:
- createArticleCommentDto와 createUserAccountDto 메서드는 테스트에서 반복적으로 사용되는 DTO 객체를 생성한다.
- 이들은 테스트 코드의 중복을 줄이고 가독성을 높이는 역할을 한다.
이번 글에서는 댓글 관리 페이지의 기능을 정의하고 테스트 방법을 상세히 살펴봤다. 테스트 환경을 구축해 코드의 신뢰성을 높이고, 다양한 상황에서 시스템의 정상 작동을 검증했다. 이런 테스트는 향후 유지보수와 기능 확장 시 큰 도움이 될 것이다.
다음 글에서는 회원 관리 페이지의 기능 정의와 테스트 방법을 다룰 예정이다. 회원 관리는 사용자 계정 정보의 조회, 수정, 삭제 등 중요한 기능을 포함하므로 철저한 테스트가 필요하다. 이에 대해 더 깊이 있는 내용을 다루도록 하겠다.
'BackEnd > Project' 카테고리의 다른 글
[Admin] Ch03. 회원 관리 페이지 기능 테스트 정의(2) (0) | 2024.08.20 |
---|---|
[Admin] Ch03. 회원 관리 페이지 기능 테스트 정의(1) (0) | 2024.08.20 |
[Admin] Ch03. 게시글 관리 페이지 기능 테스트 정의 (0) | 2024.08.20 |
[Admin] Ch02. 로그인 페이지 만들기 (0) | 2024.08.20 |
[Admin] Ch02. 회원 관리 페이지 만들기 (0) | 2024.08.20 |