본문 바로가기
활동정리/모각코

2024 하계 모각코 4회차

by 잔디🌿 2024. 7. 20.

    목표 : 테스트코드 작성법을 공부하고 정리한다.

     

    카카오테크캠퍼스를 하다보니까 테스트코드의 중요성을 알게 되었다.

    근데 나는 남이 짜준 테스트코드를 돌리는 것 이외에는 딱히 해본 적이 없어서 이 부분을 구현하는데 힘들었다.

    그리고 테스트코드를 짜는 방법을 확실하게 몰라서 더 부담을 느끼는 것 같아서 이번에 쭉 정리를 해보려고 한다.

     

    테스트코드를 작성하는 이유

    • 디버깅 비용 절감
    • 코드변경에 대한 불안감 해소
    • 더 나은 자료
    • 좋은 코드는 테스트하기 쉽다.
    • 테스트 자동화

    @SpringBootTest : 테스트에 필요한 핵심 기능 라이브러리

    @Junit : java에서 독립된 단위 테스트를 지원해주는 프레임워크

     

    @Test 어노테이션마다 독립적으로 테스트가 진행된다.

     

    단위테스트 VS 통합테스트

     

    단위테스트는 시간과 비용면에서 좋고, test코드 자체가 하나의 문서가 되지만, 가짜객체를 사용해야하고, 실제 운영환경과 다른 답변을 내놓을 수 있다.

     

    통합테스트는 실제 객체를 사용하므로 가짜객체를 사용하지 않아도 된다. 하지만, 테스트 하나에 많은 비용이 들어가고, 문제가 생기면 어떤 계층에서 생긴건지 파악하기 어렵다.

     

    단위테스트는 Given, When, Then으로 명확하게 작성해야한다.

    • given : 테스트를 진행할 행위를 위한 사전 준비
    • when : 테스트를 진행할 행위
    • then : 테스트를 진행한 행위에 대한 검증

     

    AssertJ 라이브러리

     

    assertThat는 값 검증에 쓰인다.

    assertThat(실제값).isEqualTo(기댓값)

    assertThat(실제 객체).isInstanceOf(객체 예상 타입)

    assertThat(실제값).isNull()

     

    assertThatThrownBy는 예외 검증에 쓰인다.

    예외가 발생하면 테스트가 통과하고, 발생하지 않으면 테스트가 실패한다.

     

    도메인 테스트

     

    @Test
    @DisplayName("멤버가 생성되는지 확인하는 테스트")
    void createMember(){
        /*
        given
         */
        Member member = Member.builder().age(10).name("hi").build();
     
        /*
        when, then
         */
        Assertions.assertThat(member.getAge()).isEqualTo(10);
        Assertions.assertThat(member.getName()).isEqualTo("hi");
    }

     

    위 코드는 member이라는 도메인을 테스트하는 코드이다.

    @Test어노테이션을 사용해야하고, @DisplayName을 통해서 테스트 시 나오는 테스트명을 정할 수 있다.

     

    @Test
    @DisplayName("멤버의 나이 바뀌는지 확인하는 테스트")
    void changeAgeTest(){
        /*
        given
         */
        Member member = Member.builder().age(10).name("hi").build();
     
        /*
        when
         */
        member.changeAge(13);
        /*
        then
         */
        assertThat(member.getAge()).isEqualTo(12);
    }

    이렇게 도메인 내에 특정 메서드가 있으면 이에 대한 테스트도 진행할 수 있다.

     

    JPA테스트

     

    @DataJpaTest : JPA를 사용하는 레파지토리를 테스트할 때 사용되는 어노테이션이다.

    @DataJpaTest는 @Transaction을 포함하고 있어서 1개의 테스트가 끝나면 Rollback해 다른 테스트에게 영향을 미치지 않는다

     

    @DataJpaTest
    public class MemberRepositoryTest {
     
        @Autowired
        MemberRepository memberRepository;
     
        @Test
        @DisplayName("멤버 만들기")
        void createMember(){
            /*
            given
             */
            Member member1 = Member.builder().name("hi1").age(10).build();
            Member member2 = Member.builder().name("hi2").age(20).build();
     
            /*
            when
             */
            Member result1 = memberRepository.save(member1);
            Member result2 = memberRepository.save(member2);
     
            /*
            then
             */
            assertThat(result1.getAge()).isEqualTo(member1.getAge());
            assertThat(result2.getAge()).isEqualTo(member2.getAge());
     
        }
     
        @Test
        @DisplayName("멤버의 리스트를 반환 하는지 확인")
        void MemberList(){
            /*
            given
             */
            Member member1 = Member.builder().name("hi1").age(10).build();
            Member member2 = Member.builder().name("hi2").age(20).build();
            memberRepository.save(member1);
            memberRepository.save(member2);
     
            /*
            when
             */
            List<Member> result = memberRepository.findAll();
     
            /*
            then
             */
            assertThat(result.size()).isEqualTo(2);
        }
        
    }

    위 코드는 memberRepository에 값이 잘 들어가는지, 원하는 값들이 잘 return되는지를 테스트한다.

    객체 주입은 필드로 주입하는 방식을 사용한다.

     

    Service Test

     

    Service 계층은 Repository객체를 Spring에게 주입받는다.

    따라서 Repository는 가짜 객체로서 응답을 설정해야한다.

     

    Junit5기능을 사용하고 가짜 객체를 사용하기 떄문에 @ExtendWith(SpringExtention.class)를 붙여줘야 한다.

     

    @ExtendWith(SpringExtension.class)
    public class MemberServiceTest {
     
        // Test 주체
        MemberService memberService;
     
        // Test 협력자
        @MockBean
        MemberRepository memberRepository;
     
     
        // Test를 실행하기 전마다 MemberService에 가짜 객체를 주입시켜준다. 
        @BeforeEach
        void setUp(){
            memberService = new MemberServiceImpl(memberRepository);
        }
    }

    @BeforeEach는 Test를 실행하기 전 항상 실행하도록 하는 어노테이션이다. 여기서는 가짜객체를 주입하는 데 사용된다.

    @MockBean은 가짜 객체를 만드는 역할은 한다. Test의 협력자이다.

    MemberService : Test의 주체로서 가짜 객체를 주입받고, 자신의 로직을 실행하고 결과를 가지고 검증한다.

     

    Mock이란

    실제 객체를 만들기엔 비용과 시간이 많이 들거나 의존성이 길게 걸쳐져 있어서 제대로 구현하기 어려울 때 가짜 객체를 만들어 사용하는데 이게 mock이다.

     

    @Test
    @DisplayName("멤버 생성 성공")
    void createMemberSuccess(){
        /*
        given
         */
        Member member3 = Member.builder().name("hi3").age(10).build();
        ReflectionTestUtils.setField(member3,"id",3l);
     
        Mockito.when(memberRepository.save(member3)).thenReturn(member3); // 가짜 객체 응답 정의
        /*
        when
         */
        Long hi3 = memberService.createMember("hi3", 10);
        /*
        then
         */
        assertThat(hi3).isEqualTo(3L);
    }

     

    위 코드는 save 기능을 테스트한다.

    우선 member3객체를 만드는데, ReflextionTestUtils에서 private로 된 필드의 값을 설정한다.

    Repository에다가 member3객체를 넣으면 member3을 리턴하도록 시킨다.

    그 다음 Service에 createMemeber 객체들을 넣고 반환값의 아이디가 3L인지 확인한다.

    이게 가능한 이유는 member 도메인에다가 eqauls를 오버라이딩함으로서 같은 이름, 나이를 가진 객체는 동일시 했기 떄문에다.

     

    ControllerTest

     

    @WebMvcTest(MemberController.class)
    public class MemberControllerTest {
     
        @Autowired
        MockMvc mvc;
     
        @MockBean
        MemberServiceImpl memberService;
     
    }

     

    Test의 주체는 MemberController이므로 이를 @WebMvcTest에 선언을 해야한다.

    협력자인 MEmberService는 협력자니까 @MocBean을 등록해주고, Test에 응답을 정의한다

    MockMvc는 실제로 서블릿 컨테이너를 사용하지 않고, 테스트용으로 Mvc기능을 사용할 수 있게 해주는 역할을 한다.

    테스트 때 생성되는 WebApplicationContext에서 주입받는다.

     

    @Test
    @DisplayName("리스트 반환받기")
    void getList() throws Exception {
        /*
        given
         */
        List<MemberResponseDto.ListDto> list = List.of(new MemberResponseDto.ListDto("asd", 10)
                , new MemberResponseDto.ListDto("fsd", 12));
        Mockito.when(memberService.findAll()).thenReturn(list);
     
        /*
        when then
         */
        mvc.perform(MockMvcRequestBuilders.get("/members").contentType(MediaType.APPLICATION_JSON))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$[1].name").value("fsd"))
                .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("asd"));
    }

    serviceTest할 때와 같이 리스트를 만들고, mockito로 이를 응답하도록 설정한다.

     

    그 다음 mvc.perform으로 테스트하는데, mvc.perform(MockMvcRequestBuilders.get().contentType()에서는 컨트롤러에게 요청을 보내는데, uri를 만들고, contentType를 지정한다.

     

    andDo는 요청에 대한 처리를 한다. 여기서는 응답값을 콘솔에 출력한다.

     

    andExpect : 검증하는 로직이다.

     

    MockMvcResultMatcher.status() HTTP 상태 코드를 검증하고, jsonPath Json 넘어온 것들에 대한 값을 검증한다.

     

    jsonPath("$. name"). value("fsd")는  단일 객체에 대한 값 검증이고, jsonPath("$[1]. name"). value("asd): 리스트를 반환받았을 때 지정하여 검증이다.

     

    통합테스트

     

    통합테스트는 전체적인 Spring에 쓰이는 Bean들이 등록된다. 

    @SpringBootTest는 통합 테스트를 진행하기 위한 어노테이션이다.

    이 때 주의해야 할 점은 @Transaction을 포함하지 않고 있기 때문에 Repository도 사용하고 있다면 @Transaction도 붙여서 RollBack를 실행해야한다.

     

    public class MemberServiceTest {
        @Autowired
        MemberService memberService;
     
        @Autowired
        MemberRepository memberRepository;
    }
    @Test
    @DisplayName("멤버 만들기")
    void createMemberSuccess(){
        Long memberId = memberService.createMember("hi1", 10);
        assertThat(memberId).isEqualTo(1l);
    }
     
    @Test
    @DisplayName("이름 중복으로 만들기 실패")
    void createMemberFail(){
        Long memberId = memberService.createMember("hi1", 10);
        assertThatThrownBy(() -> memberService.createMember("hi1",12)).isInstanceOf(IllegalStateException.class);
    }

     

    이렇게 단위테스트보다 간단한 것을 볼 수 있다.

     

    소감 : 공부해보니까 내가 생각한만큼 그렇게 어렵지 않아서 이제까지 만든 프로젝트에 한번 적용해봐야겠다는 생각이 들었다.

     

    https://tech.inflab.com/20230404-test-code/

     

    테스트 코드를 왜 그리고 어떻게 작성해야 할까?

    테스트 코드가 필요한 이유와 잘 작성하는 방법에 대해 공유합니다.

    tech.inflab.com

    https://dingdingmin-back-end-developer.tistory.com/entry/Springboot-Test-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1-1

     

    Springboot Test 코드 작성

    Test 코드를 작성하는 법을 알아보기 전에 Test 코드의 필요성에 대해서 알아보겠습니다. 1. 왜 Test 코드를 작성하는가? 크게 2가지 이유가 있습니다.' 1-1. Test 코드를 작성하지 않고 결과를 검증하

    dingdingmin-back-end-developer.tistory.com

    https://velog.io/@sproutt/MockBean%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%9C-%ED%86%B5%ED%95%A9Controller%ED%85%8C%EC%8A%A4%ED%8A%B8

     

    @MockBean을 사용한 통합(Controller)테스트 - 배종진

    @MockBean을 사용한 통합(Controller)테스트

    velog.io

     

    '활동정리 > 모각코' 카테고리의 다른 글

    2024 하계 모각코 6회차  (0) 2024.07.28
    2024 하계 모각코 5회차  (0) 2024.07.23
    2024 하계 모각코 3회차  (1) 2024.07.16
    2024 하계 모각코 2회차  (2) 2024.07.13
    2024 하계 모각코 1회차  (0) 2024.07.13