728x90
반응형
728x90
이전 진행 상황
- Service 개발(Comment, Photo)
- PostService 추천/비추천 로직 변경 -> 회원, 비회원(세션 단위) 24시간 이내 추천/비추천 제한
- 추천/비추천 로직 변경으로 인한 기술 스택 추가 -> Redis
- User
- Post
- Comment
- Photo
User
user 도메인의 단위 테스트로 UserService의 로직을 검증하는 테스트가 추가되었습니다. 데이터베이스와 상호 작용 없이 순수 로직 테스트로 Mock 라이브러리가 사용되었고 login 메서드에 대한 테스트가 수행되었습니다.
추가 사항
- UserServiceTest 추가
class UserServiceTest
UserService에 대한 단위 테스트입니다. 로그인 메서드가 각 케이스별로 테스트되었습니다.
@Slf4j
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
UserRepository repository;
@InjectMocks
UserService service;
static final String LOGIN_ID = "asdf1234";
static final String PASSWORD = "1234";
User user;
@BeforeEach
void setUp() {
user = User.builder()
.id(1L)
.loginId(LOGIN_ID)
.password(PASSWORD)
.role(Role.USER)
.build();
}
@Test
void login_Success() {
// given
when(repository.findByLoginId(LOGIN_ID)).thenReturn(Optional.of(user));
// when
User loginUser = service.login(LOGIN_ID, PASSWORD);
// then
assertThat(loginUser).isEqualTo(user);
verify(repository).findByLoginId(LOGIN_ID);
}
@Test
void login_FailedWrongId() {
// given
when(repository.findByLoginId("wrong")).thenReturn(Optional.empty());
// when & then
assertThatThrownBy(() -> service.login("wrong", PASSWORD)).isInstanceOf(InvalidLoginException.class);
}
@Test
void login_FailedWrongPassword() {
// given
when(repository.findByLoginId(LOGIN_ID)).thenReturn(Optional.of(user));
// when & then
assertThatThrownBy(() -> service.login(LOGIN_ID, "wrong")).isInstanceOf(InvalidLoginException.class);
}
}
Spring Security를 사용함에 따라 순수 login 기능 자체는 사용되지 않을 예정이나 기능 확장 시 사용될 사용될 가능성을 염두해 테스트를 진행하였습니다.
Post
PostService의 단위 테스트로 추천/비추천 기능이 Redis에 의존하게 되며 간단한 게시글 타입 변경, 인기글 조회에 간한 메서드만 테스트하였습니다.
추가 내용
- PostServiceTest 추가
class PostServiceTest
@Slf4j
@ExtendWith(MockitoExtension.class)
class PostServiceTest {
@Mock
PostRepository repository;
@InjectMocks
PostService service;
static final long POST_ID = 1L;
Post post;
@BeforeEach
void setUp() {
post = Post.builder()
.id(POST_ID)
.title("title")
.content("content")
.postType(PostType.NORMAL)
.recomCount(0)
.notRecomCount(0)
.build();
}
@Test
void changeToNormal() {
//given
post.changeTypeNotice();
when(repository.findById(POST_ID)).thenReturn(Optional.of(post));
//when
service.changeToNormal(POST_ID);
//then
assertThat(post.getPostType()).isEqualTo(PostType.NORMAL);
verify(repository).findById(POST_ID);
}
@Test
void changeToNotice() {
//given
when(repository.findById(POST_ID)).thenReturn(Optional.of(post));
//when
service.changeToNotice(POST_ID);
//then
assertThat(post.getPostType()).isEqualTo(PostType.NOTICE);
verify(repository).findById(POST_ID);
}
@Test
void changeToPopular() {
//given
when(repository.findById(POST_ID)).thenReturn(Optional.of(post));
//when
service.changeToPopular(POST_ID);
//then
assertThat(post.getPostType()).isEqualTo(PostType.POPULAR);
verify(repository).findById(POST_ID);
}
@Test
void viewPopularPosts() {
// given
post.changeTypePopular();
List<Post> popularPosts = List.of(post);
when(repository.findByPostType(PostType.POPULAR)).thenReturn(popularPosts);
// when
List<Post> result = service.viewPopularPosts();
// then
assertThat(result).hasSize(1);
assertThat(result.get(0).getPostType()).isEqualTo(PostType.POPULAR);
verify(repository).findByPostType(PostType.POPULAR);
}
}
Comment
comment에 관한 단위 테스트로 댓글 작성에 관해 중점으로 테스트를 진행하였습니다. 댓글 작성의 경우 부모 댓글의 유무로 원본 댓글인지 대댓글인지 판별하게 되고 작성 로직이 달라지는 과정을 테스트하였습니다.
@Slf4j
@ExtendWith(MockitoExtension.class)
class CommentServiceTest {
@Mock
CommentRepository repository;
@Mock
PostRepository postRepository;
@InjectMocks
CommentService service;
static final long USER_ID = 1L;
static final Long POST_ID = 1L;
static final Long PARENT_ID = 1L;
static final Long COMMENT_ID = 2L;
Comment comment;
Comment parentComment;
User user;
Post post;
@BeforeEach
void setUp() {
user = User.builder()
.id(USER_ID)
.loginId("LOGIN_ID")
.password("PASSWORD")
.role(Role.USER)
.build();
post = Post.builder()
.id(POST_ID)
.title("post title")
.content("post content")
.postType(PostType.NORMAL)
.recomCount(0)
.notRecomCount(0)
.user(user)
.build();
parentComment = Comment.builder()
.id(PARENT_ID)
.content("parent content")
.user(user)
.post(post)
.build();
comment = Comment.builder()
.id(COMMENT_ID)
.content("comment content")
.user(user)
.post(post)
.build();
}
@Test
void createComment_PostNotFound() {
// given
when(postRepository.findById(POST_ID)).thenReturn(Optional.empty());
// when & then
assertThatThrownBy(() -> service.createComt(comment, null, POST_ID))
.isInstanceOf(PostNotFoundException.class);
}
@Test
void createComment_WithoutParent() {
//given
when(postRepository.findById(POST_ID)).thenReturn(Optional.of(post));
when(repository.save(any(Comment.class))).thenReturn(comment);
//when
Comment savedComment = service.createComt(comment, null, POST_ID);
//then
assertThat(savedComment).isEqualTo(comment);
assertThat(comment.getParentComment()).isNull();
verify(postRepository).findById(POST_ID);
verify(repository).save(comment);
}
@Test
void createComment_WithParent() {
//given
when(repository.findById(PARENT_ID)).thenReturn(Optional.of(parentComment));
when(postRepository.findById(POST_ID)).thenReturn(Optional.of(post));
when(repository.save(any(Comment.class))).thenReturn(comment);
//when
Comment savedComment = service.createComt(comment, PARENT_ID, POST_ID);
//then
assertThat(comment.getParentComment()).isEqualTo(parentComment);
assertThat(savedComment).isEqualTo(comment);
verify(postRepository).findById(POST_ID);
verify(repository).findById(PARENT_ID);
}
@Test
void createComment_WithParentWrongId() {
//given
when(repository.findById(PARENT_ID)).thenReturn(Optional.empty());
when(postRepository.findById(POST_ID)).thenReturn(Optional.of(post));
//when & then
assertThatThrownBy(() -> service.createComt(comment, PARENT_ID, POST_ID))
.isInstanceOf(CommentNotFoundException.class);
}
@Test
void viewComments() {
//given
when(repository.findByPostId(POST_ID)).thenReturn(List.of(comment, parentComment));
//when
List<Comment> comments = service.viewComts(POST_ID);
//then
assertThat(comments.size()).isEqualTo(2);
verify(repository).findByPostId(POST_ID);
}
@Test
void viewComments_EmptyList() {
// given
when(repository.findByPostId(POST_ID)).thenReturn(Collections.emptyList());
// when
List<Comment> comments = service.viewComts(POST_ID);
// then
assertThat(comments).isEmpty();
verify(repository).findByPostId(POST_ID);
}
}
photo
이미지 파일을 담당할 PhotoService의 단위 테스트입니다. 프로젝트 내 테스트 파일 저장 경로를 지정하여 임시 저장, 업로드에 관한 부분이 정상 작동하는지 테스트하였습니다. 저장소에 파일 IO 처리가 발생하는 로직이다 보니 Assertions를 통한 검증과 @AfterEach 내용을 주석 처리한 뒤 생성 여부와 본문 내 파일 경로 변경을 직접 확인하였습니다.
@Slf4j
@ExtendWith(MockitoExtension.class)
class PhotoServiceTest {
@Mock
PhotoRepository photoRepository;
@Mock
PostRepository postRepository;
@Mock
MultipartFile multipartFile;
@Mock
MultipartFile multipartFile2;
@InjectMocks
PhotoService service;
static final String SESSION_ID = "user";
static final String ORIGINAL_FILE_NAME = "test.jpg";
static final String ORIGINAL_FILE_NAME2 = "test2.jpg";
static final String TEST_UPLOAD_PATH = "src/test/resources/test-uploads/";
@BeforeEach
void setUp() {
// 테스트용 업로드 디렉토리 생성
new File(TEST_UPLOAD_PATH).mkdirs();
// UPLOAD_PATH 값을 테스트용 경로로 변경
ReflectionTestUtils.setField(service, "UPLOAD_PATH", TEST_UPLOAD_PATH);
}
@AfterEach
void cleanup() {
// 테스트 후 생성된 파일들 삭제
FileSystemUtils.deleteRecursively(new File(TEST_UPLOAD_PATH));
}
@Test
void tempUploadPhoto_Success() throws IOException {
// given
when(multipartFile.getOriginalFilename()).thenReturn(ORIGINAL_FILE_NAME);
// transferTo 메서드 동작 정의 추가
doAnswer(invocation -> {
File file = invocation.getArgument(0);
new FileOutputStream(file).close();
return null;
}).when(multipartFile).transferTo(any(File.class));
// when
String tempFilePath = service.tempUploadPhoto(multipartFile, SESSION_ID);
// then
assertThat(tempFilePath).startsWith("/images/temp_");
assertThat(tempFilePath).endsWith(".jpg");
// 임시 파일이 실제로 생성되었는지 확인
File tempFile = new File(TEST_UPLOAD_PATH + tempFilePath.substring(8));
assertThat(tempFile).exists();
}
@Test
void tempUploadPhoto_InvalidFileName() {
// given
when(multipartFile.getOriginalFilename()).thenReturn(null);
// when & then
assertThatThrownBy(() -> service.tempUploadPhoto(multipartFile, SESSION_ID))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void tempUploadPhoto_IOExceptionCase() throws IOException {
// given
when(multipartFile.getOriginalFilename()).thenReturn("test.jpg");
doThrow(new IOException()).when(multipartFile).transferTo(any(File.class));
// when & then
assertThatThrownBy(() -> service.tempUploadPhoto(multipartFile, SESSION_ID))
.isInstanceOf(FileUploadException.class);
}
@Test
void uploadPhoto_Success() throws IOException {
// given
when(multipartFile.getOriginalFilename()).thenReturn(ORIGINAL_FILE_NAME);
when(multipartFile2.getOriginalFilename()).thenReturn(ORIGINAL_FILE_NAME2);
doAnswer(invocation -> {
File file = invocation.getArgument(0);
new FileOutputStream(file).close();
return null;
}).when(multipartFile).transferTo(any(File.class));
doAnswer(invocation -> {
File file = invocation.getArgument(0);
new FileOutputStream(file).close();
return null;
}).when(multipartFile2).transferTo(any(File.class));
//임시 파일 생성
String tempFilePath = service.tempUploadPhoto(multipartFile, SESSION_ID);
File tempFile = new File(tempFilePath);
String tempFilePath2 = service.tempUploadPhoto(multipartFile2, SESSION_ID);
File tempFile2 = new File(tempFilePath2);
//임시 파일 주소로 post 생성
Long postId = 1L;
String con = "게시글 본문 내용 중 이미지가 포함되는 형식<img src='"
+ tempFilePath + "'>이미지가 가운데 포함되어 있음"
+ "<img src='" + tempFilePath2 + "'>두 번째 이미지가 포함됨";
Post post = Post.builder()
.id(postId)
.content(con)
.build();
when(postRepository.findById(postId)).thenReturn(Optional.of(post));
when(photoRepository.save(any(Photo.class))).thenAnswer(i -> i.getArgument(0));
// when
List<Photo> photos = service.uploadPhoto(postId , SESSION_ID);
// then
// 1. 임시 파일이 정식 파일로 변환되었는지 확인
assertThat(tempFile).doesNotExist();
assertThat(tempFile2).doesNotExist();
assertThat(photos).hasSize(2);
// 2. 본문의 이미지 경로가 업데이트되었는지 확인
assertThat(post.getContent()).doesNotContain(tempFilePath.substring(8));
assertThat(post.getContent()).contains(photos.get(0).getSavedPhotoName());
assertThat(post.getContent()).doesNotContain(tempFilePath2.substring(8));
assertThat(post.getContent()).contains(photos.get(1).getSavedPhotoName());
// 3. tempFileMap에서 해당 postId 정보가 삭제되었는지 확인
Map<String, Set<String>> tempFileMap = (Map<String, Set<String>>) ReflectionTestUtils.getField(service, "tempFileMap");
assertThat(tempFileMap).doesNotContainKey(SESSION_ID);
// 4. 직접 확인
log.info("content = {}", con);
log.info("post.getContent = {}", post.getContent());
}
@Test
void uploadPhoto_PostNotFound() {
// given
when(postRepository.findById(any())).thenReturn(Optional.empty());
// when & then
assertThatThrownBy(() ->
service.uploadPhoto(1L, SESSION_ID))
.isInstanceOf(PostNotFoundException.class);
}
@Test
void uploadPhoto_NoTempFiles() {
// given
Post post = Post.builder().id(1L).content("content").build();
when(postRepository.findById(1L)).thenReturn(Optional.of(post));
// when
List<Photo> photos = service.uploadPhoto(1L, SESSION_ID);
// then
assertThat(photos).isEmpty();
}
@Test
void viewPhotos_Success() {
// given
Long postId = 1L;
List<Photo> photoList = List.of(Photo.builder().build(), Photo.builder().build());
when(photoRepository.findByPostId(postId)).thenReturn(photoList);
// when
List<Photo> result = service.viewPhotos(postId);
// then
assertThat(result).hasSize(2);
verify(photoRepository).findByPostId(postId);
}
@Test
void viewPhotos_EmptyList() {
// given
when(photoRepository.findByPostId(any())).thenReturn(Collections.emptyList());
// when
List<Photo> result = service.viewPhotos(1L);
// then
assertThat(result).isEmpty();
}
}
728x90
'개인 프로젝트 > Toy & Side' 카테고리의 다른 글
[TOY] 개발 - Adapters(Post) (0) | 2024.12.04 |
---|---|
[TOY] 개발 - Adapters(User) (0) | 2024.12.03 |
[TOY] 개발 - Application(Comment, Photo) (0) | 2024.12.01 |
[TOY] 개발 - Application(PostService) (1) | 2024.11.30 |
[TOY] 개발 - Application(UserService) (0) | 2024.11.30 |