Spring Security를 도입한 이후 @WebMvcTest를 활용한 테스트가 실패하기 시작했습니다.
Spring Security로 옮기면서 여러가지 Refactoring을 수행했는데 그중에 하나가 직접 생성하던 class를 @Component를 활용해 등록하는 것이였습니다.
하지만 그이후에 @WebMvcTest를 붙인 테스트가 Bean을 못찾겠다며 불평하기 시작했습니다.
그때는 그냥 그런가 부다 하고 @Component를 빼고 넘어갔지만 이번 글을 쓰면서 더 자세히 보니 다 이유가 있었습니다.
그리고 그 목록이 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입니다.
원래는
@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(FooController.class)
@Import({Bar.class})
class TodoControllerTest {
적으면 됩니다.
위 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을 주입시켜 줍니다.
dependencies {
// spring security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
}
build.gradle에 dependency하나 더 추가하세요.