활동정리/모각코

2024 하계 모각코 4회차

잔디🌿 2024. 7. 20. 00:58

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

 

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

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

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

 

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

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

@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