TIL - #21 테스트 코드의 작성

Quann·2023년 1월 12일
0

00. 개요

CRUD 프로젝트를 진행하면서, 테스트 코드를 작성하는 것도 목표로 두었다.

테스트 코드란, 말 그대로 내가 작성한 코드를 테스트할 수 있는 코드이다.

왜 Postman, Log 같은 기능을 사용하지 않고, 테스트 코드를 작성하지 않는지 궁금하기도 하고,

직접 테스트 코드를 작성하면서 어떻게 코드가 작성되는지도 알고자했다.


01. 테스트 코드

01.01. 테스트 코드란 무엇일까?

내가 작성한 코드가 의도한 대로 정상적으로 정상하는지 확인하기 위한 코드이다!
즉, 코드를 위한 코드인 것이다.

테스트 코드는 작동 시, Pass or Fail로 구분되며
내가 원하는 결과값이 도출되는 경우 Pass을 확인할 수 있고,
정상 예측값이 발생하지 않는 경우 Fail로 예측 범위를 벗어났음을 알 수 있다.

01.02 테스트 코드 작성의 이유?

  1. 개발 과정 중에 발생할 수 있는 예상 못한 문제를 발견 가능하다.
  2. 작성 코드의 의도성을 검증할 수 있다.
  3. 코드의 영향도를 판단할 수 있다. (하나의 코드를 변경했을 때, 다른 코드에 영향을 미치는 정도)
  4. 코드가 변경되었을 때, 계속해서 의도한 동작이 작동하는지 확인할 수 있다.
  5. 코드의 수정이 필요할 경우, 유연하고 안정적 대응이 가능하다.
  6. 코드 변경에 대한 사이드 이펙트를 줄일 수 있다.

01.03 테스트 코드의 종류

테스트의 대상 범위나 성격에 따라 UI Test / Integration Test / Unit Test 등 크게 세 가지로 나눌 수 있다.

간단하게,
UI Test는 완성된 제품에 대한 사용자 관점 테스트이다.
실제 사용 환경과 유사한 상태에서 진행되는 대형 테스트로, 서비스의 전체적 흐름을 테스트할 수 있다.

Integration Test는 중형 테스트에 속하는 테스트로서,
프로젝트 내 클래스나 모듈가느이 상호작용 유용성을 테스트한다.
단위 테스트가 정상 동작 하여도, 상호간 작용하는 플로우는 어긋날 수 있으므로 유닛 테스트보다 넓은 범주에서 테스트를 진행하는 것이다.

Unit Test는 소형 테스트에 속한다.
함수나 하나의 클래스 같은 단위에 대한 유효성을 검증하는 테스트이다.
코드가 간결하고, 빠르게 실행되며, 작성된 단위 테스트가 많아질수록 로직에 대한 신뢰도가 높아진다.
또한, 단위 테스트는 해당 단위의 로직이 어떤 역할을 수행하는지 쉽게 파악 가능하다.


02. 코드 작성

UI 테스트의 경우 사용자에게 보여줄 프론트 부분이 없기 때문에 나중에 생각하기로 했고,
단위 테스트를 먼저 진행하고자 했다.

AuthControllerTest.java


@ExtendWith(MockitoExtension.class)
class AuthControllerTest {

    @InjectMocks
    AuthController authController;

    @Mock
    AuthService authService;

    MockMvc mockMvc;

    ObjectMapper om = new ObjectMapper();

    @BeforeEach
    void before() {
        mockMvc = MockMvcBuilders.standaloneSetup(authController).build();
    }

    @DisplayName("1. 정상 회원가입 테스트")
    @Test
    void test_1() throws Exception {

        //given
        SignupRequest req = new SignupRequest("tempid", "tempPw", "닉네임", "이름", "email@naver.com", 30);

        //when, then
        mockMvc.perform(
                        MockMvcRequestBuilders
                                .post("/api/auth/sign-up")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(om.writeValueAsString(req)))
                .andExpect(
                        MockMvcResultMatchers
                                .status().isCreated());

        Mockito.verify(authService).signup(req);
    }

    @DisplayName("2. 정상 로그인 테스트")
    @Test
    void test_2() throws Exception {
        // given
        LoginRequest req = new LoginRequest("tempid", "temppw");

        when(authService.login(eq(req), any()))
                .thenReturn(new TokenResponse("access", "refresh"));

        // when, then
        mockMvc.perform(
                        post("/api/auth/sign-in")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(om.writeValueAsString(req)))
                .andExpect(
                        status().isOk())
                .andExpect(
                        MockMvcResultMatchers
                                .jsonPath("$.accessToken")
                                .value("access"))
                .andExpect(
                        MockMvcResultMatchers
                                .jsonPath("$.refreshToken")
                                .value("refresh")
                );

        verify(authService).login(eq(req), any());
    }

    @DisplayName("3. 정상 Re-Issue 테스트")
    @Test
    void test_3() throws Exception {
    	//given
        TokenRequest req = new TokenRequest("access", "refresh");

        when(authService.reissue(req, any()))
                .thenReturn(new TokenResponse("access", "refresh"));

		//when, then
        mockMvc.perform(
                        post("/api/auth/re-issue")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(om.writeValueAsString(req))
                ).andExpect(MockMvcResultMatchers
                        .jsonPath("$.accessToken")
                        .value("access"))
                .andExpect(MockMvcResultMatchers
                        .jsonPath("$.refreshToken")
                        .value("refresh"));

        verify(authService).reissue(eq(req), any());
    }
}

클래스 단위 테스트이다보니, 다른 클래스와의 상호관계는 Mock 객체를 사용해 해결하였다.

@InjectMocks 애너테이션을 통해 테스트를 진행할 클래스 파일을 선언해주고,
해당 클래스에서 필요로 하는 상호관계를 맺고 있는 객체를 @Mock 애너테이션을 통해 가짜 객체를 불러온다.

컨트롤러단 기능테스트를 위한 테스트 코드중 일부를 가져왔다.

테스트 코드 작성에서 자주 사용하는 방식인 given, when, then 으로의 구분을 통해
테스트의 실행 단계를 나눌 수 있다.

02.01 given

테스트에 필요한 요소들을 미리 세팅해둔다. 필요한 객체를 미리 생성해두거나, mock 객체가 어떤 값을 뱉어주는지에 대한 로직 등을 작성한다.
여기서는, AuthService에 대해 가상의 실행값과 리턴값을 넣어주어, 동작 방식을 선언해주게 된다.
따라서, authController.reissue() 가 호출하는 authService.reissue() 메서드에 대해 어떤 값을 넣어주면, 어떤 값을 리턴해줄 것인지 선언해준다.

3번째 테스트를 확인해보면,
authController.reissue()의 두번째 아규먼트로 HttpServletResponse가 들어가는데, 해당 객체는 실제 애플리케이션 구동시마다 다른 response가 들어가므로 any()로 선언해주었다.

첫 번째 파라미터로 eq(req)를 쓴 이유는, 그냥 객체인 req를 넣어버릴 경우

org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
-> at study.boardProject.auth.controller.AuthControllerTest.test_3(AuthControllerTest.java:102)


This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
    //correct:
    someMethod(any(), eq("String by matcher"));

다음과 같은 오류가 뜨며, 객체 내 아규먼트의 값을 통일해달라는 InvalidUseOfMatchersException이 발생한다.

따라서, any() 값을 쓰기 위해 객체의 값도 Mockito에서 제공하는 eq() 메서드를 통해 인자값의 형태를 맞춰주어야 한다.

정리하자면, when 절과 then절에서 실제 메서드 테스트시 던져줄 객체나 흐름에 대한 설정을 해두는 단계이다.

02.02 when

어떤 것을 실행 했을 때에 대한 검증을 하고싶은지에 관해 작성하는 부분이다.
해당 코드에서는 when 절과 then 절이 붙어있는데, 코드 순서상으로는 구분이 가능하다.

mockMvc.perform 부분을 when 절로, 이후 로직을 then 절로 구분이 가능하다.

perform 부분을 확인하면, 어떤 http method로, 어떤 객체를 body에 실어서 보내줄 것인지에 대한 코드가 작성된 것을 확인할 수 있다.

즉, 어떤 로직에 대한 검증을 진행할 것인지에 대한 부분이다.

02.03 then

then 절에서는 실행 결과에 대한 검증이 이루어진다.
mockMvc를 통해 실제 request를 날리게 되고, 결과를 검증할 수 있다.
HTTP Method, body값, 다양한 attribute에 대한 값을 설정해줄 수 있으며, 이를 바탕으로, given과 when 절에서 설정한 로직이 수행된다.

.andExpect() 절을 통해 값들에 대한 검증을 진행할 수 있다.
현재 메서드에서는 response로 내려준 body 값에 json이 정상적으로 출력되는지 확인하고 있다.
이 외에도, HttpStatus, 값 비교 등이 가능하다.

마지막으로 Mockito.verify() 를 통해 해당 메서드가 테스트 중에 실행되는지 검증한다. 테스트 중에 메서드가 실행되는지 검증할 수 있게 된다,


03. 결론

테스트 코드를 작성하다보면, 이걸 왜 작성해야하나 싶다.
그럴 때 마다, 앞서 서술한 테스트 코드의 장점을 읽어보면 된다.

내 코드를 100% 신뢰할 수 있는지?
이 코드를 썼던 나의 기억을 100% 기억할 수 있는지?
내 코드가 변경되지 않을 것인지?
변경 되었을 때 그 영향력은 어느정도인지?

테스트 코드가 그 문제를 해결해주는 것이다.
변경점이 생기면, 테스트 코드를 돌리면서 그 영향력을 판단하고, 로직이 정상적으로 동작하는지에 대한 검증을 할 수 있게 되는 것이다.

번거롭지만, 코드의 안정성을 위해 꼭 필요한 단계이다.


03. 오늘의 한 문단

코드를 위한 코드

profile
코드 중심보다는 느낀점과 생각, 흐름, 가치관을 중심으로 업로드합니다!

0개의 댓글