Spring Security Controller Test

fart man·2026년 3월 4일

Spring Security를 도입한 이후 @WebMvcTest를 활용한 테스트가 실패하기 시작했습니다.

첫번째 실패 원인, Bean 찾기 실패

Spring Security로 옮기면서 여러가지 Refactoring을 수행했는데 그중에 하나가 직접 생성하던 class를 @Component를 활용해 등록하는 것이였습니다.

하지만 그이후에 @WebMvcTest를 붙인 테스트가 Bean을 못찾겠다며 불평하기 시작했습니다.

그때는 그냥 그런가 부다 하고 @Component를 빼고 넘어갔지만 이번 글을 쓰면서 더 자세히 보니 다 이유가 있었습니다.

WebMvcTest는 Test 속도를 높이기 위해 특정 Bean은 생성하지 않습니다.

그리고 그 목록이 WebMvcTest 코드에 친절하게 적혀있습니다.

/**
 * Annotation that can be used for a Spring MVC test that focuses <strong>only</strong> on
 * Spring MVC components.
 * <p>
 * Using this annotation only enables auto-configuration that is relevant to MVC tests.
 * Similarly, component scanning is limited to beans annotated with:
 * <ul>
 * <li>{@code @Controller}</li>
 * <li>{@code @ControllerAdvice}</li>
 * <li>{@code @JsonComponent}</li>
 * </ul>
 * <p>
 * as well as beans that implement:
 * <ul>
 * <li>{@code Converter}</li>
 * <li>{@code DelegatingFilterProxyRegistrationBean}</li>
 * <li>{@code ErrorAttributes}</li>
 * <li>{@code Filter}</li>
 * <li>{@code FilterRegistrationBean}</li>
 * <li>{@code GenericConverter}</li>
 * <li>{@code HandlerInterceptor}</li>
 * <li>{@code HandlerMethodArgumentResolver}</li>
 * <li>{@code HttpMessageConverter}</li>
 * <li>{@code IDialect}, if Thymeleaf is available</li>
 * <li>{@code Module}, if Jackson is available</li>
 * <li>{@code SecurityFilterChain}</li>
 * <li>{@code WebMvcConfigurer}</li>
 * <li>{@code WebMvcRegistrations}</li>
 * <li>{@code WebSecurityConfigurer}</li>
 * </ul>
 * <p>
 * By default, tests annotated with {@code @WebMvcTest} will also auto-configure Spring
 * Security and {@link MockMvc} (include support for HtmlUnit WebClient and Selenium
 * WebDriver). For more fine-grained control of MockMVC the
 * {@link AutoConfigureMockMvc @AutoConfigureMockMvc} annotation can be used.
 * <p>

그리고 저의 class는 저위에 쓰인대로 @Controller도 아니고, @ControllerAdvice도 아니고, @JsonComponent도 아니고, 저기에 있는 어떤 class도 상속하지 않아서 Spring이 걸러쳤습니다.

뭐 우짜냐 싶지만 해결방안이 있다고 합니다. 바로 @Import와 @ComponentScan입니다.

@ComponentScan과 @Import에 대해

원래는

@Bean
Foo createFoo() {
	return new Foo();
}

이런 식으로 등록해야 DI가 되지만 Spring에서 좀 편하게 가자고 @ComponentScan과 @Import를 만들어 줬습니다.

이 둘중 하나를 이용하면

@Component, @Service, @Repository, @Controller, Spring의 stereotype annotation이 달린 class를 Spring 이 자동으로 Bean으로 등록하게 만들 수 있습니다.

잉? 난 저거 써본적 없는데?
그럼 지금까지 내 @Service, @Component, 얘네들은 어떻게 Bean으로 등록된건데?

그건 의례 그렇듯 spring boot가 @SpringBootApplication에 슬쩍 끼워 넣어서 그렇습니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

그건 그렇다치고 그럼 WebMvcTest 테스트할때 내 @Component어떻게 불러?

그냥 이렇게

@WebMvcTest(FooController.class)
@Import({Bar.class})
class TodoControllerTest {

적으면 됩니다.

두번째 실패 원인, Spring Security

위 WebMvcTest코드를 읽어보면 Spring Security 도 같이 불러온다고 적혀있습니다. 그래서 잘되던 테스트가 갑자기 http status 401을 뿜어내며 실패하게 됩니다.

그럴때는

    @Test
    @WithMockUser(username = "tester", roles = "USER")
    void todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다() throws Exception {
    	...
    }

이처럼 @WithMockUser를 쓰면 됩니다.

@WithMockUser 문서를 읽어보면

/**
 * When used with {@link WithSecurityContextTestExecutionListener} this annotation can be
 * added to a test method to emulate running with a mocked user. In order to work with
 * {@link MockMvc} The {@link SecurityContext} that is used will have the following
 * properties:
 *
 * <ul>
 * <li>The {@link SecurityContext} created with be that of
 * {@link SecurityContextHolder#createEmptyContext()}</li>
 * <li>It will be populated with an {@link UsernamePasswordAuthenticationToken} that uses
 * the username of either {@link #value()} or {@link #username()},
 * {@link GrantedAuthority} that are specified by {@link #roles()}, and a password
 * specified by {@link #password()}.
 * </ul>
 *
 * @see WithUserDetails
 * @author Rob Winch
 * @since 4.0
 */
 * <li>The {@link SecurityContext} created with be that of
 * {@link SecurityContextHolder#createEmptyContext()}</li>

이 test를 위해 따로 empty한 (그니까 Authentication 이 없는) SecurityContext를 만든뒤

 * <li>It will be populated with an {@link UsernamePasswordAuthenticationToken} that uses
 * the username of either {@link #value()} or {@link #username()},
 * {@link GrantedAuthority} that are specified by {@link #roles()}, and a password
 * specified by {@link #password()}.

저 이름과 비밀번호를 가진 UsernamePasswordAuthenticationToken을 주입시켜 줍니다.

응, 그건 다좋은데 @WithMockUser넣으니까 compile이 않되!!

dependencies {
    // spring security
    implementation 'org.springframework.boot:spring-boot-starter-security'
    testImplementation 'org.springframework.security:spring-security-test'
}

build.gradle에 dependency하나 더 추가하세요.

0개의 댓글