본문 바로가기

Framework/Spring

[Spring] 로그인 필요한 API 단위 테스트 - 컴도리돌이

728x90
 

작심삼주 오블완 챌린지

오늘 블로그 완료! 21일 동안 매일 블로그에 글 쓰고 글력을 키워보세요.

www.tistory.com


이제는 숨을 쉬듯이 API 요청 로직을 만들지만, 단 한 번도 테스트 코드로 API 요청을 테스트해 본 적이 없었던 것 같아요. 초반에는 단위 테스트 작성이 시간이 오래 걸리고, 주요 로직보다 더 많은 작업이 될 것 같아서 외면했었죠. 🥲 그런데 로직을 수정할 때마다 프로젝트를 재가동시키며 테스트를 하니 오히려 더 많은 시간이 소요되는 것을 느꼈습니다. 뿐만 아니라 예기치 못한 문제들이 발생하거나, 기존 기능에 새로운 기능을 추가하면서 어떤 영향을 미칠지 파악하기 어려워졌습니다. 이 경험을 통해 이제 단위 테스트는 선택이 아닌 필수라는 생각이 들었습니다. 

 

그래서 이제부터 API 요청이나 어떤 로직을 만들 때 테스트 코드도 작성하기로 마음을 먹었습니다. 하지만 안 하던 일을 하려니 생각보다 쉽게 작성되지 않더군요. 😂 특히나 대부분의 API가 로그인 후에 접근이 가능하기 때문에, 테스트가 요청을 거절하는 경우가 많았습니다. 그렇다면, 사이트의 대부분 API가 로그인 후에만 접근 가능하다면, 테스트 코드 작성 시 매번 로그인 과정을 거쳐야 하는 걸까요? 🤔


로그인 후 API 테스트를 위한 인증 처리 

1. WithMockUser로 간편한 인증 설정

@WithMockUser는 간단한 인증 설정이 필요할 때 유용하게 사용할 수 있는 어노테이션입니다. 이 어노테이션을 사용하면, 별도의 복잡한 설정 없이도 테스트에서 가짜 사용자 정보를 쉽게 설정할 수 있습니다. 기본적으로 @WithMockUser는 사용자 이름을 user, 권한을 ROLE_USER로 설정하여 인증이 필요한 API 테스트를 지원하는데, 테스트 시 사용자 이름이나 역할을 커스터마이징 할 수도 있습니다. 예를 들어, 특정 이름의 사용자로서 테스트가 필요하거나, 특정 권한을 가진 사용자를 시뮬레이션해야 할 때 @WithMockUser(username = "testUser", roles = {"USER"})와 같이 사용자 이름을 "testUser"로 지정하고 권한을 "USER"로 설정할 수 있습니다.

import org.springframework.security.test.context.support.WithMockUser;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(TestController.class)
public class ControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithMockUser(username = "testUser", roles = {"USER"})
    public void testAuthenticatedEndpoint() throws Exception {
        mockMvc.perform(get("/api/protected-endpoint"))
               .andExpect(status().isOk())
               .andDo(print());
    }
}

 

이렇게 설정하면 MockMvc를 사용해 특정 API 엔드포인트에 대한 요청을 수행하고, 응답 상태나 출력 내용을 확인하면서 인증이 필요한 테스트를 진행할 수 있습니다. 이처럼 @WithMockUser는 로그인 과정 없이 인증된 사용자를 쉽게 설정할 수 있다는 장점이 있어, 간단한 인증이 필요한 테스트에서는 효율적으로 사용할 수 있습니다. 다만 @WithMockUser는 실제 데이터베이스나 서비스에서 로드된 사용자 정보를 사용하는 것은 아니기 때문에, 실제 사용자 정보와 일치해야 하는 복잡한 테스트가 필요한 경우에는 제한적일 수 있습니다. 이 경우에는 별도로 사용자 정보를 로드하는 @WithUserDetails 어노테이션을 사용하는 것이 적합합니다.

 

2. WithUserDetails로 실제 사용자 정보 불러오기

@WithUserDetails는 UserDetailsService를 통해 실제 사용자 정보를 불러와서 테스트에서 인증된 상태를 시뮬레이션할 수 있도록 돕습니다. 이 어노테이션은 @WithMockUser와 달리, 가짜 데이터를 사용하지 않고 데이터베이스에 저장된 사용자 정보를 기반으로 테스트를 수행하는 데 유용합니다. @WithUserDetails의 value 속성은 테스트 시 사용할 사용자 이름을 지정하고, userDetailsServiceBeanName 속성은 사용할 UserDetailsService 빈 이름을 지정할 때 활용할 수 있습니다.

import org.springframework.security.test.context.support.WithUserDetails;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(TestController.class)
public class ControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithUserDetails("realUser")
    public void testAuthenticatedEndpointWithRealUser() throws Exception {
        mockMvc.perform(get("/api/protected-endpoint"))
               .andExpect(status().isOk())
               .andDo(print());
    }
}

 

예를 들어, @WithUserDetails("realUser")와 같이 설정하면, "realUser"라는 이름의 사용자를 UserDetailsService에서 로드하여 실제 인증 과정을 수행할 수 있습니다. 이를 통해 데이터베이스에 존재하는 사용자 정보를 테스트에 활용할 수 있어, 실제 환경과 유사한 조건에서 API 엔드포인트를 테스트할 수 있게 됩니다.

 

3. 직접 SecurityContext 설정하기(추천)

테스트 환경에서 인증된 사용자 정보를 세밀하게 제어할 필요가 있을 때, 직접 SecurityContext에 사용자 정보를 설정하는 방법을 사용할 수 있습니다. 이 방법은 주로 복잡한 인증 시나리오나 다양한 권한을 가진 사용자 테스트가 필요할 때 유용하게 활용됩니다. 예를 들어, 인증된 사용자로 특정 API에 접근하는 로직을 테스트하려면, 매번 실제 로그인 과정을 거칠 필요 없이 미리 설정된 사용자 정보를 활용할 수 있습니다. 이렇게 하면 로그인 과정을 건너뛰고 테스트를 더 효율적으로 진행할 수 있습니다.

 

SecurityContext는 스프링 시큐리티에서 현재 인증된 사용자 정보를 저장하는 장소입니다. 이를 통해 애플리케이션은 현재 사용자의 인증 상태와 권한 정보를 조회할 수 있습니다. 따라서 테스트 중 SecurityContext를 설정하여 인증된 사용자를 지정해 주면, 실제로 로그인한 것처럼 인증된 상태에서 테스트를 수행할 수 있습니다.

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(TestController.class)
public class ControllerTest {

	@Autowired
    private MockMvc mockMvc;

    @BeforeEach
    public void setupSecurityContext() {
        User mockUser = new User("testUser", "password", List.of(new SimpleGrantedAuthority("ROLE_USER")));
        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(mockUser, null, mockUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(auth);
    }

    @Test
    public void testAuthenticatedEndpoint() throws Exception {
        mockMvc.perform(get("/api/protected-endpoint"))
               .andExpect(status().isOk())
               .andDo(print());
    }
}

 

위 코드는 @BeforeEach 메서드에서 SecurityContext에 인증된 사용자를 설정한 후, mockMvc를 사용해 보호된 API (/api/protected-endpoint)에 요청을 보내는 테스트입니다. 이 테스트는 인증된 사용자만 접근할 수 있는 엔드포인트에 대해 정상적인 응답을 받는지 확인하는 데 사용됩니다.

SecurityContext를 직접 설정하는 방법은 다양한 사용자 인증 시나리오를 테스트할 수 있는 매우 강력한 도구입니다. @WithMockUser나 @WithUserDetails로 해결할 수 없는 복잡한 인증 테스트가 필요할 때, 이 방법을 통해 테스트 효율을 극대화하고, 인증 로직을 정확하게 검증할 수 있습니다. 💡

 

단위 테스트를 위해 인증 설정을 구성하는 여러 방법들을 살펴보았습니다. @WithMockUser와 @WithUserDetails는 로그인 후 접근이 필요한 API 테스트에서 유용하며, 직접 SecurityContext 설정은 복잡한 인증 시나리오에도 효과적입니다. 이런 설정을 통해 테스트가 더욱 빠르고 효율적으로 실행될 수 있으며, 코드를 수정해도 기존 기능의 안전성을 빠르게 확인할 수 있습니다. 🙌