쇼핑몰 만들기 프로젝트- 회원(부제: 테스트 중심의 개발을 해보자.)

yeom yaloo·2023년 1월 17일
0

쇼핑몰

목록 보기
1/19
post-thumbnail

🛍️쇼핑몰 프로젝트 - 회원(2)


Repository

JPA 관련 test

  • 해당 작업은 객체와 관계형 데이터베이스를 연동해주는 기술 중 하나인 jpa를 사용해서 진행하기 때문에 repository를 기본적으로는 jpa에서 제공하는 것으로 상속받아 사용한다. 이를 테스트하기 위해서는 @DataJpaTest를 사용하여 진행한다.
  • @DataJpaTest: jpa component에 초점을 맞춘 jpa를 사용할 때 이를 테스트하기 위해 만들어진 애노테이션으로 레포지토리와 관련된 테스트는 이를 이용해서 진행할 것이다.
  • dummy를 사용해서 값을 주고 이를 이용한 테스트를 진행할 수 있게 한다.

[Test Doubles]

https://jesusvalerareales.medium.com/testing-with-test-doubles-7c3abb9eb3f2
📌출처: https://jesusvalerareales.medium.com/testing-with-test-doubles-7c3abb9eb3f2

  • Dummy: It is used as a placeholder when an argument needs to be filled in.
  • Stub: It provides fake data to the SUT (System Under Test).
  • Spy: It records information about how the class is being used.
  • Mock: It defines an expectation of how it will be used. It will cause failure if the expectation isn’t met.
    Fake: It is an actual implementation of the contract but is unsuitable for production.

test code

package com.boostore.version_1.member.repository;

import com.boostore.version_1.member.dummy.MemberDummy;
import com.boostore.version_1.member.dummy.MembershipDummy;
import com.boostore.version_1.member.entity.Member;
import com.boostore.version_1.member.entity.Membership;
import com.boostore.version_1.member.repository.JpaMemberRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class JpaMemberRepositoryTest {

    @Autowired
    JpaMemberRepository jpaMemberRepository;

    @Autowired
    TestEntityManager testEntityManager;

    private Member member;
    private Membership membership;

    @BeforeEach
    void setUp(){
        member = MemberDummy.dummy();
        membership = MembershipDummy.dummy();

    }

    @DisplayName("회원 저장 테스트")
    @Test
    void testSaveMember(){
        //given
        testEntityManager.persist(membership);

        //when
        Member savedMember = jpaMemberRepository.save(member);

        //then
        assertThat(savedMember).isNotNull();
        assertThat(savedMember.getMembership().getGrade()).isEqualTo(membership.getGrade());
    }

    @DisplayName("id로 해당하는 회원 조회 테스트")
    @Test
    void testMemberFindById(){
        //given
        testEntityManager.persist(membership);
        Member savedMember = testEntityManager.persist(member);

        //when
        Optional<Member> optionalMember = jpaMemberRepository.findMemberByMemberId(savedMember.getMemberId());

        //then
        assertThat(optionalMember.isPresent());
        assertThat(optionalMember.get().getName()).isEqualTo(savedMember.getName());

    }

    @DisplayName("모든 회원 조회 테스트")
    @Test
    void testMemberFindAll(){
        //given
        testEntityManager.persist(membership);
        Member savedMember = testEntityManager.persist(member);

        //when
        List<Member> members = jpaMemberRepository.findAll();

        //then
        assertThat(members).isNotNull();
        assertThat(members.size()).isOne();
    }


    /** 회원가입과 관련된 service 레이어에서 사용될 메소드를 검증하기 위한 테스트 */
    @DisplayName("해당 id를 가진 회원이 있는지 조회하는 테스트")
    @Test
    void testExistMemberById() {

        Member savedMember = testEntityManager.persist(member);

        boolean resultTrue = jpaMemberRepository.existsMemberById(savedMember.getId());
        assertThat(resultTrue).isTrue();

        boolean resultFalse = jpaMemberRepository.existsMemberById("test");
        assertThat(resultFalse).isFalse();

    }
    @DisplayName("해당 닉네임으로 가입한 회원이 있는지 조회하는 테스트")
    @Test
    void testExistMemberByNickname() {

        Member savedMember = testEntityManager.persist(member);

        boolean resultTrue = jpaMemberRepository.existsMemberByNickname(savedMember.getNickname());
        assertThat(resultTrue).isTrue();

        boolean resultFalse = jpaMemberRepository.existsMemberByNickname("test");
        assertThat(resultFalse).isFalse();


    }
    @DisplayName("해당 이메일로 가입한 회원이 있는지 조회하는 테스트")
    @Test
    void testExistMemberByEmailAddress() {

        Member savedMember = testEntityManager.persist(member);

        boolean resultTrue = jpaMemberRepository.existsMemberByEmailAddress(savedMember.getEmailAddress());
        assertThat(resultTrue).isTrue();

        boolean resultFalse = jpaMemberRepository.existsMemberByEmailAddress(savedMember.getEmailAddress());
        assertThat(resultFalse).isTrue();

    }


}

더미 객체

package com.boostore.version_1.member.dummy;

import com.boostore.version_1.member.domain.GenderCode;
import com.boostore.version_1.member.entity.Member;
import com.boostore.version_1.member.entity.Membership;

import java.time.LocalDateTime;

public class MemberDummy {

    public static Member dummy(){
        String id = "yeomyaloo";
        String name = "yaloo";
        String password = "1234";
        String email = "test@test.com";
        String birthday = "20001111";
        String phoneNumber = "010-0000-0000";
        String address = "ㅇㅇ시 ㅇㅇ구";
        // memberCreatedAt gradeDummy

        return Member.builder()
                .membership(MembershipDummy.dummy())
                .id(id)
                .nickname(name)
                .name(name)
                .genderCoder(GenderCode.FEMALE)
                .birthday(birthday)
                .password(password)
                .phoneNumber(phoneNumber)
                .address(address)
                .emailAddress(email)
                .memberCreatedAt(LocalDateTime.now())
                .build();
    }

    public static Member dummy(Membership membership) {
        String id = "yeomyaloo";
        String name = "yaloo";
        String password = "1234";
        String email = "test@test.com";
        String birthday = "20001111";
        String phoneNumber = "010-0000-0000";
        String address = "ㅇㅇ시 ㅇㅇ구";
        // memberCreatedAt gradeDummy

        return Member.builder()
                .membership(membership)
                .id(id)
                .nickname(name)
                .name(name)
                .genderCoder(GenderCode.FEMALE)
                .birthday(birthday)
                .password(password)
                .phoneNumber(phoneNumber)
                .address(address)
                .emailAddress(email)
                .memberCreatedAt(LocalDateTime.now())
                .build();
    }
}
  • 해당 더미 객체를 사용해서 테스트를 진행했다.
  • @DataJpaTest의 경우엔 @Transactional이 해당 애노테이션에 미리 붙여져 있기 때문에 따로 처리해주지 않아도 실제로 테이블에 값이 들어가는 등의 문제는 생기지 않는다.

더미 객체를 사용해서 테스트를 진행한 이유?

  • 해당 테스트에 더미 객체를 사용한 이유는 mock 객체처럼 실제하지 않은 값을 넣어주면 레포지토리 테스트가 되지 않기 때문이다. 이를 해결하기 위해서는 실제 값을 넣어줄 때 사용하는 dummy object를 사용해서 진행하도록 한다.
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.test.autoconfigure.orm.jpa;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.autoconfigure.OverrideAutoConfiguration;
import org.springframework.boot.test.autoconfigure.core.AutoConfigureCache;
import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.properties.PropertyMapping;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.repository.config.BootstrapMode;
import org.springframework.test.context.BootstrapWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(DataJpaTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class})
@OverrideAutoConfiguration(
    enabled = false
)
@TypeExcludeFilters({DataJpaTypeExcludeFilter.class})
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest {
    String[] properties() default {};

    @PropertyMapping("spring.jpa.show-sql")
    boolean showSql() default true;

    @PropertyMapping("spring.data.jpa.repositories.bootstrap-mode")
    BootstrapMode bootstrapMode() default BootstrapMode.DEFAULT;

    boolean useDefaultFilters() default true;

    ComponentScan.Filter[] includeFilters() default {};

    ComponentScan.Filter[] excludeFilters() default {};

    @AliasFor(
        annotation = ImportAutoConfiguration.class,
        attribute = "exclude"
    )
    Class<?>[] excludeAutoConfiguration() default {};
}

Service

Servce Layer의 test

  • @ExtendWith 애노테이션을 사용하여 테스트를 진행한다. 이때 @WebMvcTest의 경우 @Service, @Repository, @Component등의 요소들은 테스트할 수 없기 때문에 Service의 경우라면 @ExtendWith를 사용해서 진행하자.

@Controller

Controller test

  • @Controller, @RestController 컨트롤러의 경우엔 @WebMvcTest 애노테이션을 사용해서 컨트롤러부만 테스트를 진행한다. 이때 위의 설명과 같이 서비스, 레포지토리, 컴포넌트 같은 요소들은 따로 이 애노테이션을 사용해서 테스트가 불가하다.
  • @WebMvcTest의 경우엔 단위테스트로 비교적 가볍게 테스트가 가능하다.
  • 해당 컨트롤러 테스트의 경우 given으로 값을 미리 주고 테스트를 진행하기 때문에 따로 dummy등의 값이 필요하지 않다.

[Controller test]

package com.boostore.version_1.member.controller;

import com.boostore.version_1.member.dto.request.MemberCreateRequest;
import com.boostore.version_1.member.dto.response.MemberCreateResponse;
import com.boostore.version_1.member.entity.Member;
import com.boostore.version_1.member.entity.Membership;
import com.boostore.version_1.member.entity.Role;
import com.boostore.version_1.member.service.inter.MemberService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

import javax.print.attribute.standard.MediaSize;

import static org.hamcrest.Matchers.equalTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;


@WebMvcTest(MemberRestController.class)
class MemberRestControllerTest {

    private final String NAME = "yaloo";
    private final String NICKNAME = "yaloo";
    private final String ID = "yaloo";
    private final String PHONENUMBER = "01000009999";
    private final String INVALID_PASSWORD = "abcde";
    private final String VALID_PASSWORD = "asdad122ed@";
    private final String BIRTH = "19960320";
    private final String EMAIL = "test@test.com";
    private final String GENDER = "FEMALE";

    private final String ROLE_MEMBER = "ROLE_MEMBER";
    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MemberService memberService;
    private Member member;
    private MemberCreateRequest createRequest;
    private MemberCreateResponse createResponse;

    @BeforeEach
    void setUp() {

        Long memberId = 1L;
        Long roleId = 1L;


        member = Member.builder()
                .memberId(memberId)
                .name(NAME)
                .nickname(NICKNAME)
                .id(ID)
                .build();

        Role role = Role.builder()
                .roleId(roleId)
                .roleName(ROLE_MEMBER)
                .build();

        createResponse = MemberCreateResponse.fromEntity(member, role);
    }

    @DisplayName("회원 등록시 사용자로부터 받은 데이터가 검증 조건에 맞지 않은 경우 요청에 실패하도록 하는 테스트")
    @Test
    @WithMockUser
    void signUpMember_invalidInputData() throws Exception {
        //given
        MemberCreateRequest request = new MemberCreateRequest();
        Mockito.when(memberService.createMember(any())).thenReturn(createResponse);

        //when
        ResultActions perform = mockMvc.perform(post("/api/service/members/sign-up").with(csrf())
                .contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(request)));

        //then
        perform.andDo(print()).andExpect(status().isBadRequest());
        verify(memberService, never()).createMember(any());

    }



    @DisplayName("회원 가입 테스트에 시 정규 표현식에 어긋나는 경우 요청 실패 테스트")
    @Test
    @WithMockUser
    void signUpMember_invalidRegex() throws Exception {
        //given
        MemberCreateRequest request = new MemberCreateRequest(
                ID,
                NICKNAME,
                NAME,
                GENDER,
                BIRTH,
                INVALID_PASSWORD,
                PHONENUMBER,
                EMAIL
        );

        Mockito.when(memberService.createMember(any())).thenReturn(createResponse);

        //when
        ResultActions perform = mockMvc.perform(post("/api/service/members/sign-up").with(csrf())
                .contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(request)));


        //then
        perform.andDo(print()).andExpect(status().isBadRequest());

        verify(memberService, never()).createMember(any());
    }

    @DisplayName("회원 가입 성공 테스트")
    @Test
    @WithMockUser
    void signUpMember() throws Exception {
        //given
        MemberCreateRequest request = new MemberCreateRequest(
                ID,
                NICKNAME,
                NAME,
                GENDER,
                BIRTH,
                VALID_PASSWORD,
                PHONENUMBER,
                EMAIL
        );

        Mockito.when(memberService.createMember(any())).thenReturn(createResponse);


        Membership membership = Membership.createMembership();
        Member member = request.toEntity(membership);


        //when
        ResultActions perform = mockMvc.perform(post("/api/service/members/sign-up").with(csrf())
                .contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(request)));


        //then
        perform.andDo(print()).andExpect(status().isCreated())
                        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                        .andExpect(jsonPath("$.name", equalTo(member.getName())))
                        .andExpect(jsonPath("$.nickname", equalTo(member.getNickname())))
                        .andExpect(jsonPath("$.id", equalTo(member.getId())))
                        .andExpect(jsonPath("$.role", equalTo(ROLE_MEMBER)));


        verify(memberService, times(1)).createMember(any());
     }

     
}

  • Service와 같은 경우엔 @mockBean을 사용해서 진행해주어야 한다. (스프링 컨텍스트에 의해서 mock 객체를 등록하여 사용할 수 있게 해준다.)

Error 발생 처리

@WebMvcTest 중 발생한 403, 401 문제 해결

  • 시큐리티 관련 이슈로 해당 test에 애노테이션과 메소드를 붙여주면 해결된다.
    
    @DisplayName("회원 등록시 사용자로부터 받은 데이터가 검증 조건에 맞지 않은 경우 요청에 실패하도록 하는 테스트")
    @Test
    @WithMockUser
    void signUpMember() throws Exception {
        //given
        MemberCreateRequest request = new MemberCreateRequest();
        Mockito.when(memberService.createMember(any())).thenReturn(createResponse);

        //when
        ResultActions perform = mockMvc.perform(post("/api/service/members/sign-up").with(csrf())
                .contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(request)));

        //then
        perform.andDo(print()).andExpect(status().isBadRequest());
        verify(memberService, never()).createMember(any());

     }
  • 403 Forbidden은 서버가 허용하지 않는 웹 페이지나 미디어를 사용자가 요청할 때 웹 서버가 반환하는 HTTP 상태 코드이다.
    @WithMockUser 애노테이션을 메소드 상단에 붙여주어 해결했다.
    스프링 시큐리티에서 제공하는 애노테이션으로 이 애노테이션을 붙여주면 인증이 필요한 작업에 인증을 대신 부여해서 진행할 수 있게 해준다.
  • 401 unauthorized
    with(csrf()) csrf 토큰을 넘겨줄 수 있게 한다.
profile
즐겁고 괴로운 개발😎

0개의 댓글