SMALL

@WebMvcTest 어노테이션은 Controller Layer를 테스트할 때 사용된다.

( https://joomn11.tistory.com/86 )

 

해당 어노테이션을 사용하면 Spring MVC 컨트롤러를 이용한 요청과 응답에 필요한 Bean들만 로딩하여 가벼운 테스트 환경을 제공한다. 

 

@WebMvcTest 어노테이션을 사용시 다음 내용만 스캔하도록 제한한다

@Controller,
@ControllerAdvice,
@JsonComponent,
Converter / GenericConverter,
Filter,
WebSecurityConfigurerAdapter,
WebMvcConfigurer,
HandlerMethodArgumentResolver

@Component, @Service or @Repository  Bean들은 스캔에서 제외된다. 

 

Spring Security에 대한 설정은 @WebMvcTest의 스캔 대상에 해당한다. 

하지만, 대부분의 Spring Security에서는 다른 Bean들에 대한 Dependency가 존재하게 된다.

@RequiredArgsConstructor
@EnableWebSecurity 
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomOAuth2UserService customOAuth2UserService; // Dependency!!

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	
        http.csrf().disable().headers().frameOptions().disable()
                .and().authorizeRequests() 
                .antMatchers("/", "/css/**", "/js/**", "h2-console/**").permitAll()
                .antMatchers("/api/**").hasRole(Role.USER.name()).anyRequest().authenticated() 
                .and().logout().logoutSuccessUrl("/") 
                .and().oauth2Login() 
                .userInfoEndpoint() 
                .userService(customOAuth2UserService); 

    	http.csrf().disable().headers().frameOptions()
    	    	.disable().and().authorizeRequests().anyRequest().permitAll();
    }

}

 

그렇기 때문에  @WebMvcTest를 진행할 경우 아래와 같은 에러를 보게 된다.

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException:
Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type '${classInfo}' available: expected at least 1 bean which qualifies as autowire candidate.

 

해당 문제를 해결하기 위해서는 Controller Layer에 해당하는 내용에만 Test를 집중할 수 있도록 WebSecurityConfigurerAdapter를 스캔 대상에서 제외하고 @WithMockUser를 통해 Mock 인증 사용자를 생성한다.

 

@WebMvcTest(controllers = HelloController.class, 
        excludeFilters = { //!Added!
                @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SecurityConfig.class) })
public class HelloControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    @WithMockUser(roles = "USER") //!Added!
    public void hello가_리턴된다() throws Exception {
        String hello = "hello";
        mvc.perform(get("/hello")).andExpect(status().isOk()).andExpect(content().string(hello));
    }

}
LIST
SMALL

Spring의 Controller Layer를 테스트하기 위해서는 

요청을 수행하고 응답을 만들어내는 Servlet API Mock 객체를 사용한다.

 

오늘 소개할 객체는 MockMvc이다.

MockMvc : Main entry point for server-side Spring MVC test support

 

MockMvc를 주입받는 방법에는 크게 두가지 방법이 있다. 

  • @SpringBootTest + @AutoConfigureMockMvc
  • @WebMvcTest 

각각의 방식에 대해서 알아 보도록 하자.

 

@SpringBootTest + @AutoConfigureMockMvc

 

@SpringBootTest는 웹 애플리케이션 컨텍스트와 설정을 모두 불러와 실제 웹 서버에 연결을 시도한다.

만약 Web Layer에 해당하는 부분만 테스트를 진행하고 싶은 경우에는 불필요한 설정도 로드하게 되는 것이다. 

통합 테스트를 할때 사용된다.

 

@SpringBootTest
@AutoConfigureMockMvc
public class SbtControllerTest {
	@Autowired
	private MockMvc mockMvc;
    ....
}

 

@WebMvcTest 

 

해당 설정은 Web Layer에 요청을 처리하기 위해 필요한 Bean들만 로딩한다.

단순히 Spring MVC 컨트롤러를 이용한 요청과 응답을 테스트하고자 한다면 @WebMVCTest를 사용하면 보다 가볍게 테스트할 수 있다.

 

@WebMvcTest
public class WebMvcControllerTest {
	@Autowired
	private MockMvc mockMvc;
    ....
}

 

경량화되어서 주는 장점도 있지만, 그에 상응하는 단점도 존재한다.

보통의 WebApplication은 Controller - Service - Repository Layer로 구성되어서 

Controller와 Service가 Dependency가 존재하는데, @WebMvcTest를 사용하는 경우에는 

Controller에 Service 객체를 주입받을 수 없다.

 

그렇기 때문에 Test를 진행하는 사용자가 Mock 객체를 주입해 주어야 한다. 

 

@WebMvcTest
public class WebMvcControllerTest {
	@Autowired
	private MockMvc mockMvc;
    
	@MockBean //Added !!
	private WebMvcService service;
	...
}

 

Service에 해당하는 객체를 Mock으로 주입 받은 경우 테스트 코드 작성 예시는 아래와 같다.

 

import static org.hamcrest.CoreMatchers.is;
import static org.mockito.BDDMockito.given;
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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

    @Test
	public void testGet() throws Exception {

		ResponseDto dto = new ResponseDto("id2", "sName2", "sCode2", "this is s2");

		given(service.findById("id2")).willReturn(dto);

		mockMvc.perform(get("/api/{id}", "id2").contentType(MediaType.APPLICATION_JSON))
				.andDo(print())
				.andExpect(status().isOk())
				.andExpect(jsonPath("$.name", is("sName2")));

	}

 

Controller의 특정 요청을 했을 때, 호출되는 Service의 메서드의 행동을 미리 정의해 놓는 형태이다. 

다음으로 getList를 테스트하는 경우에 예시 코드를 보자.

 

import static org.hamcrest.CoreMatchers.is;
import static org.mockito.BDDMockito.given;
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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

    @Test
	public void testGetList() throws Exception {
		List<StageResponseDto> list = Arrays.asList(
				new StageResponseDto("id2", "sName2", "sCode2", "this is s2"));
		
		given(service.findAll()).willReturn(list);
        
		mockMvc.perform(get("/api").contentType(MediaType.APPLICATION_JSON))
					.andDo(print())
					.andExpect(status().isOk())
					.andExpect(jsonPath("$").isArray())
					.andExpect(jsonPath("$", Matchers.hasSize(1)))
					.andExpect(jsonPath("$[0].name", is("sName2")));
	}

 

 

 

 

LIST
SMALL

Interceptor

 

Interceptor는 "가로채다"라는 의미를 가지고 있다.

Spring에서도 해당 의미와 유사하게 

Client에서 Server로 보낸 Request 객체를 Dispatcher Servlet의 Controller에 도달하기 전에 가로채는 역할을 한다.

 

Interceptor를 통해 가로챈 요청에 추가 로직을 넣을 수도 있고, Request 객체를 검증하는 작업도 할 수 있다. 

출처 : http://www.egovframe.org/wiki/doku.php?id=egovframework:rte:ptl:dispatcherservlet

 

Interceptor를 사용했을 때의 장점은 아래와 같다

  • 공통 로직을 사용하는 경우 코드 누락에 대한 위험성 감수
  • 코드 재사용성 증가
    • 메모리 낭비, 서버 부하 감수 

 

어떠한 프로그램이든 공통 로직은 한 곳에서 관리하는 게 가장 좋다.

그런 면에서 Interceptor는 좋은 옵션을 제공한다. 

 

사용 예제

 

@Component
public class MyInterceptor implements HandlerInterceptor {

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		// do what you want .. 
		return true;
	}

	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		// do what you want .. 
	}

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
 		   Object handler, Exception ex)
			throws Exception {
		// do what you want .. 
	}

}

 

Interceptor를 구현하기 위해서는 "HandlerInterceptor"를 Implements 받으면 된다.

해당 Interface에는 아래의 3가지 메서드를 제공한다.

  • preHandler(...)
    • Controller가 호출되기 전에 실행된다
    • return 값은 boolean으로 해당 값이 false이면 Controller가 호출되지 않는다
  • postHandler
    • Controller가 호출 된 이후지만, View가 생성되기 전에 실행된다
    • ModelAndView 객체를 인자로 받아 View를 생성하기 전에 해당 객체의 정보를 조작할 수 있다
  • afterCompletion
    • View의 모든 작업이 완료된 후에 호출된다

 

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
	
	private final MyInterceptor interceptor;
    
	@Override
	public void addInterceptors(InterceptorRegistry registry) {

		registry.addInterceptor(interceptor).addPathPatterns("/**");
	}
	
}

 

작성한 Interceptor를 Spring에 등록하기 위해서는 WebMvcConfigurer를 Implements 하여서 코드를 추가해야 한다. 

 

WebMvcConfigurer 해당 Interface에 addInterceptors라는 메서드에서 추가해 주면 된다. 

또한 해당 Interceptor가 어떠한 조건일 때 실행되고, 실행되지 않는 등의 설정을 추가할 수 있다.

 

 

 

 

LIST
SMALL

Spring Boot Appliction이 실행되는 시점에 특정 코드를 추가하고 싶은 경우가 존재할 것이다.

이런 경우에 어떻게 해야하는지 알아보도록 하자.

CommandLineRunner

 

@Component
public class InitCommand implements CommandLineRunner {

	@Override
	public void run(String... args) throws Exception {
		// do somethings ..
	}
}

 

ApplicationRunner

 

@Component
public class InitAppliation implements ApplicationRunner {
	@Override
	public void run(ApplicationArguments args) throws Exception {
		// do somethings .. 
	}
}

 

차이점

두 interface 모두 run이라는 메서드를 제공하고, 해당 메서드에서 원하는 로직을 작성하면 된다.

그렇다면 이 둘의 차이점은 무엇일까? 

 

차이점으로는 메서드에 arguments이다. 

CommandLineRunner는 String Array를 Application Runner는 ApplicationArguments를 인자로 받는다.

그렇 각각의 값들을 어떻게 세팅하고 어떻게 꺼내 쓰는지 알아보도록 하자.

 

java -jar SpringApp.jar agrs1 args2 --name=junsu --type=child

 

위와 같은 형태로 Application을 실행시켰다고 하면 

CommandLineRunner에서는 String Array로 해당 값들을 아래와 같이 받을 수 있다.

	@Override
	public void run(String... args) throws Exception {
		System.out.println("#######################################");
		for (String str : args) {
			System.out.println(str);
		}
		
	}

 

ApplicationRunner에서는 ApplicationArguments의 메서드들을 이용해서 값들을 가져올 수 있다.

NonOptionArgs와 Option(Name, Value)로 나누어서 값을 가져올 수 있게 되어있다. 

 

	@Override
	public void run(ApplicationArguments args) throws Exception {

		System.out.println("*******************************************");
		args.getNonOptionArgs().forEach(str -> System.out.println(str));

		args.getOptionNames().forEach(str -> {
			System.out.print(str + " -- ");
			System.out.println(args.getOptionValues(str).get(0));
		});
		
		System.out.println(args.getOptionValues("name"));
	}

 

LIST

+ Recent posts