Skip to content

Commit

Permalink
feature/#109 Post 테스트 코드 작성 (#117)
Browse files Browse the repository at this point in the history
* feat: Post 도메인 성공 테스트 및 클래스 생성

* refactor: Post 도메인 테스트 수정

* feat: PostServiceTest 코드 작성을 위한 세팅

* fix: 테스트 중 발견한 createPost 오류 수정

* feat: createPost 테스트 구현

* feat: createdAt을 사용하지 않는 테스트 구현

* feat: TestEntityUtils를 적용해 createdAt 사용 테스트 구현

* fix: CodeRabbit 리뷰 반영, 테스트 케이스 수정 및 null 체크 보완

* fix: FakeUserService 삭제 후 테스트용SecurityContext 대체

* fix: UserServiceTest 초기화 부분 수정
  • Loading branch information
LeeShinHaeng authored Nov 28, 2024
1 parent e0fb190 commit 229642f
Show file tree
Hide file tree
Showing 8 changed files with 427 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
package kgu.developers.api.post.application;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import org.springframework.data.domain.PageRequest;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import kgu.developers.api.post.presentation.exception.PostNotFoundException;
import kgu.developers.api.post.presentation.request.PostRequest;
import kgu.developers.api.post.presentation.response.PostDetailResponse;
Expand All @@ -20,6 +12,13 @@
import kgu.developers.domain.post.domain.PostRepository;
import kgu.developers.domain.user.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Service
@RequiredArgsConstructor
Expand All @@ -35,12 +34,12 @@ public class PostService {
public PostPersistResponse createPost(PostRequest request, Category category) {
User author = userService.me();
Post createPost = Post.create(request.title(), request.content(), category, author);
postRepository.save(createPost);
return PostPersistResponse.from(createPost.getId());
Long id = postRepository.save(createPost).getId();
return PostPersistResponse.from(id);
}

public PostSummaryPageResponse getPostsByKeywordAndCategory(PageRequest request, String keyword,
Category category) {
Category category) {
PaginatedListResponse<Post> paginatedListResponse = postRepository.findAllByTitleContainingAndCategoryOrderByCreatedAtDesc(
keyword, category, request);
return PostSummaryPageResponse.of(paginatedListResponse.contents(), paginatedListResponse.pageable());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Builder;

@Builder
public record PostRequest(
@Schema(description = "게시물 제목", example = "SW 부트캠프 4기 교육생 모집", requiredMode = REQUIRED)
@NotBlank
Expand Down
166 changes: 166 additions & 0 deletions aics-api/src/testFixtures/java/post/application/PostServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package post.application;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;

import kgu.developers.api.post.application.PostService;
import kgu.developers.api.post.presentation.exception.PostNotFoundException;
import kgu.developers.api.post.presentation.request.PostRequest;
import kgu.developers.api.post.presentation.response.PostDetailResponse;
import kgu.developers.api.post.presentation.response.PostPersistResponse;
import kgu.developers.api.post.presentation.response.PostSummaryPageResponse;
import kgu.developers.api.user.application.UserService;
import kgu.developers.domain.post.domain.Category;
import kgu.developers.domain.post.domain.Post;
import kgu.developers.domain.user.domain.Major;
import kgu.developers.domain.user.domain.User;
import mock.FakePostRepository;
import mock.FakeUserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.PageRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class PostServiceTest {
private PostService postService;

@BeforeEach
public void init() {
FakePostRepository fakePostRepository = new FakePostRepository();
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
FakeUserRepository fakeUserRepository = new FakeUserRepository();
UserService userService = new UserService(bCryptPasswordEncoder, fakeUserRepository);

this.postService = new PostService(fakePostRepository, userService);

fakeUserRepository.save(User.builder()
.id("202411345")
.password(bCryptPasswordEncoder.encode("password1234"))
.name("홍길동")
.email("[email protected]")
.phone("010-1234-5678")
.major(Major.CSE)
.build());

UserDetails user = userService.getUserById("202411345");
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(
new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities())
);

fakePostRepository.save(Post.create(
"테스트용 제목1", "테스트용 내용1",
Category.DEPT_INFO, userService.getUserById("202411345")
));

fakePostRepository.save(Post.create(
"테스트용 제목2", "테스트용 내용2",
Category.DEPT_INFO, userService.getUserById("202411345")
));
}

@Test
@DisplayName("createPost는 게시글을 생성할 수 있다")
public void createPost_Success() {
// given
PostRequest request = PostRequest.builder()
.title("테스트용 제목3")
.content("테스트용 내용3")
.build();
Category category = Category.DEPT_INFO;

// when
PostPersistResponse response = postService.createPost(request, category);

// then
assertEquals(3, response.postId());

// when
PostDetailResponse created = postService.getPostById(response.postId());

// then
assertEquals(request.title(), created.title());
assertEquals(request.content(), created.content());
assertEquals(category.getDescription(), created.category());
}

@Test
@DisplayName("getPostById는 게시글을 조회할 수 있다")
public void getPostById_Success() {
// given
Long postId = 1L;

// when
PostDetailResponse response = postService.getPostById(postId);

// then
assertEquals(postId, response.postId());
}

@Test
@DisplayName("getPostById는 존재하지 않는 ID로 조회시 PostNotFoundException을 발생시킨다")
public void getPostById_Throws_PostNotFoundException() {
// given
Long postId = 10L;

// when
// then
assertThatThrownBy(
() -> postService.getPostById(postId)
).isInstanceOf(PostNotFoundException.class);
}

@Test
@DisplayName("getPostsByKeywordAndCategory는 게시글을 페이징 조회할 수 있다")
public void getPostsByKeywordAndCategory_Success() {
// given
String keyword = "제목";
Category category = Category.DEPT_INFO;
int page = 0;
int size = 10;

// when
PostSummaryPageResponse posts = postService.getPostsByKeywordAndCategory(
PageRequest.of(page, size), keyword, category
);

// then
assertEquals(2, posts.contents().size());
}

@Test
@DisplayName("togglePostPinStatus는 게시글의 고정 상태를 변경할 수 있다")
public void togglePostPinStatus_Success() {
// given
Long postId = 1L;
PostDetailResponse before = postService.getPostById(postId);

// when
postService.togglePostPinStatus(postId);

// then
PostDetailResponse after = postService.getPostById(postId);
assertNotEquals(before.isPinned(), after.isPinned());
}

@Test
@DisplayName("deletePost 이후 게시글을 조회할 수 없다")
public void deletePost_Throws_PostNotFoundException() {
// given
Long postId = 1L;

// when
postService.deletePost(postId);

// then
assertThatThrownBy(
() -> postService.getPostById(postId)
).isInstanceOf(PostNotFoundException.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package post.presentation;

public class PostControllerTest {
}
87 changes: 87 additions & 0 deletions aics-domain/src/testFixtures/java/mock/FakePostRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package mock;

import kgu.developers.common.response.PageableResponse;
import kgu.developers.common.response.PaginatedListResponse;
import kgu.developers.domain.post.domain.Category;
import kgu.developers.domain.post.domain.Post;
import kgu.developers.domain.post.domain.PostRepository;
import org.springframework.data.domain.Pageable;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;

public class FakePostRepository implements PostRepository {
private final List<Post> data = Collections.synchronizedList(new ArrayList<>());
private final AtomicLong autoGeneratedId = new AtomicLong(0);

@Override
public Post save(Post post) {
if (post == null) {
throw new IllegalArgumentException("게시글은 null일 수 없습니다.");
}
if (post.getTitle() == null || post.getContent() == null) {
throw new IllegalArgumentException("제목과 내용은 필수 입력값입니다.");
}

Post newPost = Post.builder()
.id(autoGeneratedId.incrementAndGet())
.title(post.getTitle())
.content(post.getContent())
.views(post.getViews())
.category(post.getCategory())
.author(post.getAuthor())
.isPinned(post.isPinned())
.comments(new ArrayList<>(post.getComments()))
.file(post.getFile())
.build();

TestEntityUtils.setCreatedAt(newPost, LocalDateTime.now());

data.add(newPost);
return newPost;
}

@Override
public PaginatedListResponse<Post> findAllByTitleContainingAndCategoryOrderByCreatedAtDesc(
String keyword, Category category, Pageable pageable
) {
List<Post> filteredPosts = data.stream()
.filter(
post -> post.getTitle().contains(keyword) && post.getCategory().equals(category)
)
.sorted(Comparator.comparing(Post::getCreatedAt).reversed())
.collect(Collectors.toList());

int start = (int) pageable.getOffset();
int end = Math.min(start + pageable.getPageSize(), filteredPosts.size());

List<Post> paginatedPosts = start > filteredPosts.size() ?
Collections.emptyList() :
filteredPosts.subList(start, end);

List<Long> ids = paginatedPosts.stream()
.map(Post::getId)
.toList();

return PaginatedListResponse.of(paginatedPosts, PageableResponse.of(pageable, ids));
}

@Override
public Optional<Post> findById(Long postId) {
return data.stream()
.filter(post -> post.getId().equals(postId))
.findAny();
}

@Override
public void deleteAllByDeletedAtBefore(int retentionDays) {
LocalDateTime threshold = LocalDateTime.now().minusDays(retentionDays);
data.removeIf(post -> post.getDeletedAt() != null && post.getDeletedAt().isBefore(threshold));
}
}
23 changes: 23 additions & 0 deletions aics-domain/src/testFixtures/java/mock/TestEntityUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package mock;

import kgu.developers.common.domain.BaseTimeEntity;

import java.lang.reflect.Field;
import java.time.LocalDateTime;

public class TestEntityUtils {
public static void setCreatedAt(BaseTimeEntity entity, LocalDateTime createdAt) {
if (entity == null || createdAt == null) {
throw new IllegalArgumentException("entity와 createdAt은 null이 될 수 없습니다.");
}

try {
Field field = BaseTimeEntity.class.getDeclaredField("createdAt");
field.setAccessible(true);
field.set(entity, createdAt);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("TestEntityUtils에서 createdAt 설정 실패", e);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package post.application;

public class PostServiceTest {
}
Loading

0 comments on commit 229642f

Please sign in to comment.