# Table of Contents

# Spring Security 테스트

Spring Security를 테스트하는 방법에 대해 알아본다.

# 의존성 추가

스프링 시큐리티와 관련된 의존성은 다음과 같다.

// build.gradle
dependencies {
    testImplementation 'org.springframework.security:spring-security-test'
}

# @WithMockUser

@WithMockUser를 사용하면 인증된 가짜 사용자를 만들 수 있다. 별도의 설정이 없다면 username = "user", password = "password", role = "USER"로 설정된다.

import org.springframework.security.test.context.support.WithMockUser;

@WebMvcTest
class TestControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    @WithMockUser(roles = "USER")
    public void test() throws Exception {
        mvc.perform(get("/test/test1"))
                .andExpect(content().string("test1"));
    }
}

다음과 같이 username, password, role를 직접 설정할 수 있다.








 





@WebMvcTest
class TestControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    @WithMockUser(username = "paul", password = "1234", roles = "USER")
    public void test() throws Exception {
        // ...
    }
}

다음과 같이 클래스 레벨에 추가할 수 있다.


 




@WebMvcTest
@WithMockUser(username = "paul", password = "1234", roles = "USER")
class TestControllerTest {
    // ...
}

# @WithAnonymousUser

@WithAnonymousUser를 사용하면 인증되지 않는 사용자를 테스트할 수 있다.


 




@WebMvcTest
@WithAnonymousUser
class TestControllerTest {
    // ...
}

# @WithUserDetails

대부분 UserDetailsService인터페이스의 loadUserByUsername(String username)를 구현하여 인증 정보를 가져온다.

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepositoryImpl userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userRepository.findByEmail(username)
                .orElseThrow(() -> new UsernameNotFoundException("Username Not Found."));

        return UserInfo("paul@gmail.com", "Jason Paul", "USER");
    }
}

@WithUserDetails을 사용하면 테스트 환경에서 UserDetailsService.loadUserByUsername(String username)가 반환할 사용자 정보를 직접 설정할 수 있다.

import org.springframework.security.test.context.support.WithUserDetails;

@WebMvcTest
class TestControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    @WithUserDetails("paul@gmail.com")
    public void test() throws Exception {
        mvc.perform(get("/test/test1"))
                .andExpect(content().string("test1"));

    }
}

# SecurityContext에 직접 Authentication 주입하기

SecurityContext에 직접 Authentication을 주입할 수도 있다.

class Test {

    @BeforeEach
    void setUp() {
        UserDetails user = ...
        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication(new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities()));
    }

    @Test
    void test() {
        ...
    }
}

# @WebMvcTest와 스프링 시큐리티

@WebMvcTest는 컨트롤러 계층과 관련된 컴포넌트만 컨테이너에 등록하는 슬라이싱 테스트다. @WebMvcTest는 스프링 시큐리티와 관련된 설정도 로드하기 때문에 별도의 시큐리티 관련 구성 클래스를 정의했다면 그 내용이 적용된다.





 
 
 

















@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final MemberDetailsService memberDetailsService;
    private final JwtUtil jwtUtil;
    private final MemberRepository memberRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable()
                .csrf().disable()
                .cors().disable()
                .formLogin().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .addFilterBefore(validateAccessTokenFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests(authorize -> authorize
                        .anyRequest().permitAll()
                );
    }
    // ...
}

문제는 위 코드와 같이 시큐리티 구성 클래스가 다른 빈에 의존할 때 발생한다. @WebMvcTest 테스트 코드를 실행하면 MemberDetailsService, JwtUtil, MemberRepository 빈이 없기 때문에 다음과 같은 에러가 발생한다.

Parameter 0 of constructor in SecurityConfig required a bean of type 'MemberDetailsService' that could not be found.

따라서 다음과 같이 시큐리티 구성 클래스를 빈 등록 대상에서 제외시켜야한다.



 
 
 









 






@WebMvcTest(
    controllers = TestController.class,
    excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class)
    }
)
@DisplayName("TestController 테스트")
class TestControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    @DisplayName("test1() 테스트")
    @WithMockUser
    public void test_test1() throws Exception {
        mvc.perform(get("/test/test1"))
                .andExpect(content().string("test1"));
    }
}

이렇게 하면 스프링 시큐리티의 기본값이 적용되기 때문에 @WithMockUser어노테이션을 붙여 인증된 목업 유저로 테스트를 진행할 수 있다.