Project/Afternote

[트러블 슈팅] Test 환경 구축

gyuun365 2026. 3. 22. 16:04

일단 afternote 프로젝트를 처음 만들 때, docker를 써보고 싶어서 여러 컨테이너를 다루는 docker-compose 환경을 구축했었다. 일단 그래서 build 속도가 다른 프로젝트들보단 좀 느렸다.  또한 프론트 분들에게 postman을 쓰게 하는 것보다 swagger 환경이 좀 더 편할 것 같아서 test도 수동으로 하나하나 swagger를 했었는데, 백엔드 중 1명이 mock과 junit을 써서 api controller하나를 test하는 것이 보였다. 너무 편해보여서 꼭 리팩토링 시에 넣어야지라고 생각했다. 

test 환경 구축에는 "테스트를 실행하고 관리하는 프레임워크인 JUnit 5" 와 "가짜 객체 mock을 만드는 Mockito" 를 사용했다.

 

Controller Test

일단 하나만 가져왔지만 이 메서드들을 관리하는 class 의 맨 위에

"@ExtendWith(MockitoExtension.class)"

를 사용해서 이 테스트 클래스에서 Mockito를 사용할 거라고 말을 해주고

@Mock
    private AuthService authService;

auth controller를 검사할 클래스이므로 authService라는 가짜 객체를 만들어줘 라는 의미이다.

 @InjectMocks
    private AuthController authController;

그래서 저렇게 만든 @Mock들을 바로 @InjectMocks 가 붙은 이 클래스에 꽂는 단계이다. controller가 진짜 서비스 대신 가짜 서비스를 바라보게 만드는 단계라고 생각하면된다. 

@Test
    @DisplayName("회원가입 API 성공")
    void signUp_Success() throws Exception {
        User user = org.mockito.Mockito.mock(User.class);
        given(user.getId()).willReturn(1L);
        given(user.getEmail()).willReturn("test@test.com");
        given(authService.signup(any())).willReturn(user);

        String requestBody = """
                {
                  "email": "test@test.com",
                  "password": "Password1!",
                  "name": "tester"
                }
                """;

        mockMvc.perform(post("/auth/sign-up")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(requestBody))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value(200))
                .andExpect(jsonPath("$.data.email").value("test@test.com"));

        verify(authService).signup(any());
    }

@Test

  • 의미: "이 메서드는 독립적인 테스트 케이스야."
  • 역할: 실행 버튼을 눌렀을 때 이 어노테이션이 붙은 메서드들만 각각 실행된다.

@DisplayName("...")

  • 의미: "테스트 결과창에 메서드 이름 대신 이 이름을 보여줘."
  • 역할: 개발자가 테스트 결과를 한눈에 읽기 편하게 가독성을 높여준다. (예: signUp_Success 대신 "회원가입 API 성공")

 

  • User user = mock(User.class);: 실제 User 객체를 만드는 대신, Mockito가 만든 가짜 유저(Mock)를 만드는 단계
  • given(user.getId()).willReturn(1L);: 누군가 가짜 유저에게 "너 ID가 뭐니?"라고 물으면 무조건 1L이라고 대답하도록 세팅.
  • given(authService.signup(any())).willReturn(user);: "어떤 값이 들어오든(any()), 서비스의 signup 메서드가 실행되면 방금 만든 가짜 유저를 결과로 돌려줘"라고 정해두는 것이다.

requestBody 부분은 JSON 본문을 그대로 흉내 낸 문자열이다. """을 꼭 써야한다는 것만 기억하면 될 것 같다.

 

mockMvc.perform(post("/auth/sign-up")         // (1) 주소와 방식 결정
                .contentType(MediaType.APPLICATION_JSON) // (2) 데이터 형식 지정
                .content(requestBody))      // (3) 실제 데이터 내용물

 

  • (1) post("/auth/sign-up"): /auth/sign-up이라는 주소와 post 라는 방식을 지정해 줘야함
  • (2) contentType(...): 데이터 형식을 지정해줘야함. 이게 없으면 서버가 데이터를 읽지 못할 수 있음.
  • (3) content(requestBody): 실제 전송할 JSON 데이터({"email":...})를 넣으면 됨

 

.andExpect(status().isOk())         // (4) 상태 코드 확인
        .andExpect(jsonPath("$.status").value(200)) // (5) 응답 JSON 내부 확인
        .andExpect(jsonPath("$.data.email").value("test@test.com")); // (6) 상세 데이터 확인

 

  • (4) status().isOk(): 서버 응답 코드가 200 OK인지 확인한다. (404나 500이 뜨면 여기서 테스트 실패!)
  • (5, 6) jsonPath(...): 응답으로 온 JSON 데이터의 특정 위치를 파고들어서 값을 확인합니다.
    • $는 JSON 전체를 의미합니다.
    • $.data.email은 { "data": { "email": "..." } } 구조에서 이메일을 찾아내라는 뜻.

이런식으로 controller test를 할 수 있다. 1~2개만 직접 작성해보고 나머지는 AI를 이용해서 노가다를 안하는 방법을 추천한다.

 

Service Unit Test

사실 controller test는 그 안의 서비스 로직들이 잘 되고 있는지는 검증하지 못한다. 그래서 service 단위로도 test를 해야하는데 

예시로는 컨트롤러 없이 AuthService만 따로 떼어내서 테스트 하는 것이다.

   @Test
    @DisplayName("회원가입 성공")
    void signup_Success() {
        SignupRequest request = org.mockito.Mockito.mock(SignupRequest.class);
        given(request.getEmail()).willReturn("test@test.com");
        given(request.getPassword()).willReturn("Password1!");
        given(request.getName()).willReturn("tester");

        given(userRepository.existsByEmail("test@test.com")).willReturn(false);
        given(passwordEncoder.encode("Password1!")).willReturn("encoded");
        given(userRepository.save(any(User.class))).willAnswer(invocation -> {
            User u = invocation.getArgument(0);
            ReflectionTestUtils.setField(u, "id", 1L);
            return u;
        });

        User user = authService.signup(request);

        assertThat(user.getId()).isEqualTo(1L);
        assertThat(user.getEmail()).isEqualTo("test@test.com");
        verify(tokenService, org.mockito.Mockito.never()).saveToken(any(), any());
    }

회원가입 시 서비스가 수행해야 할 비즈니스 규칙을 하나하나 확인하는 코드이다.

  1. 중복 체크: 이메일이 이미 존재하는지 확인했는가? (userRepository.existsByEmail)
  2. 보안: 비밀번호를 생짜(Raw)로 저장하지 않고 암호화했는가? (passwordEncoder.encode)
  3. 저장: 최종적으로 DB에 유저 정보를 저장했는가? (userRepository.save)
  4. 확인: 저장된 유저의 ID가 제대로 부여되었는가? (assertThat)
given(userRepository.save(any(User.class))).willAnswer(invocation -> {
    User u = invocation.getArgument(0);
    ReflectionTestUtils.setField(u, "id", 1L); // 가짜 ID 1번 부여
    return u;
});

실제 DB는 저장 시점에 자동으로 ID를 생성하지만 가짜 DB는 그런 기능이 없다.

그래서 저장 메서드가 호출되면, 들어온 유저 객체에 강제로 ID 1번을 박아서 돌려주는 코드이다.

 

이제 authService.signup을 통해 가짜 객체들로 실제로 실행을 하면!

assertThat(user.getId()).isEqualTo(1L);  // ID가 1인가?
assertThat(user.getEmail()).isEqualTo("test@test.com"); // 이메일이 일치하는가?
verify(tokenService, never()).saveToken(any(), any()); // 토큰 저장은 안 일어났지?

 

  • assertThat: 최종 결과물이 내가 원한 모양인지 확인함
  • verify(..., never()): 아직 회원가입이므로 토큰 저장이 안 일어난 것을 재확인하는 단계이다.
 verify(tokenService).saveToken("refresh", 10L);

 

그래서 로그인 로직에는 이런 식으로 실제로 저장되어 있는 것을 확인하곤 한다.