테스트 코드 작성이 클린코드에서 중요하다고 말씀하셨으니 만들어야지.
도메인 영역
일단 게시글 엔티티의 도메인 규칙 테스트를 작성해 보자.
class PostTest {
Member member;
Post post;
@BeforeEach
void setUp() {
// given
PublishBlogpostFormRequest request = getPublishBlogpostFormRequest();
// when
member = Mockito.mock(Member.class);
post = Post.of(request, member);
}
@Test
@DisplayName("Post.of(): 요청값과 Member로 올바른 Post가 생성된다")
void createPostFromFactoryMethod() {
// then
assertThat(post.getId()).isNull();
assertThat(post.getTitle()).isEqualTo("Hello World");
assertThat(post.getContent()).isEqualTo("This is content");
assertThat(post.getMember()).isEqualTo(member);
}
@Test
@DisplayName("title이 null이면 예외를 반환한다")
void constructorNullCheck() {
//when
var publishBlogpostFormRequest = new PublishBlogpostFormRequest();
//then
assertThatThrownBy(()->Post.of(publishBlogpostFormRequest,member))
.isInstanceOf(NullPointerException.class);
}
@Test
@DisplayName("title과 content는 수정 가능한 필드이다")
void changePostFields() {
// when
post.changeTitle("new title");
post.changeContent("new content");
// then
assertThat(post.getTitle()).isEqualTo("new title");
assertThat(post.getContent()).isEqualTo("new content");
}
@Test
@DisplayName("title 수정시에도 Null이 들어가선 안된다.")
void changePostFieldsNullCheck() {
// then
assertThatThrownBy(()->post.changeTitle(null))
.isInstanceOf(NullPointerException.class);
}
@Test
@DisplayName("Post 생성 시 Member가 정상적으로 매핑되는지 테스트")
void memberMappingTest() {
// then
assertThat(post.getMember()).isNotNull();
assertThat(post.getMember()).isEqualTo(member);
}
}
이런 간단한 테스트는 AI로 생성하면 테스트 코드 작성에 큰 시간이 들지 않는 반면 유지보수성은 높아진다.
다만 셋업함수같은 부분은 직접 나눠주었다.
public class PostFixture {
public static PublishBlogpostFormRequest getPublishBlogpostFormRequest() {
return new PublishBlogpostFormRequest(null, "Hello World", "This is content");
}
}
이후 서비스단 테스트에서도 재사용될 만한 부분은 Fixture로 만들어둔다.
어플리케이션 영역
리포지토리
@DataJpaTest
class PostRepositoryTest {
@Autowired
private PostRepository postRepository;
@Autowired
private MemberRepository memberRepository;
@Autowired
EntityManager entityManager;
@Test
@DisplayName("findByMember()가 해당 member의 게시글 목록을 페이징하여 반환한다")
void findByMemberTest() {
// given
Member member = Member.register(createRegisterRequest(), createPasswordEncoder());
memberRepository.save(member);
Post p1 = Post.of(getPublishBlogpostFormRequest(), member);
Post p2 = Post.of(getPublishBlogpostFormRequest(), member);
Post p3 = Post.of(getPublishBlogpostFormRequest(), member);
postRepository.save(p1);
postRepository.save(p2);
postRepository.save(p3);
entityManager.flush();
entityManager.clear();
// when
Page<Post> page = postRepository.findByMember(member, PageRequest.of(0, 2));
// then
assertThat(page.getTotalElements()).isEqualTo(3);
assertThat(page.getContent().size()).isEqualTo(2);
// assertThat(page.getContent().getFirst().getMember()).isEqualTo(member); 프록시 객체 equals 정의하기
}
}
어차피 JPA가 구현해주는 부분이니까 엄밀한 테스트가 필요하진 않을 것 같다.
- PageRequest는 Pageable의 구현체이다.
포트
@SpringBootTest
@Transactional
@Import(AnthiveTestConfiguration.class)
class PostModifyTest {
@Autowired MemberRegister memberRegister;
@Autowired PostModify postModify;
@Autowired PostFinder postFinder;
@Autowired EntityManager entityManager;
Member member;
Post post;
@BeforeEach
void setUp() {
member = memberRegister.register(MemberFixture.createRegisterRequest());
post = postModify.publishPost(member.getEmail().address(), getPublishBlogpostFormRequest());
entityManager.flush();
entityManager.clear();
}
@Test
void publishPost() {
assertThat(post.getId()).isNotNull();
}
@Test
void publishPostFail() {
PublishBlogpostFormRequest request = new PublishBlogpostFormRequest(null, null, "This is content");
assertThatThrownBy(()->postModify.publishPost(member.getEmail().address(), request)).
isInstanceOf(ConstraintViolationException.class);
}
@Test
void editPost() {
postModify.editPost(member.getEmail().address(), new PublishBlogpostFormRequest(post.getId(), "new title", "new content"));
entityManager.flush();
entityManager.clear();
assertThat(postFinder.getPost(post.getId()).getContent()).isEqualTo("new content");
}
@Test
void editPostFail() {
Post post = postModify.publishPost(member.getEmail().address(), getPublishBlogpostFormRequest());
assertThatThrownBy(()->postModify.editPost(member.getEmail().address(), new PublishBlogpostFormRequest(post.getId(), null, "new content")))
.isInstanceOf(ConstraintViolationException.class);
}
@Test
void deletePost() {
Authentication auth = mock(Authentication.class);
given(auth.getName()).willReturn(member.getEmail().address());
postModify.deletePost(post.getId(), auth);
entityManager.flush();
entityManager.clear();
assertThatThrownBy(()->postFinder.getPost(post.getId()))
.isInstanceOf(PostNotFoundException.class);
}
}
deletePost를 테스트할 때 사용자의 인증이 필요한데 그냥 Mock으로 처리했다.
@SpringBootTest
@Transactional
@Import(AnthiveTestConfiguration.class)
class PostFinderTest {
@Autowired MemberRegister memberRegister;
@Autowired PostModify postModify;
@Autowired PostFinder postFinder;
@Autowired EntityManager entityManager;
Member member;
Post post;
@BeforeEach
void setUp() {
member = memberRegister.register(MemberFixture.createRegisterRequest());
post = postModify.publishPost(member.getEmail().address(), getPublishBlogpostFormRequest());
entityManager.flush();
entityManager.clear();
}
@Test
void getUsersPosts_success() {
// given
Pageable pageable = PageRequest.of(0, 10);
// when
Page<Post> result = postFinder.getUsersPosts(member.getEmail().address(), pageable);
// then
assertThat(result.getTotalElements()).isEqualTo(1);
}
@Test
void getUsersPosts_memberNotFound() {
// given
String username = "unknown@test.com";
Pageable pageable = PageRequest.of(0, 10);
// when - then
assertThrows(UsernameNotFoundException.class,
() -> postFinder.getUsersPosts(username, pageable));
}
@Test
void getPost_success() {
// when
Post result = postFinder.getPost(post.getId());
// then
assertThat(result.getId()).isEqualTo(post.getId());
}
@Test
void getPost_notFound() {
assertThrows(PostNotFoundException.class,
() -> postFinder.getPost(1L));
}
}
첫번째 테스트에서 DB에 게시글이 저장되고 다시 삭제되지만 세번째 테스트에서 다시 게시글을 작성하면 id는 2부터 시작한다. 이건 DB를 다뤄봤다면 알것이다. 데이터를 삭제한다고 해서 해당 id부터 다시 채워지진 않는다는 거.
@SpringBootTest
class PostPermissionMockTest {
@MockitoBean
private MemberRepository memberRepository; // 실제 객체 사용 X
@MockitoBean
private PostRepository postRepository; // 실제 객체 사용 X
@Autowired
private PostPermission postPermission; // 구현체는 PostService
@Test
void author_can_delete_post() {
// given
Long postId = 1L;
Member mockMember = Mockito.mock(Member.class);
Mockito.when(mockMember.getEmail()).thenReturn(new Email("user@test.com"));
Post mockPost = Mockito.mock(Post.class);
Mockito.when(mockPost.getMember()).thenReturn(mockMember);
// PostRepository 동작 모킹
Mockito.when(postRepository.findById(postId))
.thenReturn(Optional.of(mockPost));
// 인증 객체 모킹
Authentication auth = Mockito.mock(Authentication.class);
Mockito.when(auth.getName()).thenReturn("user@test.com");
// when & then — 예외 없어야 정상
assertDoesNotThrow(() ->
postPermission.checkAuthorPermission(postId, auth)
);
}
@Test
void non_author_cannot_delete_post() {
// given
Long postId = 1L;
Member mockAuthor = Mockito.mock(Member.class);
Mockito.when(mockAuthor.getEmail()).thenReturn(new Email("author@test.com"));
Post mockPost = Mockito.mock(Post.class);
Mockito.when(mockPost.getMember()).thenReturn(mockAuthor);
// PostRepository 동작 모킹
Mockito.when(postRepository.findById(postId))
.thenReturn(Optional.of(mockPost));
Authentication auth = Mockito.mock(Authentication.class);
Mockito.when(auth.getName()).thenReturn("other@test.com");
// when & then — SecurityException 발생해야 함
assertThrows(SecurityException.class, () ->
postPermission.checkAuthorPermission(postId, auth)
);
}
}
마지막으로 Permission을 검사하는 테스트는 실제로 DB까지 가지 않고 그냥 리파지토리의 함수를 모킹했다.
이전에 쓰던 @MockBean 이 @MockitoBean으로 변경되었다.
@ExtendWith(MockitoExtension.class)
class PostPermissionUnitTest {
@InjectMocks
@Spy
private PostService postService; // <— Spy로 만들어야 내부 메서드 mocking 가능
@Test
void mock_internal_getPost() {
// given
Post mockPost = Mockito.mock(Post.class);
Member mockMember = Mockito.mock(Member.class);
when(mockMember.getEmail()).thenReturn(new Email("author@test.com"));
when(mockPost.getMember()).thenReturn(mockMember);
// ★ 핵심: getPost 내부 호출을 stub
doReturn(mockPost).when(postService).getPost(1L);
Authentication auth = Mockito.mock(Authentication.class);
when(auth.getName()).thenReturn("author@test.com");
// when & then
assertDoesNotThrow(() ->
postService.checkAuthorPermission(1L, auth)
);
}
}
혹은 이런 식으로 내부 메서드를 모킹하는것도 가능하다. 다만 인터페이스가 아닌 구현체를 직접 테스트해서 별로 마음에 들지는 않는다.
'스프링 부트로 블로그 서비스 개발하기' 카테고리의 다른 글
| 대규모 시스템 설계 인강) MSA화 진행하기: 모듈 추가하고 build.gradle 분리 (0) | 2026.01.07 |
|---|---|
| 포스트 CRUD 기능 - 컨트롤러단 + API 테스트 (0) | 2025.12.07 |
| 포스트 CRUD 기능 - 도메인과 서비스단 (0) | 2025.12.05 |
| 회원가입 기능 - 테스트 코드 작성 (0) | 2025.12.02 |
| 회원가입 기능 - 컨트롤러단 인풋 검증 (0) | 2025.11.27 |