@Entity
@Table(name = "blog_member_mappings",
indexes = {
@Index(name = "uk_blog_member_mapping",columnList = "mb_no, blog_id, role_id", unique = true)
}
)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@Getter
public class BlogMemberMapping {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "blog_member_id")
private Long blogMemberId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "mb_no", referencedColumnName = "mb_no", nullable = false)
private Member member;
@Setter
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="blog_id", referencedColumnName = "blog_id", nullable = false)
private Blog blog;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="role_id", referencedColumnName = "role_id", nullable = false)
private Role role;
private BlogMemberMapping(Member member, Blog blog, Role role) {
this.member = member;
this.blog = blog;
this.role = role;
}
public static BlogMemberMapping ofNewBlogMemberMapping(Member member, Blog blog, Role role) {
return new BlogMemberMapping(member,blog,role);
}
}
블로그 멤버 매핑은 Role, Blog, Member 세 가지 엔티티를 연결하는 중요한 관계 엔티티이다.
이러한 복잡한 관계를 테스트할 때 단위 테스트와 통합 테스트 각각의 적합성에 대해서 고민한 내용에 대해 얘기해보려한다.
이전에 Blog, Member 등에 대해서는 테스트 코드를 작성할 때 당연히 단위 테스트로 진행했다.
@SpringBootTest
@ActiveProfiles("test")
class BlogRepositoryTest {
@Autowired
BlogRepository blogRepository;
Blog blog = null;
@BeforeEach
void setUp(){
blog = Blog.ofNewBlog(
"testFid",
true,
"testBlogName",
"testNickName",
"testDescription"
);
blogRepository.save(blog);
}
@AfterEach
void tearDown(){
blogRepository.delete(blog);
}
@Test
@DisplayName("블로그 아이디 - 블로그 찾기 ")
void findBlogsByBlogId() {
Optional<Blog> testBlog = blogRepository.findBlogsByBlogId(blog.getBlogId());
assertNotNull(testBlog);
assertAll(
()-> assertEquals(true, testBlog.get().isBlogMain()),
()-> assertEquals("testFid", testBlog.get().getBlogFid()),
()-> assertEquals("testBlogName",testBlog.get().getBlogName()),
()-> assertEquals("testNickName", testBlog.get().getBlogMbNickname()),
()-> assertEquals("testDescription", testBlog.get().getBlogDescription())
);
}
...
}
@ActiveProfiles("test")
@SpringBootTest
@Transactional
class BlogServiceImplTest {
@Mock // 블로그레포지토리는 테스트가 끝났으므로 (검증 완료) 실제 객체가 아닌 Mock 객체
BlogRepository blogRepository;
@InjectMocks // Mock 객체 자동 주입 후 AutoWired
BlogServiceImpl blogService;
@Test
@DisplayName("블로그 저장")
void saveBlog() {
BlogRequest blogRequest = new BlogRequest(
"testFid",
true,
"testName",
"testNickName",
"testDescription"
);
Mockito.doAnswer(invocationOnMock -> {
Blog blog = invocationOnMock.getArgument(0);
Field blogId = Blog.class.getDeclaredField("blogId");
Field createdAt = Blog.class.getDeclaredField("createdAt");
blogId.setAccessible(true);
blogId.set(blog, 1L);
createdAt.setAccessible(true);
createdAt.set(blog, LocalDateTime.now());
return null;
})
.when(blogRepository)
.save(Mockito.any(Blog.class));
BlogResponse blogResponse = blogService.saveBlog(blogRequest);
assertNotNull(blogResponse);
assertAll(
()-> assertNotNull(blogResponse.getBlogId()),
()-> assertTrue(blogResponse.getCategories().isEmpty()),
()-> assertTrue(blogResponse.getBlogMemberMappings().isEmpty()),
()-> assertEquals("testFid",blogResponse.getBlogFid()),
()-> assertTrue(blogResponse.isBlogMain()),
()-> assertEquals("testName", blogResponse.getBlogName()),
()-> assertEquals("testNickName", blogResponse.getBlogMbNickName()),
()-> assertEquals("testDescription", blogResponse.getBlogDescription()),
()-> assertNotNull(blogResponse.getCreatedAt()),
()-> assertNull(blogResponse.getUpdatedAt()),
()-> assertTrue(blogResponse.getBlogIsPublic())
);
}
...
}
단위 테스트는 개별 컴포넌트나 메서드를 격리된 환경에서 테스트하는 방식으로 위의 예시로 서비스 테스트의 경우 Repository는 이미 검증이 끝났기 때문에 Mockito를 사용해 가짜 객체를 만들어 Service 비즈니스로직에만 집중해서 테스트를 할 수 있다.
따라서 블로그 멤버 매핑도 단위 테스트로 진행하려 했다.
통합테스트
여러 컴포넌트간의 상호작용을 테스트
@SpringBootTest
@ActiveProfiles("test")
@Transactional
class BlogMemberMappingServiceTest {
@Autowired
private BlogMemberMappingService blogMemberMappingService;
@Autowired
private BlogMemberMappingRepository blogMemberMappingRepository;
...
}
즉 실제 ID가 생성되고 관계가 맺어지는 것을 테스트해야 하므로 통합 테스트로 변경해서 진행했다.
테스트 목표인 블로그 멤버 매핑 서비스 코드를 다시 한번 보자
public class BlogMemberMappingServiceImpl implements BlogMemberMappingService {
private final BlogMemberMappingRepository blogMemberMappingRepository;
@Override
@Transactional
public void createBlogMemberMapping(Member member, Blog blog, Role role) {
Optional<BlogMemberMapping> existingMapping =
blogMemberMappingRepository.findBlogMemberMappingByMember_MbNoAndBlog_BlogId(
member.getMbNo(), blog.getBlogId()
);
if(existingMapping.isPresent()){
return;
}
BlogMemberMapping memberMapping =
BlogMemberMapping.ofNewBlogMemberMapping(
member, blog, role
);
blogMemberMappingRepository.save(memberMapping);
}
}
이거 근데 보이드면 검증이 불가능하기 때문에 dto만들어서 BlogMemberMappingResponse를 반환형을 바꿔준다면?
확인해보니 void로 지정할 경우
1. 단순성 : 구현이 간단하고 명확하고 메서드는 작업을 수행하고 끝난다
2. 또한 오직 작업 수행에만 집중하고 결과 정보 생성에 대한 책임이 없다
3. CQS(명령 쿼리 분리 원칙) 디자인 원칙
이와 같은 장점이 있지만
따라서 단순한 작업일 경우엔 void, 복잡한 비즈니스 로직이나 API에서는 DTO 반환 방식이 좀 더 유연하다고 할 수 있다.
테스트가 설계를 주도하는 것과 같은 상황에 다음과 같이 생각해보자
등을 생각하다...가
블로그 멤버 매핑 서비스의 필요성에 대해서 생각하게 됐다.
당연히 모든 엔티티는 레포지토리, 서비스가 있을 것이다 라고 깊은 고민을 해보지 않은채 단정을 지었기 때문에 미쳐 놓쳐버린 것이다.
단순 CRUD vs 비즈니스 로직: 서비스 = 비즈니스 로직, 레포지토리 = 데이터 액세스
만약 블로그 멤버 매핑이 단순한 데이터 저장/조회를 넘어 권한 검증, 비즈니스 규칙 적용 등이 필요하다면 서비스 계층이 적합하지만 지금 블로그 멤버 매핑은 간단한 데이터 저장.조회 뿐이기 때문에 레포지토리 계층에 끝내는게 맞는 것 같다.