프로젝트/쇼핑몰 프로젝트

[쇼핑몰 프로젝트] Spring MVC 구조 - 단위 테스트 코드 작성하기

Joo.v7 2025. 3. 26. 00:26

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 테스트 - @WebMvcTestMockMvc

컨트롤러 단의 테스트는 주로 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% 이상의 테스트 커버리지를 달성했다. 하지만 일정상의 문제로 프론트의 테스트 코드는 작성하지 못했는데 다음에는 프론트의 테스트도 꼭 진행할 것이다.

내가 담당한 도메인의 테스트 커버리지


 

참고