0. 개요
쇼핑몰 프로젝트의 코드는 MVC 구조로 작성했다.
Controller - Service - Repository 이렇게 크게 3가지의 구조인데, 각각의 단위 테스트를 어떻게 했는지 공유하려고 한다.
처음에는 @SpringBootTest를 사용해서 통합 테스트를 진행했지만, 애플리케이션의 설정과 모든 빈을 로드하는 과정에서 시간이 오래 걸렸다. 코드가 점점 복잡해지면서 테스트가 느려졌기 때문에, 최종적으로는 단위 테스트로 방향을 바꿔 진행하기로 했다.
테스트 코드는 when - given - then 형식으로 작성해야 한다.
1. 의존성 추가: spring-boot-starter-test
JUnit 5, AssertJ, Mockito, JsonPath ...와 같은 유용한 라이브러리를 제공한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2. Controller 테스트 - @WebMvcTest와 MockMvc
컨트롤러 단의 테스트는 주로 HTTP 요청과 응답에 관한 테스트를 진행했다.
(1) @WebMvcTest
- Spring MVC 테스트에 사용하는 애너테이션으로, 주로 Controller 레벨에서의 테스트를 수행할 때 사용된다. 이는 Spring Boot의 전체 애플리케이션 컨텍스트를 로드하지 않고, 웹 계층만 로드하여 더 빠르고 효율적으로 테스트를 수행할 수 있다.
- 서비스, 레포지토리, 기타 컴포넌트는 로딩하지 않는다.
- MockMvc와 함께 사용하여 실제 HTTP 요청을 모방한 테스트를 진행할 수 있다.
(2) MockMvc
- 서블릿 컨테이커(Servlet Container) 없이 HTTP 요청을 모방하여 컨트롤러를 테스트할 수 있게 도와주는 도구다.
(3) 코드
- @WebMvcTest(controllers = MemberController.class) -> 어떤 controller만 테스트 할 것이다.
- @MockBean은 가짜 객체를 만들어서 사용하는 것이다. 이로 인해 실제 서비스 구현을 건드리지 않고 테스트를 할 수 있다.
@WebMvcTest(controllers = MemberController.class)
public class MemberControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private MemberService memberService;
@MockBean
private GradeService gradeService;
@Test
@DisplayName("GET Member by memberId using RequestHeader")
void getMemberByIdTest() throws Exception {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.reconstruct(grade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role, Member.Status.ACTIVE, LocalDateTime.now(), LocalDateTime.now());
MemberResponseDto result = MemberResponseDto.from(member);
Mockito.when(memberService.getMemberById(Mockito.anyLong())).thenReturn(result);
mockMvc.perform(get("/task/members")
.header("X-MEMBER-ID", "1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.grade").value(grade.getName()))
.andExpect(jsonPath("$.role").value(role.getName()))
.andExpect(jsonPath("$.name").value(member.getName()))
.andExpect(jsonPath("$.loginId").value(member.getLoginId()))
.andExpect(jsonPath("$.dateOfBirth").value(String.valueOf(member.getDateOfBirth())))
.andExpect(jsonPath("$.gender").value(String.valueOf(member.getGender())))
.andExpect(jsonPath("$.email").value(member.getEmail()))
.andExpect(jsonPath("$.phoneNumber").value(member.getPhoneNumber()))
.andExpect(jsonPath("$.status").value(String.valueOf(member.getStatus())))
.andExpect(jsonPath("$.createdAt").value(containsString(member.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))))
.andExpect(jsonPath("$.lastLoginAt").value(containsString(member.getLastLoginAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))));
}
@Test
@DisplayName("GET Member by memberId using PathVariable")
void getMemberByParameterId() throws Exception {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.reconstruct(grade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role, Member.Status.ACTIVE, LocalDateTime.now(), LocalDateTime.now());
MemberResponseDto result = MemberResponseDto.from(member);
Mockito.when(memberService.getMemberById(Mockito.anyLong())).thenReturn(result);
mockMvc.perform(get("/task/members/{memberId}", 1))
.andExpect(status().isOk())
.andExpect(jsonPath("$.grade").value(grade.getName()))
.andExpect(jsonPath("$.role").value(role.getName()))
.andExpect(jsonPath("$.name").value(member.getName()))
.andExpect(jsonPath("$.loginId").value(member.getLoginId()))
.andExpect(jsonPath("$.dateOfBirth").value(String.valueOf(member.getDateOfBirth())))
.andExpect(jsonPath("$.gender").value(String.valueOf(member.getGender())))
.andExpect(jsonPath("$.email").value(member.getEmail()))
.andExpect(jsonPath("$.phoneNumber").value(member.getPhoneNumber()))
.andExpect(jsonPath("$.status").value(String.valueOf(member.getStatus())))
.andExpect(jsonPath("$.createdAt").value(containsString(member.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))))
.andExpect(jsonPath("$.lastLoginAt").value(containsString(member.getLastLoginAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))));
}
@Test
@DisplayName("POST Member")
void createMemberTest() throws Exception {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.reconstruct(grade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role, Member.Status.ACTIVE, LocalDateTime.now(), LocalDateTime.now());
MemberResponseDto result = MemberResponseDto.from(member);
Mockito.when(memberService.registerMember(Mockito.any())).thenReturn(result);
mockMvc.perform(post("/task/members")
.contentType(MediaType.APPLICATION_JSON)
.content("{\n" +
" \"name\": \"김주혁\",\n" +
" \"loginId\": \"joo\",\n" +
" \"password\": \"jjjjjjjjjj\",\n" +
" \"dateOfBirth\": \"2024-12-20\",\n" +
" \"gender\": \"M\",\n" +
" \"email\": \"helloworld@gmail.com\",\n" +
" \"phoneNumber\": \"01011111111\"\n" +
"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.grade").value(grade.getName()))
.andExpect(jsonPath("$.role").value(role.getName()))
.andExpect(jsonPath("$.name").value(member.getName()))
.andExpect(jsonPath("$.loginId").value(member.getLoginId()))
.andExpect(jsonPath("$.dateOfBirth").value(String.valueOf(member.getDateOfBirth())))
.andExpect(jsonPath("$.gender").value(String.valueOf(member.getGender())))
.andExpect(jsonPath("$.email").value(member.getEmail()))
.andExpect(jsonPath("$.phoneNumber").value(member.getPhoneNumber()))
.andExpect(jsonPath("$.status").value(String.valueOf(member.getStatus())))
.andExpect(jsonPath("$.createdAt").value(containsString(member.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))))
.andExpect(jsonPath("$.lastLoginAt").value(containsString(member.getLastLoginAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))));
}
@Test
@DisplayName("PUT Member")
void updateMemberTest() throws Exception {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.reconstruct(grade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role, Member.Status.ACTIVE, LocalDateTime.now(), LocalDateTime.now());
MemberResponseDto result = MemberResponseDto.from(member);
Mockito.when(memberService.modifyMember(Mockito.any(), Mockito.any())).thenReturn(result);
mockMvc.perform(put("/task/members")
.header("X-MEMBER-ID", 1L)
.contentType(MediaType.APPLICATION_JSON)
.content("{\n" +
" \"name\": \"김주혁\",\n" +
" \"password\": \"jjjjjjjjjj\",\n" +
" \"email\": \"helloworld@gmail.com\",\n" +
" \"phoneNumber\": \"01011111111\"\n" +
"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.grade").value(grade.getName()))
.andExpect(jsonPath("$.role").value(role.getName()))
.andExpect(jsonPath("$.name").value(member.getName()))
.andExpect(jsonPath("$.loginId").value(member.getLoginId()))
.andExpect(jsonPath("$.dateOfBirth").value(String.valueOf(member.getDateOfBirth())))
.andExpect(jsonPath("$.gender").value(String.valueOf(member.getGender())))
.andExpect(jsonPath("$.email").value(member.getEmail()))
.andExpect(jsonPath("$.phoneNumber").value(member.getPhoneNumber()))
.andExpect(jsonPath("$.status").value(String.valueOf(member.getStatus())))
.andExpect(jsonPath("$.createdAt").value(containsString(member.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))))
.andExpect(jsonPath("$.lastLoginAt").value(containsString(member.getLastLoginAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))));
}
@Test
@DisplayName("DELETE Member")
void deleteMemberTest() throws Exception {
mockMvc.perform(delete("/task/members")
.header("X-MEMBER-ID", "1"))
.andExpect(status().is2xxSuccessful());
Mockito.verify(memberService, Mockito.times(1)).removeMember(1L);
}
@Test
@DisplayName("Change Member Status To ACTIVE")
void modifyMemberStatus() throws Exception {
mockMvc.perform(get("/task/members/status/{loginId}", "joo"))
.andExpect(status().is2xxSuccessful());
Mockito.verify(memberService, Mockito.times(1)).changeStatusToActivation("joo");
}
@Test
@DisplayName("GET Member Grade")
void getGradeTest() throws Exception {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.reconstruct(grade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role, Member.Status.ACTIVE, LocalDateTime.now(), LocalDateTime.now());
MemberResponseDto memberResponseDto = MemberResponseDto.from(member);
GradeResponseDto gradeResponseDto = GradeResponseDto.from(grade);
Mockito.when(memberService.getMemberById(Mockito.anyLong())).thenReturn(memberResponseDto);
Mockito.when(gradeService.getGradeByName(grade.getName())).thenReturn(gradeResponseDto);
mockMvc.perform(get("/task/members/grade")
.header("X-MEMBER-ID", "1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(gradeResponseDto.id()))
.andExpect(jsonPath("$.name").value(gradeResponseDto.name()))
.andExpect(jsonPath("$.accumulationRate").value(gradeResponseDto.accumulationRate()))
.andExpect(jsonPath("$.description").value(gradeResponseDto.description()));
}
@Test
@DisplayName("Check Membership")
void checkMembershipTest() throws Exception {
MembershipCheckRequestDto membershipCheckRequestDto = new MembershipCheckRequestDto("password");
MembershipCheckResponseDto membershipCheckResponseDto = new MembershipCheckResponseDto(true);
Mockito.when(memberService.validateMembership(1L, membershipCheckRequestDto)).thenReturn(membershipCheckResponseDto);
mockMvc.perform(post("/task/members/membership")
.header("X-MEMBER-ID", "1")
.contentType(MediaType.APPLICATION_JSON)
.content("{\n" +
" \"password\": \"password\"\n" +
"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.isMember").value(membershipCheckResponseDto.isMember()));
}
@Test
@DisplayName("Member NetAmountPayments")
void memberNetAmountPaymentsTest() throws Exception {
Integer totalAmount = 1000;
Mockito.when(memberService.memberNetPaymentAmount(Mockito.anyLong())).thenReturn(totalAmount);
mockMvc.perform(get("/task/members/payments/net-amount")
.header("X-MEMBER-ID", "1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").value(totalAmount));
}
@Test
@DisplayName("GET Books for Member")
void getListBooksForMemberTest() throws Exception {
Publisher publisher = new Publisher();
publisher.setName("joo");
Book book1 = new Book();
book1.setBookId(1L);
book1.setTitle("test");
book1.setPrice(100);
book1.setContent("test");
book1.setAmount(10);
book1.setDescription("test");
book1.setPubdate(LocalDate.now());
book1.setIsbn13("1111111111111");
book1.setSalePrice(500);
book1.setPublisher(publisher);
Image image = new Image();
image.setImageId(1L);
image.setBook(new Book());
image.setName("imageName");
image.setUrl("http://test-image-url.com");
MemberLikeViewDto memberLikeViewDto = new MemberLikeViewDto(book1, image);
List<MemberLikeViewDto> memberLikeViewDtoList = List.of(memberLikeViewDto);
Mockito.when(memberService.getLikeBooksByMemberId(Mockito.anyLong())).thenReturn(memberLikeViewDtoList);
mockMvc.perform(get("/task/members/likes/books")
.header("X-MEMBER-ID", "1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].book.bookId").value(book1.getBookId()))
.andExpect(jsonPath("$[0].book.title").value(book1.getTitle()))
.andExpect(jsonPath("$[0].book.price").value(book1.getPrice()))
.andExpect(jsonPath("$[0].book.content").value(book1.getContent()))
.andExpect(jsonPath("$[0].book.amount").value(book1.getAmount()))
.andExpect(jsonPath("$[0].book.description").value(book1.getDescription()))
.andExpect(jsonPath("$[0].book.pubdate").value(book1.getPubdate().toString()))
.andExpect(jsonPath("$[0].book.isbn13").value(book1.getIsbn13()))
.andExpect(jsonPath("$[0].book.salePrice").value(book1.getSalePrice()))
.andExpect(jsonPath("$[0].book.publisher.name").value(book1.getPublisher().getName()))
.andExpect(jsonPath("$[0].image.imageId").value(image.getImageId()))
.andExpect(jsonPath("$[0].image.name").value(image.getName()))
.andExpect(jsonPath("$[0].image.url").value(image.getUrl()));
}
@Test
@DisplayName("GET loginId by memberId")
void getMemberIdByLoginIdTest() throws Exception {
Mockito.when(memberService.getLoginIdById(Mockito.anyLong())).thenReturn("joo");
mockMvc.perform(get("/task/members/loginId")
.header("X-MEMBER-ID", 1L))
.andExpect(status().isOk())
.andExpect(content().string("joo"));
}
@Test
@DisplayName("GET Member For JWT")
void getMemberForJwtTest() throws Exception {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.reconstruct(grade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role, Member.Status.ACTIVE, LocalDateTime.now(), LocalDateTime.now());
Mockito.when(memberService.getMemberByLoginId(Mockito.anyString())).thenReturn(member);
mockMvc.perform(get("/task/members/jwt/{loginId}", "joo")
.header("X-MEMBER-ID", "1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(member.getId()))
.andExpect(jsonPath("$.loginId").value(member.getLoginId()))
.andExpect(jsonPath("$.role").value(member.getRole().getName()));
}
}
3. Service 테스트 - @ExtendWith(MockitoExtention.class)와 Mockito
서비스 단의 테스트는 비즈니스 로직을 다루기 때문에, 로직이 의도된 대로 동작하는가에 초점을 두고 테스트를 진행했다.
(1) @ExtendWith(MockitoExtention.class)
- JUnit 5에서 Mockito를 사용할 때 필요한 어노테이션으로, Mockito를 확장해서 테스트에 사용할 수 있도록 해준다.
- @Mock, @InjectMocks 등으로 선언된 객체들이 자동으로 초기화되거나 주입된다.
(2) Mockito
- 모의 객체를 만들어서 테스트하는 데 사용되는 Java 라이브러리다.
- 테스트 중에 의존성 객체를 실제 객체 대신 모의 객체를 사용하여, 빠르고 독립적인 단위 테스트를 작성할 수 있게 도와줍니다.
(3) 코드
- @ExtendWith(MockitoExtension.class) -> Junit 5에서 Mockito를 사용할 수 있도록 확장함.
- @Mock -> Mock 객체를 자동으로 생성.
- @InjectMocks -> 해당 객체에 의존성을 주입하는 기능을 제공.
ex) MemberService가 @InjectMocks로 지정되어 있다. 그럼 이 객체에 필요한 Mock 객체들(roleService, gradeService, memberRepository)이 memberService로 주입된다.
@Slf4j
@ExtendWith(MockitoExtension.class)
public class MemberServiceTest {
@Mock
private RoleServiceImpl roleService;
@Mock
private GradeServiceImpl gradeService;
@Mock
private MemberRepository memberRepository;
@InjectMocks
private MemberServiceImpl memberService;
@Test
@DisplayName("Get All Members Successfully")
void getAllMembersTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.createNewMember(grade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role);
Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Order.desc("createdAt")));
List<Member> memberList = List.of(member);
Page<Member> memberPage = new PageImpl<>(memberList, pageable, memberList.size());
Mockito.when(memberRepository.findAll(pageable)).thenReturn(memberPage);
Page<MemberResponse> result = memberService.getAllMembers(pageable);
Mockito.verify(memberRepository, Mockito.times(1)).findAll(pageable);
assertThat(result.getPageable().getPageSize()).isEqualTo(10);
assertThat(result.getContent().getFirst().name()).isEqualTo(member.getName());
assertThat(result.getContent().getFirst().gender()).isEqualTo("M");
assertThat(result.getContent().getFirst().email()).isEqualTo("helloworld@gmail.com");
assertThat(result.getContent().getFirst().phoneNumber()).isEqualTo("01011111111");
}
@Test
@DisplayName("Get All Members Failed - Negative page number")
void getAllMembersFailedTest() {
Pageable pageable = PageRequest.of(0, -1, Sort.by(Sort.Order.desc("createdAt")));
assertThrows(MemberIllegalArgumentException.class, () -> memberService.getAllMembers(pageable));
}
@Test
@DisplayName("Get Member Successfully")
void getMemberByIdTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.createNewMember(grade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role);
Mockito.when(memberRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(member));
MemberResponseDto result = memberService.getMemberById(1L);
Mockito.verify(memberRepository, Mockito.times(1)).findById(1L);
assertThat(result.name()).isEqualTo(member.getName());
assertThat(result.gender()).isEqualTo("M");
assertThat(result.email()).isEqualTo("helloworld@gmail.com");
assertThat(result.phoneNumber()).isEqualTo("01011111111");
}
@Test
@DisplayName("Get Member Failed 1 - memberId doesn't exist")
void getMemberByIdFailedTest1() {
assertThrows(MemberNotFoundException.class, () -> memberService.getMemberById(-1L));
}
@Test
@DisplayName("Get Member Failed 2 - DB error")
void getMemberByIdFailedTest2() {
Mockito.when(memberRepository.existsById(Mockito.anyLong())).thenReturn(true);
Mockito.when(memberRepository.findById(Mockito.anyLong())).thenThrow(MemberNotFoundException.class);
assertThrows(MemberNotFoundException.class, () -> memberService.getMemberById(1l));
}
@Test
@DisplayName("Get Member by LoginId Successfully")
void getMemberByLoginIdTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.createNewMember(grade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role);
Mockito.when(memberRepository.findByLoginId(Mockito.anyString())).thenReturn(Optional.of(member));
Member result = memberService.getMemberByLoginId("joo");
Mockito.verify(memberRepository, Mockito.times(1)).findByLoginId(Mockito.anyString());
assertThat(result.getName()).isEqualTo(member.getName());
assertThat(result.getLoginId()).isEqualTo(member.getLoginId());
assertThat(result.getPassword()).isEqualTo(member.getPassword());
assertThat(result.getGender()).isEqualTo(member.getGender());
assertThat(result.getEmail()).isEqualTo(member.getEmail());
assertThat(result.getPhoneNumber()).isEqualTo(member.getPhoneNumber());
}
@Test
@DisplayName("Get Member by LoginId Failed 1 - memberId doens't exist")
void getLoginByIdFailedTest1() {
assertThrows(MemberNotFoundException.class, () -> memberService.getLoginIdById(1L));
}
@Test
@DisplayName("Get Member by LoginId Failed 2 - member not found")
void getLoginByIdFailedTest2() {
assertThrows(MemberNotFoundException.class, () -> memberService.getLoginIdById(1L));
}
@Test
@DisplayName("Exists Id Successfully")
void existsByIdTest() {
memberService.existsById(1L);
Mockito.verify(memberRepository, Mockito.times(1)).existsById(Mockito.anyLong());
}
@Test
@DisplayName("Exists LoginId Successfully")
void existsByLoginIdTest() {
memberService.existsByLoginId("joo");
Mockito.verify(memberRepository, Mockito.times(1)).existsByLoginId(Mockito.anyString());
}
@Test
@DisplayName("Register Member Successfully")
void registerMemberTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.createNewMember(grade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role);
GradeResponseDto gradeResponseDto = new GradeResponseDto(1,"ROYAL", 10, "일반 등급");
RoleResponseDto roleResponseDto = new RoleResponseDto(1, "MEMBER", "일반 회원");
Mockito.when(memberRepository.existsByLoginId(Mockito.anyString())).thenReturn(false);
Mockito.when(gradeService.getDefaultGrade()).thenReturn(gradeResponseDto);
Mockito.when(roleService.getDefaultRole()).thenReturn(roleResponseDto);
Mockito.when(memberRepository.save(Mockito.any())).thenReturn(member);
// Mockito.doNothing().when(pointService).registerMemberPoints(Mockito.any());
MemberResponseDto result = memberService.registerMember(new MemberRegisterRequestDto(
"김주혁",
"joo",
"jjjjjjjjjj",
LocalDate.now(),
Member.Gender.M,
"helloworld@gmail.com",
"01011111111"
));
Mockito.verify(memberRepository, Mockito.times(1)).save(Mockito.any());
assertThat(result.name()).isEqualTo(member.getName());
assertThat(result.gender()).isEqualTo("M");
assertThat(result.email()).isEqualTo("helloworld@gmail.com");
assertThat(result.phoneNumber()).isEqualTo("01011111111");
}
@Test
@DisplayName("Register Member Failed 1 - loginId already exists")
void registerMemberFailedTest1() {
Mockito.when(memberService.existsByLoginId(Mockito.anyString())).thenReturn(true);
assertThrows(MemberIllegalArgumentException.class,
() -> memberService.registerMember(new MemberRegisterRequestDto(
"김주혁",
"joo",
"jjjjjjjjjj",
LocalDate.now(),
Member.Gender.M,
"helloworld@gmail.com",
"01011111111"
)));
}
@Test
@DisplayName("Register Member Failed 2 - DB error")
void registerMemberFailedTest2() {
GradeResponseDto gradeResponseDto = new GradeResponseDto(1,"ROYAL", 10, "일반 등급");
RoleResponseDto roleResponseDto = new RoleResponseDto(1, "MEMBER", "일반 회원");
Mockito.when(memberRepository.existsByLoginId(Mockito.anyString())).thenReturn(false);
Mockito.when(gradeService.getDefaultGrade()).thenReturn(gradeResponseDto);
Mockito.when(roleService.getDefaultRole()).thenReturn(roleResponseDto);
Mockito.when(memberRepository.save(Mockito.any())).thenThrow(DataIntegrityViolationException.class);
assertThrows(MemberIllegalArgumentException.class,
() -> memberService.registerMember(new MemberRegisterRequestDto(
"김주혁",
"joo",
"jjjjjjjjjj",
LocalDate.now(),
Member.Gender.M,
"helloworld@gmail.com",
"01011111111"
)));
}
@Test
@DisplayName("Modify Member Successfully 1 - Password changed")
void modifyMemberTest1() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.createNewMember(grade, "김주혁", "joo", BCrypt.hashpw("jjjjjjjjjj", BCrypt.gensalt()), LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role);
Mockito.when(memberRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(member));
MemberResponseDto result = memberService.modifyMember(Mockito.anyLong() ,new MemberModifyRequestDto(
"혁주김",
"oooooooooo",
"worldhello@gmail.com",
"01022222222"
));
Mockito.verify(memberRepository, Mockito.times(1)).findById(Mockito.any());
assertThat(result.name()).isEqualTo(member.getName());
assertThat(result.gender()).isEqualTo("M");
assertThat(result.email()).isEqualTo("worldhello@gmail.com");
assertThat(result.phoneNumber()).isEqualTo("01022222222");
}
@Test
@DisplayName("Modify Member Successfully 2 - Password not changed")
void modifyMemberTest2() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.createNewMember(grade, "김주혁", "joo", BCrypt.hashpw("jjjjjjjjjj", BCrypt.gensalt()), LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role);
Mockito.when(memberRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(member));
MemberResponseDto result = memberService.modifyMember(Mockito.anyLong() ,new MemberModifyRequestDto(
"혁주김",
"jjjjjjjjjj",
"worldhello@gmail.com",
"01022222222"
));
Mockito.verify(memberRepository, Mockito.times(1)).findById(Mockito.any());
assertThat(result.name()).isEqualTo(member.getName());
assertThat(result.gender()).isEqualTo("M");
assertThat(result.email()).isEqualTo("worldhello@gmail.com");
assertThat(result.phoneNumber()).isEqualTo("01022222222");
}
@Test
@DisplayName("Modify Member Failed - Db error")
void modifyMemberFailedTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.createNewMember(grade, "김주혁", "joo", BCrypt.hashpw("jjjjjjjjjj", BCrypt.gensalt()), LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "010-1111-1111", role);
Mockito.when(memberRepository.existsById(Mockito.anyLong())).thenReturn(true);
Mockito.when(memberRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(member));
Mockito.when(memberRepository.save(Mockito.any())).thenThrow(DataIntegrityViolationException.class);
assertThrows(MemberDataIntegrityViolationException.class,
() -> memberService.modifyMember(Mockito.anyLong() ,new MemberModifyRequestDto(
"혁주김",
"jjjjjjjjjj",
"worldhello@gmail.com",
"010-2222-2222"
)));
}
@Test
@DisplayName("Remove Member Successfully")
void removeMemberTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.createNewMember(grade, "김주혁", "joo", BCrypt.hashpw("jjjjjjjjjj", BCrypt.gensalt()), LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role);
Mockito.when(memberRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(member));
memberService.removeMember(Mockito.anyLong());
Mockito.verify(memberRepository, Mockito.times(1)).findById(Mockito.any());
}
@Test
@DisplayName("Change Status1 - To ACTIVE")
void changeStatusToActivation1() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.createNewMember(grade, "김주혁", "joo", BCrypt.hashpw("jjjjjjjjjj", BCrypt.gensalt()), LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role);
Mockito.when(memberRepository.findByLoginId(Mockito.anyString())).thenReturn(Optional.of(member));
memberService.changeStatusToActivation(Mockito.anyString());
Mockito.verify(memberRepository, Mockito.times(1)).findByLoginId(Mockito.any());
}
@Test
@DisplayName("Change Status2 - To SUSPENDED")
void changeStatusToActivation2() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.createNewMember(grade, "김주혁", "joo", BCrypt.hashpw("jjjjjjjjjj", BCrypt.gensalt()), LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", role);
Mockito.when(memberRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(member));
memberService.changeStatusToActivation(Mockito.anyLong(), "SUSPENDED");
Mockito.verify(memberRepository, Mockito.times(1)).findById(Mockito.any());
}
@Test
@DisplayName("Remove Member Failed - DB error")
void removeMemberFailedTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Member member = Member.createNewMember(grade, "김주혁", "joo", BCrypt.hashpw("jjjjjjjjjj", BCrypt.gensalt()), LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "010-1111-1111", role);
Mockito.when(memberRepository.existsById(Mockito.anyLong())).thenReturn(true);
Mockito.when(memberRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(member));
Mockito.when(memberRepository.save(Mockito.any())).thenThrow(DataIntegrityViolationException.class);
assertThrows(MemberDataIntegrityViolationException.class,
() -> memberService.removeMember(Mockito.anyLong()));
}
}
4. Repository 테스트 - @DataJpaTest와 TestEntityManager
레포지토리 단의 테스트는 데이터베이스와의 상호작용을 검증하는 테스트를 진행했다.
(1) @DataJpaTest
- Spring Data JPA와 관련된 테스트를 수행할 때 사용하는 어노테이션이다. (JPA 레포지토리 계층만을 테스트하는데 특화)
- 코드를 들어가 보면 @Transactional 이 적용되어 있다. 따라서 테스트 메소드가 실행된 후, DB 상태가 자동으로 롤백된다.
- JPA관련 빈들만 로드해주고, 인 메모리 DB를 사용해서 테스트가 진행된다.
(2) TestEntityManager
- Spring Boot JPA 테스트에서, JPA 엔티티를 저장/조회/수정/삭제 등의 작업을 실제 DB 없이도 수행할 수 있도록 하는 클래스.
- 영속성 컨텍스트에 직접 엔티티를 추가하거나, 변경 사항을 적용할 수 있다.
- @DataJpaTest와 함께 사용하면 테스트가 종료된 후 DB 상태가 자동으로 롤백된다.
(3) 코드
- @DataJpaTest: @Transactional이 있어서 테스트가 끝나면 DB를 롤백한다.
- @TestEntityManager -> Member를 생성하기 위해서는 Grade, Role이 필요하다. 이걸, TestEntityManager를 이용해서 영속화한 후 Member를 생성할 수 있다. ex) Grade savedGrade = entityManager.persist(grade);
@Slf4j
@DataJpaTest
@Import(QuerydslConfig.class)
public class MemberRepositoryTest {
/**
* 회원 삭제는 지원하지 않는다.
*/
@Autowired
private MemberRepository memberRepository;
@Autowired
private TestEntityManager entityManager;
@Test
@DisplayName("Find All Members with Pagination")
void findAllTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Grade savedGrade = entityManager.persist(grade);
Role savedRole = entityManager.persist(role);
Member member = Member.createNewMember(savedGrade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", savedRole);
Member savedMember = memberRepository.save(member);
Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Order.desc("createdAt")));
Page<Member> memberPage = memberRepository.findAll(pageable);
// page 정보 검증
assertThat(memberPage.getNumber()).isEqualTo(0);
assertThat(memberPage.getSize()).isEqualTo(10);
assertThat(memberPage.getSort().getOrderFor("createdAt").getDirection()).isEqualTo(Sort.Direction.DESC);
// 첫 번째 멤버 가져오기
Member target = memberPage.getContent().getFirst();
// 멤버 검증
assertThat(target.getGrade()).isEqualTo(savedGrade);
assertThat(target.getName()).isEqualTo(savedMember.getName());
assertThat(target.getLoginId()).isEqualTo(savedMember.getLoginId());
assertThat(target.getPassword()).isEqualTo(savedMember.getPassword());
assertThat(target.getDateOfBirth()).isEqualTo(savedMember.getDateOfBirth());
assertThat(target.getGender()).isEqualTo(savedMember.getGender());
assertThat(target.getEmail()).isEqualTo(savedMember.getEmail());
assertThat(target.getPhoneNumber()).isEqualTo(savedMember.getPhoneNumber());
assertThat(target.getRole()).isEqualTo(savedMember.getRole());
}
@Test
@DisplayName("Find Member by Id")
void findByIdTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Grade savedGrade = entityManager.persist(grade);
Role savedRole = entityManager.persist(role);
Member member = Member.createNewMember(savedGrade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", savedRole);
Member savedMember = memberRepository.save(member);
Member target = memberRepository.findById(savedMember.getId()).get();
// 멤버 검증
assertThat(target.getGrade()).isEqualTo(savedGrade);
assertThat(target.getName()).isEqualTo(savedMember.getName());
assertThat(target.getLoginId()).isEqualTo(savedMember.getLoginId());
assertThat(target.getPassword()).isEqualTo(savedMember.getPassword());
assertThat(target.getDateOfBirth()).isEqualTo(savedMember.getDateOfBirth());
assertThat(target.getGender()).isEqualTo(savedMember.getGender());
assertThat(target.getEmail()).isEqualTo(savedMember.getEmail());
assertThat(target.getPhoneNumber()).isEqualTo(savedMember.getPhoneNumber());
assertThat(target.getRole()).isEqualTo(savedMember.getRole());
}
@Test
@DisplayName("Find Member by loginId")
void findByLoginIdTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Grade savedGrade = entityManager.persist(grade);
Role savedRole = entityManager.persist(role);
Member member = Member.createNewMember(savedGrade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", savedRole);
Member savedMember = memberRepository.save(member);
Member target = memberRepository.findByLoginId(savedMember.getLoginId()).get();
// 멤버 검증
assertThat(target.getGrade()).isEqualTo(savedGrade);
assertThat(target.getName()).isEqualTo(savedMember.getName());
assertThat(target.getLoginId()).isEqualTo(savedMember.getLoginId());
assertThat(target.getPassword()).isEqualTo(savedMember.getPassword());
assertThat(target.getDateOfBirth()).isEqualTo(savedMember.getDateOfBirth());
assertThat(target.getGender()).isEqualTo(savedMember.getGender());
assertThat(target.getEmail()).isEqualTo(savedMember.getEmail());
assertThat(target.getPhoneNumber()).isEqualTo(savedMember.getPhoneNumber());
assertThat(target.getRole()).isEqualTo(savedMember.getRole());
}
@Test
@DisplayName("Find loginId by memberId")
void getLoginIdByIdTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Grade savedGrade = entityManager.persist(grade);
Role savedRole = entityManager.persist(role);
Member member = Member.createNewMember(savedGrade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", savedRole);
Member savedMember = memberRepository.save(member);
String target = memberRepository.getLoginIdById(savedMember.getId()).get();
assertThat(target).isEqualTo(savedMember.getLoginId());
}
@Test
@DisplayName("Exists memberId")
void existsByIdTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Grade savedGrade = entityManager.persist(grade);
Role savedRole = entityManager.persist(role);
Member member = Member.createNewMember(savedGrade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", savedRole);
Member savedMember = memberRepository.save(member);
boolean target = memberRepository.existsById(savedMember.getId());
assertThat(target).isEqualTo(true);
}
@Test
@DisplayName("Exists loginId")
void existsByLoginIdTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Grade savedGrade = entityManager.persist(grade);
Role savedRole = entityManager.persist(role);
Member member = Member.createNewMember(savedGrade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", savedRole);
Member savedMember = memberRepository.save(member);
boolean target = memberRepository.existsByLoginId(savedMember.getLoginId());
assertThat(target).isEqualTo(true);
}
@Test
@DisplayName("Get MemberNetPaymentAmount")
void memberNetPaymentAmountTest() {
Grade grade = Grade.create("ROYAL", 10, "일반 등급");
Role role = Role.createRole("MEMBER", "일반 회원");
Grade savedGrade = entityManager.persist(grade);
Role savedRole = entityManager.persist(role);
Member member = Member.createNewMember(savedGrade, "김주혁", "joo", "jjjjjjjjjj", LocalDate.now(), Member.Gender.M, "helloworld@gmail.com", "01011111111", savedRole);
}
}
5. 정리 및 배운점
Repository, Service, Controller 각각의 레이어에서 테스트를 진행하면서, Spring 테스트의 다양한 기능을 활용할 수 있었다.
- Controller 테스트에서는 @WebMvcTest와 MockMvc를 사용하여, 컨트롤러 레이어의 HTTP 요청/응답을 테스트할 수 있었다. 이를 통해 컨트롤러가 외부로부터 요청을 받았을 때, 적절한 응답을 반환하는지 확인할 수 있었으며, 웹 애플리케이션의 HTTP 인터페이스를 독립적으로 검증할 수 있었습니다.
- Service 테스트는 @ExtendWith(MockitoExtension.class)와 Mockito를 활용하여 서비스 로직을 테스트했다. 이 방법은 외부 의존성들을 모킹(mocking)하여, 서비스 자체의 로직만 독립적으로 검증할 수 있게 해주었습니다. Mockito를 사용하면 실제 의존 객체를 사용하지 않고도 원하는 행동을 유도할 수 있어, 테스트가 보다 명확하고 빠르게 진행되었다.
- Repository 테스트에서는 @DataJpaTest와 TestEntityManager를 사용하여 데이터베이스와의 상호작용을 독립적으로 테스트했다. 이를 통해 실제 데이터베이스에 영향을 주지 않으면서, 데이터베이스 연동 로직을 효율적으로 검증할 수 있었다.
6. 향후 개선할 부분
- 중복 코드 줄이기
- @BeforeEach나 @BeforeAll을 사용해서 공통된 데이터 세트를 한 번만 생성하고, 테스트마다 재활용하면 효율성을 높일 수 있을것이다.
- 프론트엔드 서버의 테스트 코드 작성
- 백엔드를 개발할 때는 아래와 같이 내가 작성한 코드에 한해서는 평균 80% 이상의 테스트 커버리지를 달성했다. 하지만 일정상의 문제로 프론트의 테스트 코드는 작성하지 못했는데 다음에는 프론트의 테스트도 꼭 진행할 것이다.
참고
'프로젝트 > 쇼핑몰 프로젝트' 카테고리의 다른 글
[쇼핑몰 프로젝트] Spring JWT 재발급 로직에서 Interceptor 대신 Filter를 선택한 이유 (+Spring Cloud OpenFeign, Spring MVC) (2) | 2025.08.01 |
---|---|
[쇼핑몰 프로젝트] Spring Boot 민감 정보 보호와 배포 자동화: GitHub Actions secrets & .env 활용 (0) | 2025.07.31 |
[쇼핑몰 프로젝트] 분산 환경에서 데이터 캐싱 - Redis (1) | 2025.03.24 |
[쇼핑몰 프로젝트] 서버 다중화 환경에서 세션 기반 인증 문제 해결 – JWT 적용 (0) | 2025.03.21 |
[쇼핑몰 프로젝트] 외부에 노출되는 PK, 어떤 전략을 사용해야 할까? (0) | 2025.03.13 |