728x90
이전글
목표
- 1. controller / 2. service로 나눠서 테스트를 진행해야 함.
- 1. 성공, 2. 실패 테스트 케이스를 모두 통과하는 실제 코드 작성.
- Exception 처리는 enum Error코드에 작성 후 호출 해서 사용
- 실패의 경우 enum값의 errorCode로 처리하기
- when(어떤 상황일 때)를 각각 설계해 보고, Test case에 따라 예상되는 결괏값을 작성해 주기
- Controller, Service 두 클래스의 테스트 코드 작성하기
체크사항
포스트 단건 조회(상세 보기)
controller
- GET /posts/1로 조회 시, 조회 성공 - id, title, body, userName 4가지 항목이 있는지 검증
포스트 등록
controller
- 포스트 작성 성공
- 포스트 작성 실패(1) - 인증 실패 - JWT를 Bearer Token으로 보내지 않은 경우
- 포스트 작성 실패(2) - 인증 실패 - JWT가 유효하지 않은 경우
포스트 수정
controller
- 포스트 수정 실패(1) : 인증 실패
- 포스트 수정 실패(2) : 작성자 불일치
- 포스트 수정 실패(3) : 데이터베이스 에러
- 포스트 수정 성공
포스트 삭제
controller
- 포스트 삭제 성공
- 포스트 삭제 실패(1) : 인증 실패
- 포스트 삭제 실패(2) : 작성자 불일치
- 포스트 삭제 실패(3) : 데이터베이스 에러
포스트 리스트
controller
- 조회 성공 : 0번이 1번보다 날짜가 최신
Post Controller Test
Basic
@WebMvcTest(PostController.class)
@WithMockUser
class PostControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@MockBean
UserService userService;
@MockBean
PostService postService;
private String token;
@Value("${jwt.secret}")
private String secretKey;
public final LocalDateTime time = LocalDateTime.now();
@BeforeEach()
public void getToken() {
long expireTimeMs = 1000 * 60 * 60;
token = JwtUtil.createJwt("chordpli", secretKey, System.currentTimeMillis() + expireTimeMs);
}
}
- @SpringBootTest가 아닌, @WebMvcTest를 사용하였습니다.
- @SpringBootTest를 사용하면 스프링이 구동되면서 모든 빈을 로드하기 때문에 시간이 오래 걸리고, 단위가 커져서 세부적인 테스트를 하기 어려워지기 때문입니다.
- @WithMockUser 어노테이션을 사용해서 Spring Security 인증을 받아옵니다.
- @Autowired를 사용해서 의존성을 주입합니다.
- MockMvc, ObjectMapper의 의존성을 주입합니다.
- @MockBean을 사용해서 UserService, PostService를 선언합니다.
- token을 발급하는 상황을 가정하기 위해 secretKey, token, getToken()을 만들고 선언하였습니다.
Fixture
UserFixture
public class UserFixture {
public static User get(String userName, String password) {
return User.builder()
.id(1)
.userName(userName)
.password(password)
.userRole(UserRole.USER)
.build();
}
}
PostFixture
public class PostFixture {
public static Post get() {
User user = UserFixture.get("chordpli", "1234");
return Post.builder()
.id(1)
.user(user)
.title("title")
.body("body")
.build();
}
}
포스트 단건 조회(상세 보기)
조회 성공
...
class PostControllerTest {
...
/* 포스트 상세 */
@Test
@DisplayName("포스트 상세 보기")
void post_one_detail() throws Exception {
Post dto = PostFixture.get();
int postId = dto.getId();
PostReadResponse response = PostReadResponse.builder()
.id(dto.getId())
.title(dto.getTitle())
.body(dto.getBody())
.userName(dto.getUser().getUserName())
.createdAt(time)
.lastModifiedAt(time)
.build();
given(postService.getPost(any())).willReturn(response);
String url = String.format("/api/v1/posts/%d", postId);
mockMvc.perform(get(url).with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(dto)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultCode").exists())
.andExpect(jsonPath("$.resultCode").value("SUCCESS"))
.andExpect(jsonPath("$.result.id").exists())
.andExpect(jsonPath("$.result.id").value(1))
.andExpect(jsonPath("$.result.title").exists())
.andExpect(jsonPath("$.result.title").value("제목"))
.andExpect(jsonPath("$.result.body").exists())
.andExpect(jsonPath("$.result.body").value("내용"))
.andExpect(jsonPath("$.result.userName").exists())
.andExpect(jsonPath("$.result.userName").value("chordpli"))
.andExpect(jsonPath("$.result.createdAt").exists())
.andExpect(jsonPath("$.result.lastModifiedAt").exists())
.andDo(print());
verify(postService, times(1)).getPost(any());
}
}
- Fixture를 사용해서 미리 만들어 놓은 Post 정보를 불러옵니다.
- Post 정보에 맞춰 PostReadResponse를 생성합니다.
- postService의 getPost를 사용할 때, response를 반환하도록 합니다.
- url을 GET 메서드를 사용하여 dto 내용과 함께 보냅니다.
- given 했을 때 우리는 response를 받기로 했기 때문에 andExpect를 사용해서 값들이 존재하는지, 값이 일치하는지 확인합니다.
- postService의 getPost가 1번 사용됐는지 확인합니다.
포스트 등록
포스트 작성 성공
...
class PostControllerTest {
...
/* 포스트 등록 */
@Test
@DisplayName("포스트 작성 성공")
void post_success() throws Exception {
UserLoginResponse user = new UserLoginResponse(token);
PostRequest request = new PostRequest("title", "content");
PostResponse response = new PostResponse("포스트 등록 완료.", 1);
given(postService.post(any(), any())).willReturn(response);
String url = "/api/v1/posts";
mockMvc.perform(post(url).with(csrf())
.header(HttpHeaders.AUTHORIZATION, "Bearer " + user.getJwt())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultCode").exists())
.andExpect(jsonPath("$.resultCode").value("SUCCESS"))
.andExpect(jsonPath("$.result.message").exists())
.andExpect(jsonPath("$.result.message").value("포스트 등록 완료."))
.andExpect(jsonPath("$.result.postId").exists())
.andExpect(jsonPath("$.result.postId").value(1))
.andDo(print());
verify(postService, times(1)).post(any(), any());
}
}
- Token이 담겨있는 UserLoginResponse를 생성합니다.
- Post 작성에 필요한 request를 생성합니다.
- Post 작성이 완료됐을 때 성공 여부를 반환하는 Response를 생성합니다.
- postService의 post메서드가 사용되면, response를 반환합니다.
- POST 메서드의 url을 보낼 때, header에 "Bearer + Token을, body에 request정보를 담습니다.
- given 했을 때 우리는 response를 받기로 했기 때문에 andExpect를 사용해서 값들이 존재하는지, 값이 일치하는지 확인합니다.
포스트 작성 실패 - Bearer Token으로 보내지 않은 경우
...
class PostControllerTest {
...
@Test
@DisplayName("포스트 작성 실패")
void post_fail_no_token() throws Exception {
PostRequest request = new PostRequest("title", "content");
given(postService.post(any(), any())).willThrow(new SNSAppException(INVALID_PERMISSION, "토큰이 존재하지 않습니다."));
String url = "/api/v1/posts";
mockMvc.perform(post(url).with(csrf())
.header(HttpHeaders.AUTHORIZATION, "")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(request)))
.andExpect(status().isUnauthorized())
.andDo(print());
verify(postService, times(1)).post(any(), any());
}
}
- Post 작성에 필요한 request를 생성합니다.
- postService의 post메서드를 실행시켰을 때, INVALID_PERMISSION 예외를 발생시킵니다.
- POST 메서드의 url을 보낼 때, header에 Token을 담지 않고 body에 request 정보를 담습니다.
- given 했을 때 우리는 INVALID_PERMISSON 예외를 받기로 했으므로 HTTP STATUS가 Unauthorized인지 확인합니다.
포스트 작성 실패 - JWT가 유효하지 않은 경우
...
class PostControllerTest {
...
@Test
@DisplayName("포스트 작성 실패")
void post_fail_invalid_token() throws Exception {
User user = UserFixture.get("chordpli", "1234");
PostRequest request = new PostRequest("title", "content");
token = JwtUtil.createJwt(user.getUserName(), secretKey, System.currentTimeMillis());
given(postService.post(any(), any())).willThrow(new SNSAppException(INVALID_TOKEN, "유효하지 않은 토큰입니다."));
String url = "/api/v1/posts";
mockMvc.perform(post(url).with(csrf())
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(request)))
.andExpect(status().isUnauthorized())
.andDo(print());
verify(postService, times(1)).post(any(), any());
}
}
- Post 작성에 필요한 request를 생성합니다.
- token을 생성할 때, 만들자마자 만료되도록 시간을 지정하여 token을 다시 초기화시켜줍니다.
- postService의 post메서드를 사용할 때 INVALID_TOKEN 예외를 발생시킵니다.
- POST 메서드의 url을 보낼 때, header에 Token(만료된)과 body에 request 정보를 담습니다.
- given 했을 때 우리는 INVALID_PERMISSON 예외를 받기로 했으므로 HTTP STATUS가 Unauthorized인지 확인합니다.
포스트 수정
포스트 수정 성공
...
class PostControllerTest {
...
/* 포스트 수정 */
@Test
@DisplayName("포스트 수정 성공")
void success_post_modify() throws Exception {
User user = UserFixture.get("chordpli", "1234");
Post post = PostFixture.get(user);
PostModifyRequest request = new PostModifyRequest("title", "content");
post.setTitle(dto.getTitle());
post.setBody(dto.getBody());
given(postService.modifyPost(any(), any(), any())).willReturn(post);
Integer postId = 1;
String url = String.format("/api/v1/posts/%d", postId);
mockMvc.perform(put(url).with(csrf())
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultCode").exists())
.andExpect(jsonPath("$.resultCode").value("SUCCESS"))
.andExpect(jsonPath("$.result.postId").exists())
.andExpect(jsonPath("$.result.postId").value(1))
.andExpect(jsonPath("$.result.message").exists())
.andExpect(jsonPath("$.result.message").value("포스트 수정 완료"))
.andDo(print());
Assertions.assertEquals(post.getTitle(), request.getTitle());
Assertions.assertEquals(post.getBody(), request.getBody());
}
}
- Post를 만들기 위한 User를 생성합니다.
- Post를 생성합니다.
- 수정하기 위한 request dto를 생성합니다.
- Post를 requestDto에 맞춰 Setter를 사용하여 수정합니다.
- postService의 modifyPost 메서드를 사용했을 때 post를 반환합니다.
- PUT메서드의 url을 보낼 때, haeder에 토큰과 body에 request정보를 담아 보냅니다.
- given 했을 때 우리는 post 정보를 반환받기로 하였으므로 post 정보가 잘 반환되었는지 확인하기 위해 값이 존재하는지, 예상하는 값과 동일한지 확인합니다.
- post의 title과 request의 title이 같은지 확인합니다.
- post의 body와 request의 body가 같은지 확인합니다.
포스트 수정 실패 - 인증 실패
...
class PostControllerTest {
...
/* 포스트 수정 */
@Test
@DisplayName("포스트 수정 실패_인증 실패")
void fail_post_modify_authentication_failed() throws Exception {
User user = UserFixture.get("chordpli", "1234");
token = JwtUtil.createJwt(user.getUserName(), secretKey, System.currentTimeMillis());
PostModifyRequest request = new PostModifyRequest("title", "content");
given(postService.modifyPost(any(), any(), any())).willThrow(new SNSAppException(INVALID_PERMISSION, "유효하지 않은 토큰입니다."));
Integer postId = 1;
String url = String.format("/api/v1/posts/%d", postId);
mockMvc.perform(put(url).with(csrf())
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(request)))
.andExpect(status().isUnauthorized())
.andDo(print());
verify(postService, times(1)).modifyPost(any(), any(), any());
}
}
- 만료된 토큰을 만들기 위한 User와, 토큰을 만료된 토큰으로 초기화시킵니다.
- post를 수정하기 위한 request를 생성합니다.
- postService의 modifyPost를 실행시켰을 때, INVALID_PERMISSION 예외를 발생시킵니다.
- PUT메서드의 url을 보낼 때 header에 만료된 토큰과 body에 request를 담아 보냅니다.
- given 했을 때 우리는 INVALID_PERMISSON 예외를 받기로 했으므로 HTTP STATUS가 Unauthorized인지 확인합니다.
포스트 수정 실패 - 작성자 불일치
...
class PostControllerTest {
...
/* 포스트 수정 */
@Test
@DisplayName("포스트 수정 실패_작성자 불일치")
void fail_post_modify_mismatch_author() throws Exception {
PostModifyRequest request = new PostModifyRequest("title", "content");
given(postService.modifyPost(any(), any(), any())).willThrow(new SNSAppException(INVALID_PERMISSION, "사용자가 권한이 없습니다."));
Integer postId = 1;
String url = String.format("/api/v1/posts/%d", postId);
mockMvc.perform(put(url).with(csrf())
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(request)))
.andExpect(status().isUnauthorized())
.andDo(print());
verify(postService, times(1)).modifyPost(any(), any(), any());
}
}
- post를 수정하기 위한 request를 생성합니다.
- postService의 modifyPost를 실행시켰을 때, INVALID_PERMISSION 예외를 발생시킵니다.
- PUT메서드의 url을 보낼 때 header에 만료된 토큰과 body에 request를 담아 보냅니다.
- given 했을 때 우리는 INVALID_PERMISSON 예외를 받기로 했으므로 HTTP STATUS가 Unauthorized인지 확인합니다.
포스트 수정 실패 - 데이터베이스 에러
...
class PostControllerTest {
...
/* 포스트 수정 */
@Test
@DisplayName("포스트 수정 실패_DB 에러")
void fail_post_modify_db_error() throws Exception {
PostModifyRequest request = new PostModifyRequest("title", "content");
given(postService.modifyPost(any(), any(), any())).willThrow(new SNSAppException(DATABASE_ERROR, "데이터 베이스에 에러가 발생하였습니다."));
Integer postId = 1;
String url = String.format("/api/v1/posts/%d", postId);
mockMvc.perform(put(url).with(csrf())
.header(HttpHeaders.AUTHORIZATION,"Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(request)))
.andExpect(status().isInternalServerError())
.andDo(print());
verify(postService, times(1)).modifyPost(any(), any(), any());
}
}
- post를 수정하기 위한 request를 생성합니다.
- postService의 modifyPost를 실행시켰을 때, DATABASE_ERROR 예외를 발생시킵니다.
- PUT메서드의 url을 보낼 때 header에 만료된 토큰과 body에 request를 담아 보냅니다.
- given 했을 때 우리는 DATABASE_ERROR 예외를 받기로 했으므로 HTTP STATUS가 InternalServerError인지 확인합니다.
포스트 삭제
포스트 삭제 성공
...
class PostControllerTest {
...
/* 포스트 삭제 */
@Test
@DisplayName("포스트 삭제 성공")
void success_post_delete() throws Exception {
doNothing().when(postService).deletePost(any(), any());
Integer postId = 1;
String url = String.format("/api/v1/posts/%d", postId);
mockMvc.perform(delete(url).with(csrf())
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(postId)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultCode").exists())
.andExpect(jsonPath("$.resultCode").value("SUCCESS"))
.andExpect(jsonPath("$.result.postId").exists())
.andExpect(jsonPath("$.result.postId").value(1))
.andExpect(jsonPath("$.result.message").exists())
.andExpect(jsonPath("$.result.message").value("포스트 삭제 완료"))
.andDo(print());
}
}
- delete 메서드의 경우에는 반환이 없는 void 형식이므로 이전과 다르게 테스트 코드를 작성하였습니다.
- void 메서드를 사용하기 위해 doNothing()을 사용하여 postService가 deletePost를 실행시켰습니다.
- HTTP DELETE메서드를 사용하여 header에 token과 body에 삭제시킬 postId를 담아 url을 넘깁니다.
- 삭제가 잘 되었는지 값의 존재 유무, 메시지가 원하는 값이 나왔는지 확인합니다.
포스트 삭제 실패 - 인증 실패
...
class PostControllerTest {
...
/* 포스트 삭제 */
@Test
@DisplayName("포스트 삭제 실패_인증 실패")
void fail_post_delete_authentication_failed() throws Exception {
User user = UserFixture.get("chordpli", "1234");
token = JwtUtil.createJwt(user.getUserName(), secretKey, System.currentTimeMillis());
willThrow(new SNSAppException(INVALID_TOKEN, "유효하지 않은 토큰입니다.")).given(postService).deletePost(any(), any());
Integer postId = 1;
String url = String.format("/api/v1/posts/%d", postId);
mockMvc.perform(delete(url).with(csrf())
.header(HttpHeaders.AUTHORIZATION, token)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(1)))
.andExpect(status().isUnauthorized())
.andDo(print());
verify(postService, times(1)).deletePost(any(), any());
}
}
- User를 생성하여 만료된 토큰을 초기화시켜 줍니다.
- void 메서드를 사용하기 위해 willThrow로 postSerivce에서 deletePost가 사용되었을 때 예외를 던지도록 작성하였습니다.
- HTTP DELETE메서드를 사용하여 header에 token과 body에 삭제시킬 postId를 담아 url을 넘깁니다.
- given 했을 때 willthorw를 사용하여 INVALID_TOKEN 예외를 받기로 했으므로 HTTP STATUS가 Unauthorized 확인합니다.
포스트 삭제 실패 - 작성자 불일치
...
class PostControllerTest {
...
/* 포스트 삭제 */
@Test
@DisplayName("포스트 삭제 실패_작성자 불일치")
void fail_post_delete_mismatch_author() throws Exception {
willThrow(new SNSAppException(INVALID_PERMISSION, "사용자가 권한이 없습니다.")).given(postService).deletePost(any(), any());
Integer postId = 1;
String url = String.format("/api/v1/posts/%d", postId);
mockMvc.perform(delete(url).with(csrf())
.header(HttpHeaders.AUTHORIZATION,"Bearer" + token)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(postId)))
.andExpect(status().isUnauthorized())
.andDo(print());
verify(postService, times(1)).deletePost(any(), any());
}
}
- void 메서드를 사용하기위해 willThrow로 postSerivce에서 deletePost가 사용되었을때 예외를 던지도록 작성하였습니다.
- HTTP DELETE메서드를 사용하여 header에 token과 body에 삭제시킬 postId를 담아 url을 넘깁니다.
- given 했을 때 willthorw를 사용하여 INVALID_PERMISSION 예외를 받기로 했으므로 HTTP STATUS가 Unauthorized 확인합니다.
포스트 삭제 실패 - 데이터베이스 에러
...
class PostControllerTest {
...
/* 포스트 삭제 */
@Test
@DisplayName("포스트 삭제 실패_DB 에러")
void fail_post_delete_db_error() throws Exception {
willThrow(new SNSAppException(DATABASE_ERROR, "데이터 베이스에 에러가 발생하였습니다.")).given(postService).deletePost(any(), any());
Integer postId = 1;
String url = String.format("/api/v1/posts/%d", postId);
mockMvc.perform(delete(url).with(csrf())
.header(HttpHeaders.AUTHORIZATION, "Bearer" + token)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(postId)))
.andExpect(status().isInternalServerError())
.andDo(print());
verify(postService, times(1)).deletePost(any(), any());
}
}
- void 메서드를 사용하기위해 willThrow로 postSerivce에서 deletePost가 사용되었을때 예외를 던지도록 작성하였습니다.
- HTTP DELETE메서드를 사용하여 header에 token과 body에 삭제시킬 postId를 담아 url을 넘깁니다.
- given 했을 때 willthorw를 사용하여 DATABASE_ERROR 예외를 받기로 했으므로 HTTP STATUS가 InternalServerError 확인합니다.
포스트 리스트
조회 성공
...
class PostControllerTest {
...
/* 포스트 리스트 */
@Test
@DisplayName("포스트 리스트 조회 성공_0번이 1번보다 날짜가 최신일 때")
void success_post_list() throws Exception {
List<PostReadResponse> posts = new ArrayList<>();
PageRequest pageRequest = PageRequest.of(0, 20, Sort.by("createdAt").descending());
User user = User.builder()
.id(1)
.userName("chordpli")
.password("1234")
.userRole(UserRole.USER)
.build();
Post post1 = Post.builder()
.id(1)
.title("title1")
.body("body1")
.user(user)
.build();
post1.setCreatedAt(LocalDateTime.now());
post1.setLastModifiedAt(LocalDateTime.now());
Thread.sleep(1000);
Post post2 = Post.builder()
.id(2)
.title("title1")
.body("body1")
.user(user)
.build();
post2.setCreatedAt(LocalDateTime.now());
post2.setLastModifiedAt(LocalDateTime.now());
PostReadResponse response1 = post1.toResponse();
PostReadResponse response2 = post2.toResponse();
posts.add(response1);
posts.add(response2);
given(postService.getAllPost(pageRequest)).willReturn(posts);
String url = String.format("/api/v1/posts/");
mockMvc.perform(get(url).with(csrf())
.header(HttpHeaders.AUTHORIZATION, "")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(pageRequest)))
.andExpect(status().isOk())
.andDo(print());
Assertions.assertTrue(posts.get(1).getCreatedAt().isAfter(posts.get(0).getCreatedAt()));
}
}
- 게시물 리스트를 불러와야 하므로 1개의 User를 만들고 시간차를 두고 2개의 Post를 생성하였습니다.
- 게시물 리스트를 확인하는 조건으로 두 post의 생성 시각을 비교하도록 요구사항이 있었습니다.
- postService의 getAllpost를 사용했을 때, posts가 반환되도록 구성하였습니다.
- HTTP GET메서드를 사용하여 body에 pageRequest를 담아 보냅니다.
- status가 200인지 확인합니다.
- post의 두 번째 글이 첫 번째 글보다 나중에 만들어졌는지 확인합니다.
다음글
반응형
'프로젝트 > Archive' 카테고리의 다른 글
[05] 리팩토링 - 1 (0) | 2022.12.29 |
---|---|
[04] Post Test 코드 작성 - 2 (Service Test) (0) | 2022.12.27 |
[04] User Test 코드 작성 (0) | 2022.12.26 |
[JWT] JWT Exception 처리 (0) | 2022.12.25 |
[03] 게시된 포스트 삭제 (0) | 2022.12.23 |