[캡스톤 디자인] Test 코드 작성하기

Dev_Sanizzang·2023년 7월 27일
0

캡스톤디자인

목록 보기
11/15

📕 개요

현재 캡스톤디자인 프로젝트는 단 1개의 Test 코드도 작성하지 않고 개발하고 있었다. 😂
왜냐하면 기한까지 일단 기능 구현을 빨리해야되서 테스트코드를 짤 여유가 없었을 뿐더러 아직까지는 복잡한 로직을 구현한 코드가 없었기 때문에 테스트코드에 대한 필요성을 느끼지 못하고 있었다.
하지만 이곳 저곳에서 테스트 코드 작성이 개발의 절반을 차지할 정도로 중요하다는 말을 하고 나는 테스트코드의 적용의 필요성을 느꼈다..
고로 이번 포스팅에서는 캡스톤 디자인 프로젝트에 테스트 코드를 적용해보는 글을 써보겠다.

테스트 코드 작성 참고 사이트
https://dingdingmin-back-end-developer.tistory.com/entry/Springboot-Test-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1-1

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

1. Test 코드를 작성하지 않고 결과를 검증하는 과정은 비용이 많이 든다.

  • Test 코드를 사용하지 않았을 때
  1. 검증 코드 작성
  2. 애플리케이션 실행
  3. PostMan 혹은 브라우저 Request 요청
  4. log 혹은 print로 결과 검증
  5. 원하지 않는 결과 발생 시 애플리케이션 종료
  6. 다시 코드 작성
  • Test 코드를 사용했을 때
  1. Test 코드 작성
  2. Test 코드 실행
  3. 결과 검증
  4. Test 코드 수정

위와 같이 Test 코드를 사용했을 때는 애플리케이션을 실행, 종료할 필요가 없어 비용이 줄어들고, Test 코드를 통해서 명확한 결과 검즈잉 가능하다.

2. Spring의 계층 구조

  • Controller: 클라이언트 요청을 받고 클라이언트에게 결과를 반환
  • Service: 비즈니스 로직을 실행하고 결과 반환
  • Repository: database에 쿼리를 이용해서 CRUD를 하는 계층
  • Domain: Entity 클래스

애플리케이션을 실행해서 Test를 진행한다면, 어느 계층에서 잘못된 코드가 있는지 파악하는데 많은 비용이 든다. 하지만 Test코드를 통해서 계층별로 Test를 진행한다면 어느 부분이 잘못된지 파악을 쉽게할 수 있다.

SpringBoot Test

Spring Initailizer를 통해서 프로젝트를 생성하면 spring-boot-starter-test dependency가 자동으로 추가된다. 우리는 이것을 이용해서 Test 코드를 작성하면 된다.

spring-boot-test-starter 구성요소

  1. spring-boot-test: 테스트에 필요한 핵심 기능 라이브러리
  2. spring-boot-test-autoconfigure: 테스트 진행 위한 Configuration 라이브러리

JUnit

  1. Java에서 독립된 단위 테스트를 지원해주는 프레임워크
  2. Assert(검증)을 이용해서 결과를 기댓값과 실제 값을 비교
  3. @Test 어노테이션마다 독립적으로 테스트가 진행

단위 테스트와 통합 테스트

  • 단위(Unit) 테스트: 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위 테스트 -> 쉽게 말하면 하나의 기능 혹은 메서드라고 이해하면 된다.
  • 통합(integration) 테스트: 모듈을 통합화하는 과정에서 모듈 간의 호환성을 확인하는 테스트 -> unit이 하나였다면 여러 개의 계층이 테스트에 참여한 것이라고 생각하면 쉬울 것이다.

단위 테스트의 장단점

장점

  • 새로운 기능에 대해서 빠르게 작성 가능하다
  • Test 코드 자체가 하나의 문서
  • 시간과 비용의 절감

단점

  • 독립적인 테스트이므로 다른 객체와 상호작용 처리를 위해서 가짜 객체의 정의가 필요하다
  • 가짜 객체의 답변 작성이 필요하다
  • 실제 운영 환경과 다른 답변을 내놓을 수 있는 가능성이 있다

통합 테스트의 장단점

장점

  • 실제 객체를 사용하므로 가짜 객체를 사용하지 않아 정의하지 않아도 된다
  • 실제 운영 환경과 같은 값을 도출 할 수 있다

단점

  • 테스트 하나의 많은 비용이 들어간다
  • 어느 계층에서 발생한 문제인지 파악하기 힘들다

단위, 통합 테스트 어떤걸 해야할까?

테스트의 종류와 적절한 선택은 개발 과정 및 애플리케이션의 특성에 따라 다르지만, 일반적으로 애플리케이션의 완전성을 보장하기 위해 단위 테스트와 통합 테스트 둘 다 수행하는 것이 가장 이상적이다. 이 두 종류의 테스트는 각각 다른 목적과 장단점을 가지고 있기 때문이다.

개별 코드 단위의 정확성을 보장하기 위해 단위 테스트를, 코드 간의 상호작용 및 전체 시스템의 동작을 검증하기 위해 통합 테스트를 수행하는 것이 좋다. 이 외에도 성능 테스트, 스트레스 테스트, 회귀 테스트, 스모크 테스트 등 여러 종류의 테스트가 있으며, 이들은 애플리케이션의 특성 및 요구 사항에 따라 선택하면 된다.

좋은 단위 테스트

  1. 1개의 테스트는 1개의 기능에 대해서만 테스트
  2. 테스트 주체와 협력자를 구분하기 (여기서 주체는 테스트를 할 객체이며, 협력자는 테스트를 진행하기 위해 정의하는 가짜 객체이다.)
  3. Given, when, then으로 명확하게 작성하기
  • Given: 테스트를 진행할 행위를 위한 사전 준비
  • when: 테스트를 진행할 행위
  • then: 테스트를 진행한 행위에 대한 결과 검증

👊 테스트 적용 해보기

Domain Test

  • @Test: 반드시 필요, 반환하는 것이 없도록 void여야 함
  • @DisplayName을 통해서 테스트 진행 시 나오는 테스트명을 정할 수 있음
class UsersTest {

    @Test
    @DisplayName("유저가 생성되는지 확인하는 테스트")
    void createUser() {
        // given
        Users users = Users.builder()
                .loginId("test1234")
                .encryptedPwd("encryptedPwd")
                .name("kim")
                .phoneNumber("010-1234-1234")
                .birthday(LocalDate.of(2023,07,27))
                .gender(MALE)
                .profileImage("imageUrl")
                .build();

        // when, then
        Assertions.assertThat(users.getName()).isEqualTo("kim");
        Assertions.assertThat(users.getPhoneNumber()).isEqualTo("010-1234-1234");
    }
}

Repository 테스트

  • @DataJpaTest: JPA를 사용하는 Repository에 대한 검증을 수행할 때 사용하는 어노테이션

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

  • @DataJpaTest로 검증할 수 있는 목록

    • DataSource에 대한 설정
    • CRUD 동작 확인
      @AutoConfigurationDatabase에 Replace.NONE 설정을 주면 실제 DB로 검증할 수 있다. 따로 명시하지 않을 시 내장된 임베디드 DB를 사용한다.
@DataJpaTest
class UserRepositoryTest {

    @Autowired
    UserRepository userRepository;
    
    @Test
    @DisplayName("User 생성")
    void createMember() {
        // given
        Users users = Users.builder()
                .loginId("test1234")
                .encryptedPwd("encryptedPwd")
                .name("kim")
                .phoneNumber("010-1234-1234")
                .birthday(LocalDate.of(2023,07,27))
                .gender(MALE)
                .profileImage("imageUrl")
                .build();
        
        // when
        Users result = userRepository.save(users);
        
        // then
        Assertions.assertThat(result.getName()).isEqualTo(users.getName());
    }
    
    @Test
    @DisplayName("LoginId로 User 불러오기")
    void findByLoginId() {
        // given
        Users users = Users.builder()
                .loginId("test1234")
                .encryptedPwd("encryptedPwd")
                .name("kim")
                .phoneNumber("010-1234-1234")
                .birthday(LocalDate.of(2023,07,27))
                .gender(MALE)
                .profileImage("imageUrl")
                .build();
        userRepository.save(users);
        
        // when
        Users result = userRepository.findByLoginId("test1234");
        
        // then
        Assertions.assertThat(result.getName()).isEqualTo(users.getName());
    }
}

Service 테스트

Service 계층 같은 경우 UserRepository, ClubServiceClient, FileStore 객체를 Spring에게 주입받고 있다.
따라서 Service 계층의 Test는 주체가 Service 객체이며, 협력자는 UserRepository, ClubServiceClient, FileStore 객체이다.
그렇기에 UserRepository, ClubServiceClient, FileStore는 가짜 객체로서 응답을 설정해줘야한다.

JUnit5 기능을 사용하고, Test에서 가짜 객체를 사용하기 때문에 @ExtendWith(SpringExtension.class)를 붙여줘야 한다.

@ExtendWith(SpringExtension.class)
class UserServiceTest {

    // Test 주체
    UserService userService;

    // Test 협력자
    @MockBean
    UserRepository userRepository;

    @MockBean
    ClubServiceClient clubServiceClient;

    @MockBean
    FileStore fileStore;

    // Test를 실행하기 전마다 UserService에 가짜 객체를 주입시켜준다.
    @BeforeEach
    void setUp() {
        userService = new UserServiceImpl(userRepository, clubServiceClient, fileStore);
    }

    @Test
    @DisplayName("user-id로 User 조회")
    void getUserById() {
        // given
        Users users = Users.builder()
                .loginId("test1234")
                .encryptedPwd("encryptedPwd")
                .name("kim")
                .phoneNumber("010-1234-1234")
                .birthday(LocalDate.of(2023,07,27))
                .gender(MALE)
                .profileImage("imageUrl")
                .build();
        Mockito.when(userRepository.findById(1L)).thenReturn(Optional.of(users));

        // when, then
        Assertions.assertThat(userService.getUserById(1L)).isInstanceOf(ResponseUserById.class);
    }
}
  • @BeforeEach: Test를 실행하기 전 항상 실행하도록 하는 어노테이션
  • @MockBean: 가짜 객체를 만드는 역할. 물론 가짜 객체이므로 응답을 정의해줘야한다. Test의 협력자 역할을 한다.
  • UsserService: Test의 주체로서 가짜 객체를 주입받고, 자신의 로직을 실행하고 결과를 가지고 검증을 한다.

Mockito
userRepository.findById(1L) 메서드가 호출되면 가짜 객체 응답으로 users를 반환하도록 설정할 수 있다.
즉, userRepository.findById(1L) 메서드를 호출할 때 실제 DB에 저장되는 대신 users 객체를 반환하도록 가짜로 설정한 것이다.

Mockito를 이용하여 userRepository의 동작을 가짜로 설정함으로써, 외부 종속성을 고려하지 않고 유저 조회 메서드만을 단독으로 테스트할 수 있다.

Controller 테스트

  • @WebMvcTest: Mvc를 위한 테스트로서 컨트롤러가 설계대로 동작하는지에 대해 검증하는데 필요한 어노테이션
    Controller를 구체적으로 적을 수 있고, ControllerAdvice, Filter 등을 포함과 제외시킬 수 있어 Security에 대한 Test도 가능하다.
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    MockMvc mvc;

    @MockBean
    UserServiceImpl userService;

    @Test
    @WithMockUser(username = "testUser", roles = "USER") // 가짜 사용자로 인증 시도
    @DisplayName("회원 정보 조회")
    void getUserById() throws Exception {
        // given
        Users users = Users.builder()
                .loginId("test1234")
                .encryptedPwd("encryptedPwd")
                .name("kim")
                .phoneNumber("010-1234-1234")
                .birthday(LocalDate.of(2023,07,27))
                .gender(MALE)
                .profileImage("imageUrl")
                .build();
        ReflectionTestUtils.setField(users,"id",1L);
        ResponseUserById response = ResponseUserById.builder().user(users).build();
        Mockito.when(userService.getUserById(1L)).thenReturn(response);

        // then
        mvc.perform(MockMvcRequestBuilders.get("/user/{user-id}", 1L))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(response.getId()))
                .andExpect(MockMvcResultMatchers.jsonPath("$.name").value(response.getName()));
    }
}
  • ReflectionTestUtils.setField() : test를 진행하면서 private로 선언된 필드 값을 넣어줄 수 있다.

  • @WithMockUser(username = "testUser", roles = "USER"): 현재 프로젝트는 Spring Security를 사용하고 있어 사용자 인증이 필요하다.(인증을 안할 시 401 에러가 뜬다.) @WithMockUser를 사용하여 테스트 메소드에 인증된 사용자 정보를 추가하면, 해당 사용자로 인증된 것처럼 테스트를 수행할 수 있다.

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

  • mvc.perform(MockMvcRequestBuilders.get(). contentType(): 컨트롤러에게 요청을 보내는 역할을 한다. uri를 만들고, contentType을 지정한다.

  • andDo(): 요청에 대한 처리를 한다. MockMvcResultHanlder.print()를 인자로 넣었으므로 요청과 응답에 대한 것들을 콘솔에 출력해준다.

  • andExpect(): 검증하는 로직. MockMvcResultMatcher.status()는 HTTP 상태 코드를 검증하고, jsonPath는 Json로 넘어온 것들에 대한 값을 검증할 수 있다.

🚪 마무리

이렇게 테스트 코드 작성의 이유와 간단한 테스트 코드 작성을 해봤다.
앞으로는 기능 구현을 할 때 테스트 코드를 작성할 수 있도록 해야겠다.

profile
기록을 통해 성장합니다.

0개의 댓글