while(1) work();
반응형

WebTestClient

WebTestClient는 서버 애플리케이션을 테스트하기 위해 설계된 HTTP 클라이언트입니다. Spring의 WebClient를 래핑하여 요청을 수행하는 데 사용하지만 응답을 확인하기 위한 테스트 파사드를 노출합니다. WebTestClient는 엔드투엔드 HTTP 테스트를 수행하는 데 사용할 수 있습니다. 또한 모의 서버 요청 및 응답 객체를 통해 실행 중인 서버 없이 Spring MVC 및 Spring WebFlux 애플리케이션을 테스트하는 데 사용할 수 있습니다.

Setup

WebTestClient를 설정하려면 바인딩할 서버 설정을 선택해야 합니다. 여러 모의 서버 설정 중 하나를 선택하거나 라이브 서버에 연결할 수 있습니다.

컨트롤러에 바인딩

이 설정을 사용하면 실행 중인 서버 없이 모의 요청 및 응답 개체를 통해 특정 컨트롤러를 테스트할 수 있습니다.

WebFlux 애플리케이션의 경우 다음을 사용하여WebFlux Java 구성에 해당하는 인프라를 로드하고, 지정된 컨트롤러를 등록하고, 요청을 처리할 WebHandler 체인을생성합니다:

WebTestClient client =
		WebTestClient.bindToController(new TestController()).build();

Spring MVC의 경우, 다음을 사용하여스탠드얼론MockMvc빌더에위임하여 WebMvc Java 구성에 해당하는 인프라를 로드하고, 지정된 컨트롤러를 등록하고, 요청을 처리할MockMvc의 인스턴스를 생성합니다:

WebTestClient client =
		MockMvcWebTestClient.bindToController(new TestController()).build();

ApplicationContext에 바인딩

이 설정을 사용하면 실행 중인 서버 없이도 Spring MVC 또는 Spring WebFlux 인프라 및 컨트롤러 선언으로 Spring 구성을 로드하고 이를 사용하여 모의 요청 및 응답 객체를 통해 요청을 처리할 수 있습니다.

WebFlux의 경우, 요청을 처리할 WebHandler 체인을 생성하기 위해 Spring ApplicationContext가WebHttpHandlerBuilder에전달되는 곳에서 다음을 사용하세요:

@SpringJUnitConfig(WebConfig.class) 
class MyTests {

	WebTestClient client;

	@BeforeEach
	void setUp(ApplicationContext context) {  
		client = WebTestClient.bindToApplicationContext(context).build(); 
	}
}
  로드할 구성을 지정합니다
  구성을 주입합니다
  웹 테스트 클라이언트 생성

Spring MVC의 경우, 요청을 처리할 MockMvc 인스턴스를 생성하기 위해 Spring ApplicationContext가MockMvcBuilders.webAppContextSetup에전달되는 곳에서 다음을 사용합니다:

@ExtendWith(SpringExtension.class)
@WebAppConfiguration("classpath:META-INF/web-resources") 
@ContextHierarchy({
	@ContextConfiguration(classes = RootConfig.class),
	@ContextConfiguration(classes = WebConfig.class)
})
class MyTests {

	@Autowired
	WebApplicationContext wac; 

	WebTestClient client;

	@BeforeEach
	void setUp() {
		client = MockMvcWebTestClient.bindToApplicationContext(this.wac).build(); 
	}
}
  로드할 구성을 지정합니다
  구성을 주입합니다
  웹 테스트 클라이언트 생성

라우터 기능에 바인딩

이 설정을 사용하면 실행 중인 서버 없이 모의 요청 및 응답 객체를 통해 기능적 엔드포인트를 테스트할 수 있습니다.

WebFlux의 경우, 요청을 처리하기 위한 서버 설정을 생성하기 위해 RouterFunctions.toWebHandler에 위임하는 다음을 사용합니다:

RouterFunction<?> route = ...
client = WebTestClient.bindToRouterFunction(route).build();

Spring MVC의 경우 현재WebMvc 함수형 엔드포인트를 테스트할 수 있는 옵션이 없습니다.

서버에 바인드

이 설정은 실행 중인 서버에 연결하여 전체 엔드투엔드 HTTP 테스트를 수행합니다:

client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();

클라이언트 구성

앞서 설명한 서버 설정 옵션 외에도 기본 URL, 기본 헤더, 클라이언트 필터 등을 포함한 클라이언트 옵션을 구성할 수 있습니다. 이러한 옵션은 bindToServer() 다음에 쉽게 사용할 수 있습니다. 다른 모든 구성 옵션의 경우 다음과 같이 configureClient() 를 사용하여 서버에서 클라이언트 구성으로 전환해야 합니다:

client = WebTestClient.bindToController(new TestController())
		.configureClient()
		.baseUrl("/test")
		.build();

Writing Tests

WebTestClient는 exchange()를 사용하여 요청을 수행하는 시점까지 WebClient와동일한 API를 제공합니다. 양식 데이터, 멀티파트 데이터 등을 포함한 모든 콘텐츠가 포함된 요청을 준비하는 방법에 대한 예제는WebClient 설명서를 참조하세요.

Exchange() 호출 후 WebTestClient는 WebClient에서 분리되어 대신 응답을 확인하는 워크플로우를 계속 진행합니다.

응답 상태 및 헤더를 어설트하려면 다음을 사용합니다:

client.get().uri("/persons/1")
	.accept(MediaType.APPLICATION_JSON)
	.exchange()
	.expectStatus().isOk()
	.expectHeader().contentType(MediaType.APPLICATION_JSON);

기대 중 하나가 실패하더라도 모든 기대가 어설트되도록 하려면 여러 개의 연쇄된 기대*(..) 호출 대신 기대All(..)을 사용하면 됩니다. 이 기능은 AssertJ의 소프트 어설션 지원 및 JUnit Jupiter의 assertAll() 지원과 유사합니다.

client.get().uri("/persons/1")
	.accept(MediaType.APPLICATION_JSON)
	.exchange()
	.expectAll(
		spec -> spec.expectStatus().isOk(),
		spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
	);

그런 다음 다음 중 하나를 통해 응답 본문을 디코딩하도록 선택할 수 있습니다:

  • expectBody(Class<T>): 단일 객체로 디코딩합니다.
  • expectBodyList(Class<T>): 객체를 List<T>로 디코딩하고 수집합니다.
  • expectBody(): JSON 콘텐츠 또는 빈 본문으로 바이트열[] 로 디코딩합니다.

그리고 결과 상위 레벨 객체에 대해 어설션을 수행합니다:

client.get().uri("/persons")
		.exchange()
		.expectStatus().isOk()
		.expectBodyList(Person.class).hasSize(3).contains(person);

기본 제공 어설션이 충분하지 않은 경우, 대신 객체를 소비하고 다른 어설션을 수행할 수 있습니다:

   import org.springframework.test.web.reactive.server.expectBody

client.get().uri("/persons/1")
		.exchange()
		.expectStatus().isOk()
		.expectBody(Person.class)
		.consumeWith(result -> {
			// custom assertions (e.g. AssertJ)...
		});

또는 워크플로를 종료하고 EntityExchangeResult를 얻을 수 있습니다:

EntityExchangeResult<Person> result = client.get().uri("/persons/1")
		.exchange()
		.expectStatus().isOk()
		.expectBody(Person.class)
		.returnResult();
  제네릭을 사용하여 대상 유형으로 디코딩해야 하는 경우, Class<T> 대신ParameterizedTypeReference를허용하는 오버로드된 메서드를 찾아보세요

콘텐츠 없음

응답에 콘텐츠가 없을 것으로 예상되는 경우 다음과 같이 어설트할 수 있습니다:

client.post().uri("/persons")
		.body(personMono, Person.class)
		.exchange()
		.expectStatus().isCreated()
		.expectBody().isEmpty();

응답 콘텐츠를 무시하려면 다음과 같이 어설션 없이 콘텐츠를 해제합니다:

client.get().uri("/persons/123")
		.exchange()
		.expectStatus().isNotFound()
		.expectBody(Void.class);

JSON 콘텐츠

대상 유형 없이 expectBody() 를 사용하여 상위 수준 Object를 통하지 않고 원시 콘텐츠에 어설션을 수행할 수 있습니다.

JSONAssert로 전체 JSON 콘텐츠를 확인하려면 다음과 같이 하세요:

client.get().uri("/persons/1")
		.exchange()
		.expectStatus().isOk()
		.expectBody()
		.json("{\"name\":\"Jane\"}")

JSONPath로 JSON 콘텐츠를 확인하려면:

client.get().uri("/persons")
		.exchange()
		.expectStatus().isOk()
		.expectBody()
		.jsonPath("$[0].name").isEqualTo("Jane")
		.jsonPath("$[1].name").isEqualTo("Jason");

스트리밍 응답

"text/event-stream " 또는"application/x-ndjson"과 같이 잠재적으로 무한한 스트림을 테스트하려면 먼저 응답 상태와 헤더를 확인한 다음 FluxExchangeResult를 얻습니다:

FluxExchangeResult<MyEvent> result = client.get().uri("/events")
		.accept(TEXT_EVENT_STREAM)
		.exchange()
		.expectStatus().isOk()
		.returnResult(MyEvent.class);

이제 reactor-test의 StepVerifier로 응답 스트림을 소비할 준비가 되었습니다:

Flux<Event> eventFlux = result.getResponseBody();

StepVerifier.create(eventFlux)
		.expectNext(person)
		.expectNextCount(4)
		.consumeNextWith(p -> ...)
		.thenCancel()
		.verify();

MockMvc 어설션

WebTestClient는 HTTP 클라이언트이므로 상태, 헤더, 본문 등 클라이언트 응답에 있는 내용만 확인할 수 있습니다.

MockMvc 서버 설정으로 Spring MVC 애플리케이션을 테스트할 때 서버 응답에 대해 추가 어설션을 수행할 수 있는 추가 옵션이 있습니다. 이를 수행하려면 먼저 본문 어설션 후 ExchangeResult를 가져옵니다:

// For a response with a body
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
		.exchange()
		.expectStatus().isOk()
		.expectBody(Person.class)
		.returnResult();

// For a response without a body
EntityExchangeResult<Void> result = client.get().uri("/path")
		.exchange()
		.expectBody().isEmpty();

그런 다음 MockMvc 서버 응답 어설션으로 전환합니다:

MockMvcWebTestClient.resultActionsFor(result)
		.andExpect(model().attribute("integer", 3))
		.andExpect(model().attribute("string", "a string value"));

 

MockMvc

MockMvc라고도 하는 Spring MVC 테스트 프레임워크는 Spring MVC 애플리케이션 테스트를 지원합니다. 전체 Spring MVC 요청 처리를 수행하지만 실행 중인 서버 대신 모의 요청 및 응답 객체를 통해 수행합니다.

MockMvc는 자체적으로 요청을 수행하고 응답을 확인하는 데 사용할 수 있습니다. 또한 요청을 처리할 서버로 MockMvc가 연결된 WebTestClient를 통해 사용할 수도 있습니다. WebTestClient의 장점은 원시 데이터 대신 상위 수준의 객체로 작업할 수 있는 옵션과 라이브 서버에 대한 전체 엔드투엔드 HTTP 테스트로 전환하고 동일한 테스트 API를 사용할 수 있다는 점입니다.

 

개요

컨트롤러를 인스턴스화하고, 종속성을 주입하고, 해당 메서드를 호출하여 Spring MVC에 대한 일반 단위 테스트를 작성할 수 있습니다. 그러나 이러한 테스트는 요청 매핑, 데이터 바인딩, 메시지 변환, 유형 변환, 유효성 검사를 확인하지 않으며, 지원되는 @InitBinder, @ModelAttribute 또는@ExceptionHandler 메서드도 포함하지 않습니다.

MockMvc라고도 하는 Spring MVC 테스트 프레임워크는 실행 중인 서버 없이 Spring MVC 컨트롤러에 대한 보다 완벽한 테스트를 제공하는 것을 목표로 합니다. 이를 위해 DispatcherServlet을 호출하고 실행 중인 서버 없이 전체 Spring MVC 요청 처리를 복제하는스프링 테스트 모듈에서Servlet API의 "모의" 구현을 전달합니다.

MockMvc는 경량 및 대상 테스트를 사용하여 Spring MVC 애플리케이션의 대부분의 기능을 검증할 수 있는 서버 측 테스트 프레임워크입니다. 자체적으로 사용하여 요청을 수행하고 응답을 확인할 수도 있고, 요청을 처리할 서버로 MockMvc를 연결한 상태에서 WebTestClient API를 통해 사용할 수도 있습니다.

 

정적 임포트

MockMvc를 직접 사용하여 요청을 수행하는 경우, 정적 임포트가 필요합니다:

  • MockMvcBuilders.*
  • MockMvcRequestBuilders.*
  • MockMvcResultMatchers.*
  • MockMvcResultHandlers.*

기억하기 쉬운 방법은 MockMvc*를 검색하는 것입니다. Eclipse를 사용하는 경우 Eclipse 환경 설정에서 위의 내용을 "즐겨 찾는 정적 멤버"로 추가해야 합니다.

웹 테스트 클라이언 트를 통해 MockMvc를 사용하는 경우 정적 임포트가 필요하지 않습니다. 웹 테스트 클라이언트는 정적 임포트 없이 유창한 API를 제공합니다.

 

설정 선택

MockMvc는 두 가지 방법 중 하나로 설정할 수 있습니다. 하나는 테스트하려는 컨트롤러를 직접 가리키고 Spring MVC 인프라를 프로그래밍 방식으로 구성하는 것입니다. 두 번째는 Spring MVC와 컨트롤러 인프라가 포함된 Spring 구성을 가리키는 것입니다.

특정 컨트롤러를 테스트하기 위해 MockMvc를 설정하려면 다음을 사용합니다:

class MyWebTests {

	MockMvc mockMvc;

	@BeforeEach
	void setup() {
		this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
	}

	// ...

}

또는 위와 같이 동일한 빌더에 위임하는WebTestClient를 통해 테스트할 때 이 설정을 사용할 수도 있습니다.

Spring 구성을 통해 MockMvc를 설정하려면 다음을 사용합니다:

@SpringJUnitWebConfig(locations = "my-servlet-context.xml")
class MyWebTests {

	MockMvc mockMvc;

	@BeforeEach
	void setup(WebApplicationContext wac) {
		this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
	}

	// ...

}

또는 위와 같이 동일한 빌더에 위임하는WebTestClient를 통해 테스트할 때 이 설정을 사용할 수도 있습니다.

어떤 설정 옵션을 사용해야 하나요?

웹앱컨텍스트설정에서는 실제 Spring MVC 구성을 로드하므로 보다 완벽한 통합 테스트를 수행할 수 있습니다. 테스트 컨텍스트 프레임워크는 로드된 Spring 구성을 캐시하므로 테스트 스위트에 더 많은 테스트를 도입하더라도 테스트를 빠르게 실행하는 데 도움이 됩니다. 또한 Spring 구성을 통해 모의 서비스를 컨트롤러에 주입하여 웹 계층 테스트에 계속 집중할 수 있습니다. 다음 예제는 Mockito로 모의 서비스를 선언합니다:

<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
	<constructor-arg type="java.lang.Class" value="org.example.AccountService"/>
	<constructor-arg type="java.lang.String" value="accountService"/>
</bean>

그런 다음 다음 예제와 같이 모의 서비스를 테스트에 주입하여 기대치를 설정하고 확인할 수 있습니다:

@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class AccountTests {

	@Autowired
	AccountService accountService;

	MockMvc mockMvc;

	@BeforeEach
	void setup(WebApplicationContext wac) {
		this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
	}

	// ...

}

반면에 스탠드얼론 설정은 단위 테스트에 조금 더 가깝습니다. 이 테스트는 한 번에 하나의 컨트롤러를 테스트합니다. 컨트롤러에 모의 종속성을 수동으로 주입할 수 있으며 Spring 구성을 로드하지 않아도 됩니다. 이러한 테스트는 스타일에 더 중점을 두며 어떤 컨트롤러가 테스트되고 있는지, 특정 Spring MVC 구성이 작동하는 데 필요한지 등을 더 쉽게 확인할 수 있습니다. 또한 스탠드얼론 설정은 특정 동작을 확인하거나 문제를 디버깅하기 위한 임시 테스트를 작성하는 데 매우 편리한 방법입니다.

대부분의 '통합 대 단위 테스트' 논쟁과 마찬가지로 정답이나 오답은 없습니다. 그러나 standaloneSetup을 사용하면 Spring MVC 구성을 확인하기 위해 추가적인webAppContextSetup 테스트가 필요하다는 것을 의미합니다. 또는 항상 실제 Spring MVC 구성에 대해 테스트하기 위해 모든 테스트를 webAppContextSetup으로 작성할 수 있습니다.

설정 기능

어떤 MockMvc 빌더를 사용하든 모든 MockMvcBuilder 구현은 몇 가지 공통적이고 매우 유용한 기능을 제공합니다. 예를 들어 다음과 같이 모든 요청에 대해 Accept 헤더를 선언하고 모든 응답에 200의 상태와 Content-Type 헤더를 기대할 수 있습니다:

// static import of MockMvcBuilders.standaloneSetup

MockMvc mockMvc = standaloneSetup(new MusicController())
	.defaultRequest(get("/").accept(MediaType.APPLICATION_JSON))
	.alwaysExpect(status().isOk())
	.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
	.build();

또한 타사 프레임워크(및 애플리케이션)는 MockMvcConfigurer에서와 같이 설정 지침을 미리 패키징할 수 있습니다. Spring 프레임워크에는 여러 요청에서 HTTP 세션을 저장하고 재사용하는 데 도움이 되는 이러한 내장 구현이 하나 있습니다. 다음과 같이 사용할 수 있습니다:

// static import of SharedHttpSessionConfigurer.sharedHttpSession

MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
		.apply(sharedHttpSession())
		.build();

// Use mockMvc to perform requests...

모든 MockMvc 빌더 기능의 목록을 보려면ConfigurableMockMvcBuilder에대한 자바독을 참조하거나 IDE를 사용하여 사용 가능한 옵션을 탐색하세요.

 

요청 수행하기

이 섹션에서는 MockMvc를 단독으로 사용하여 요청을 수행하고 응답을 확인하는 방법을 보여줍니다. WebTestClient를 통해 MockMvc를 사용하는 경우 대신테스트 작성에 해당하는 섹션을 참조하세요.

다음 예시와 같이 모든 HTTP 메서드를 사용하는 요청을 수행합니다:

// static import of MockMvcRequestBuilders.*

mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));

멀티파트 요청의 실제 구문 분석이 없도록 내부적으로MockMultipartHttpServletRequest를 사용하는 파일 업로드 요청을 수행할 수도 있습니다. 대신 다음 예제와 유사하게 설정해야 합니다:

mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));

다음 예제와 같이 쿼리 매개변수를 URI 템플릿 스타일로 지정할 수 있습니다:

mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));

다음 예시처럼 쿼리 또는 양식 매개변수를 나타내는 서블릿 요청 매개변수를 추가할 수도 있습니다:

mockMvc.perform(get("/hotels").param("thing", "somewhere"));

애플리케이션 코드가 서블릿 요청 매개변수에 의존하고 쿼리 문자열을 명시적으로 확인하지 않는 경우(대부분의 경우) 어떤 옵션을 사용하든 상관없습니다. 그러나 URI 템플릿으로 제공되는 쿼리 매개변수는 디코딩되는 반면 param(...) 메서드를 통해 제공되는 요청 매개변수는 이미 디코딩될 것으로 예상된다는 점에 유의하세요.

대부분의 경우 컨텍스트 경로와 서블릿 경로는 요청 URI에서 제외하는 것이 좋습니다. 전체 요청 URI로 테스트해야 하는 경우, 다음 예제에서와 같이 요청 매핑이 작동하도록 contextPath및 servletPath를 적절히 설정해야 합니다:

mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))

앞의 예제에서는 수행되는 모든 요청에 대해 contextPath와servletPath를 설정하는 것이 번거로울 수 있습니다. 대신 다음 예제에서와 같이 기본 요청 속성을 설정할 수 있습니다:

class MyWebTests {

	MockMvc mockMvc;

	@BeforeEach
	void setup() {
		mockMvc = standaloneSetup(new AccountController())
			.defaultRequest(get("/")
			.contextPath("/app").servletPath("/main")
			.accept(MediaType.APPLICATION_JSON)).build();
	}
}

앞의 속성은 MockMvc 인스턴스를 통해 수행되는 모든 요청에 영향을 줍니다. 지정된 요청에도 동일한 속성이 지정되면 기본값이 재정의됩니다. 따라서 기본 요청의 HTTP 메서드와 URI는 모든 요청에 지정되어야 하므로 중요하지 않습니다.

 

기대 정의하기

다음 예제에서 볼 수 있듯이 요청을 수행한 후 하나 이상의 andExpect(..) 호출을 추가하여 기대값을 정의할 수 있습니다. 하나의 기대가 실패하면 다른 기대는 어설션되지 않습니다.

// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*

mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());

다음 예제에서와 같이 요청을 수행한 후 andExpectAll(..)을 추가하여 여러 기대치를 정의할 수 있습니다. AndExpect(...)와 달리andExpectAll(...)은 제공된 모든 기대가 어설트되고 모든 실패가 추적 및 보고되도록 보장합니다.

// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*

mockMvc.perform(get("/accounts/1")).andExpectAll(
	status().isOk(),
	content().contentType("application/json;charset=UTF-8"));

MockMvcResultMatchers.*는 여러 기대값을 제공하며, 그 중 일부는 더 자세한 기대값으로 중첩되어 있습니다.

기대치는 두 가지 일반적인 범주에 속합니다. 첫 번째 범주의 어설션은 응답의 속성(예: 응답 상태, 헤더 및 콘텐츠)을 확인합니다. 이는 가장 중요한 어설션에 해당하는 결과입니다.

두 번째 범주의 어설션은 응답을 넘어서는 것입니다. 이러한 어설션을 사용하면 요청을 처리한 컨트롤러 메서드, 예외 발생 및 처리 여부, 모델의 콘텐츠, 선택된 뷰, 추가된 플래시 속성 등 Spring MVC의 특정 측면을 검사할 수 있습니다. 또한 요청 및 세션 속성과 같은 서블릿의 특정 측면을 검사할 수 있습니다.

다음 테스트는 바인딩 또는 유효성 검사가 실패했다고 가정합니다:

mockMvc.perform(post("/persons"))
	.andExpect(status().isOk())
	.andExpect(model().attributeHasErrors("person"));

테스트를 작성할 때 수행된 요청의 결과를 덤프하는 것이 유용한 경우가 많습니다. 다음과 같이 할 수 있으며, 여기서 print() 는MockMvcResultHandlers에서 정적으로 가져온 것입니다:

mockMvc.perform(post("/persons"))
	.andDo(print())
	.andExpect(status().isOk())
	.andExpect(model().attributeHasErrors("person"));

요청 처리로 인해 처리되지 않은 예외가 발생하지 않는 한, print() 메서드는 사용 가능한 모든 결과 데이터를 System.out에 인쇄합니다. 또한 log() 메서드와 print( ) 메서드의 두 가지 추가 변형이 있는데, 하나는 출력 스트림을 받아들이는 것이고 다른 하나는 Writer를 받아들이는 것입니다. 예를 들어, print(System.err )를 호출하면 결과 데이터가 System.err에 인쇄되고, print(myWriter )를 호출하면 결과 데이터가 사용자 지정 작성기에 인쇄됩니다. 결과 데이터를 인쇄하는 대신 로깅하려는 경우, 결과 데이터를org.springframework.test.web.servlet.result 로깅 카테고리 아래에 단일 DEBUG 메시지로 로깅하는log() 메서드를 호출할 수 있습니다.

경우에 따라 결과에 직접 액세스하여 다른 방법으로는 확인할 수 없는 것을 확인하고 싶을 수 있습니다. 다음 예제에서 볼 수 있듯이 다른 모든 기대값 뒤에 .andReturn() 을 추가하면 이 작업을 수행할 수 있습니다:

MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn();
// ...

모든 테스트가 동일한 기대치를 반복하는 경우 다음 예제에서와 같이 MockMvc 인스턴스를 빌드할 때 공통 기대치를 한 번 설정할 수 있습니다:

standaloneSetup(new SimpleController())
	.alwaysExpect(status().isOk())
	.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
	.build()

공통 기대치는 항상 적용되며 별도의 MockMvc 인스턴스를 만들지 않고는 재정의할 수 없습니다.

JSON 응답 콘텐츠에Spring HATEOAS로 생성된 하이퍼미디어 링크가 포함된 경우 다음 예제와 같이 JsonPath 표현식을 사용하여 결과 링크를 확인할 수 있습니다:

mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))
	.andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));

XML 응답 콘텐츠에Spring HATEOAS로 생성된 하이퍼미디어 링크가 포함된 경우, XPath 표현식을 사용하여 결과 링크를 확인할 수 있습니다:

Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom");
mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML))
	.andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people"));

비동기 요청

이 섹션에서는 MockMvc를 단독으로 사용하여 비동기 요청 처리를 테스트하는 방법을 보여줍니다. WebTestClient를 통해 MockMvc를 사용하는 경우, WebTestClient가 이 섹션에서 설명하는 작업을 자동으로 수행하므로 비동기 요청을 작동시키기 위해 특별히 할 일이 없습니다.

Spring MVC에서 지원되는 서블릿 비동기 요청은 서블릿 컨테이너 스레드를 종료하고 애플리케이션이 응답을 비동기적으로 계산하도록 허용한 후 비동기 디스패치가 수행되어 서블릿 컨테이너 스레드에서 처리를 완료하는 방식으로 작동합니다.

Spring MVC 테스트에서 비동기 요청은 먼저 생성된 비동기 값을 어설션한 다음 비동기 디스패치를 수동으로 수행하고 마지막으로 응답을 확인하는 방식으로 테스트할 수 있습니다. 다음은 DeferredResult, Callable 또는 Reactor Mono와 같은 반응형 유형을 반환하는 컨트롤러 메서드의 테스트 예제입니다:

// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*

@Test
void test() throws Exception {
       MvcResult mvcResult = this.mockMvc.perform(get("/path"))
               .andExpect(status().isOk()) 
               .andExpect(request().asyncStarted()) 
               .andExpect(request().asyncResult("body")) 
               .andReturn();

       this.mockMvc.perform(asyncDispatch(mvcResult)) 
               .andExpect(status().isOk()) 
               .andExpect(content().string("body"));
   }
  응답 상태가 여전히 변경되지 않았는지 확인
  비동기 처리가 시작되었음
  기다렸다가 비동기 결과를 확인합니다
  수동으로 비동기 디스패치를 수행합니다(실행 중인 컨테이너가 없으므로)
  최종 응답을 확인합니다

 

스트리밍 응답

WebTestClient를 사용하여 서버에서 보낸 이벤트와 같은 스트리밍 응답을테스트할 수 있습니다. 그러나 클라이언트 측에서 서버 스트림을 취소할 수 있는 방법이 없기 때문에 무한 스트림은 지원하지 않습니다. 무한 스트림을 테스트하려면 실행 중인서버에 바인딩하거나 Spring Boot를 사용하는 경우실행 중인 서버로 테스트해야 합니다.

비동기 응답은 물론 스트리밍 응답도 지원하지만, 서버가 중지하도록 영향을 줄 수 없으므로 서버가 자체적으로 응답 작성을 완료해야 한다는 제한이 있습니다.

 

필터 등록

다음 예시와 같이 MockMvc 인스턴스를 설정할 때 하나 이상의 서블릿 필터인스턴스를 등록할 수 있습니다:

mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();

등록된 필터는 스프링 테스트에서 MockFilterChain을 통해 호출되며, 마지막 필터는 DispatcherServlet에 위임됩니다.

 

MockMvc와 엔드투엔드 테스트 비교

MockMvc는스프링 테스트 모듈의 서블릿 API 모의 구현을 기반으로 구축되며 실행 중인 컨테이너에 의존하지 않습니다. 따라서 실제 클라이언트와 라이브 서버를 실행하는 완전한 엔드투엔드 통합 테스트와 비교할 때 몇 가지 차이점이 있습니다.

가장 쉽게 생각할 수 있는 방법은 빈 MockHttpServletRequest로 시작하는 것입니다. 여기에 무엇을 추가하느냐에 따라 요청이 어떻게 되는지 알 수 있습니다. 놀랄 수 있는 점은 기본적으로 컨텍스트 경로, jsessionid 쿠키, 포워딩, 오류 또는 비동기 디스패치, 따라서 실제 JSP 렌더링이 없다는 것입니다. 대신 "전달된" 및 "리디렉션된" URL이 MockHttpServletResponse에 저장되며 기대값으로 어설트할 수 있습니다.

즉, JSP를 사용하는 경우 요청이 전달된 JSP 페이지를 확인할 수 있지만 HTML은 렌더링되지 않습니다. 즉, JSP가 호출되지 않습니다. 그러나 전달에 의존하지 않는 다른 모든 렌더링 기술(예: Thymeleaf 및 Freemarker)은 예상대로 응답 본문으로 HTML을 렌더링한다는 점에 유의하세요. JSON, XML 및 기타 형식을 @ResponseBody 메서드를 통해 렌더링할 때도 마찬가지입니다.

또는 @SpringBootTest를 사용하여 Spring Boot의 전체 엔드투엔드 통합 테스트 지원을 고려할 수도 있습니다.Spring Boot 참조 가이드를 참조하세요.

각 접근 방식에는 장단점이 있습니다. Spring MVC 테스트에서 제공하는 옵션은 기존 단위 테스트에서 전체 통합 테스트에 이르기까지 다양한 단계로 구성되어 있습니다. 확실히, Spring MVC Test의 옵션 중 어느 것도 클래식 단위 테스트의 범주에 속하지는 않지만 조금 더 가깝습니다. 예를 들어, 모의 서비스를 컨트롤러에 주입하여 웹 계층을 격리할 수 있는데, 이 경우 웹 계층은 DispatcherServlet을 통해서만 테스트하지만 데이터 액세스 계층은 그 위의 계층과 격리하여 테스트할 수 있으므로 실제 Spring 구성으로 테스트할 수 있습니다. 또한 독립형 설정을 사용하여 한 번에 하나의 컨트롤러에 집중하고 작동하는 데 필요한 구성을 수동으로 제공할 수 있습니다.

Spring MVC 테스트를 사용할 때 또 다른 중요한 차이점은 개념적으로 이러한 테스트는 서버 측이므로 어떤 핸들러가 사용되었는지, 예외가 HandlerExceptionResolver로 처리되었는지, 모델의 내용이 무엇인지, 어떤 바인딩 오류가 있었는지 및 기타 세부 사항을 확인할 수 있다는 것입니다. 즉, 실제 HTTP 클라이언트를 통해 테스트할 때처럼 서버가 불투명한 상자가 아니기 때문에 기대치를 작성하기가 더 쉽습니다. 이는 일반적으로 고전적인 단위 테스트의 장점입니다: 작성, 추론, 디버깅이 더 쉽지만 전체 통합 테스트의 필요성을 대체하지는 못합니다. 동시에 응답이 가장 중요하게 확인해야 할 사항이라는 사실을 놓치지 않는 것이 중요합니다. 요컨대, 같은 프로젝트 내에서도 다양한 스타일과 전략으로 테스트할 수 있는 여지가 있습니다.

 

추가 예제

프레임워크의 자체 테스트에는 MockMvc를 자체적으로 또는WebTestClient를 통해 사용하는 방법을 보여주기 위한많은 샘플 테스트가 포함되어 있습니다. 더 많은 아이디어를 얻으려면 이러한 예제를 살펴보세요.

 

HtmlUnit 통합

Spring은 MockMvc와HtmlUnit 간의 통합을 제공합니다. 이를 통해 HTML 기반 뷰를 사용할 때 엔드투엔드 테스트 수행이 간소화됩니다. 이 통합을 통해 다음을 수행할 수 있습니다:

  • 서블릿 컨테이너에 배포할 필요 없이HtmlUnit,WebDriver,Geb 등의 도구를 사용하여 HTML 페이지를 쉽게 테스트할 수 있습니다.
  • 페이지 내에서 JavaScript를 테스트할 수 있습니다.
  • 선택 사항으로 모의 서비스를 사용하여 테스트 속도를 높일 수 있습니다.
  • 컨테이너 내 엔드투엔드 테스트와 컨테이너 외부 통합 테스트 간에 로직을 공유하세요.
  MockMvc는 서블릿 컨테이너에 의존하지 않는 템플릿 기술(예: Thymeleaf, FreeMarker 등)과 함께 작동하지만 서블릿 컨테이너에 의존하기 때문에 JSP에서는 작동하지 않습니다

 

Why HtmlUnit Integration?

가장 먼저 떠오르는 질문은 "이것이 왜 필요한가?"입니다 아주 기본적인 샘플 애플리케이션을 살펴봄으로써 그 답을 찾을 수 있습니다. 메시지 객체에 대한 CRUD 작업을 지원하는 Spring MVC 웹 애플리케이션이 있다고 가정해 보겠습니다. 이 애플리케이션은 모든 메시지에 대한 페이징도 지원합니다. 이를 어떻게 테스트할까요?

Spring MVC Test를 사용하면 다음과 같이 메시지를 생성할 수 있는지 쉽게 테스트할 수 있습니다:

MockHttpServletRequestBuilder createMessage = post("/messages/")
		.param("summary", "Spring Rocks")
		.param("text", "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
		.andExpect(status().is3xxRedirection())
		.andExpect(redirectedUrl("/messages/123"));

메시지를 생성할 수 있는 폼 보기를 테스트하려면 어떻게 해야 할까요? 예를 들어 양식이 다음 코드 조각과 같다고 가정해 봅시다:

<form id="messageForm" action="/messages/" method="post">
	<div class="pull-right"><a href="/messages/">Messages</a></div>

	<label for="summary">Summary</label>
	<input type="text" class="required" id="summary" name="summary" value="" />

	<label for="text">Message</label>
	<textarea id="text" name="text"></textarea>

	<div class="form-actions">
		<input type="submit" value="Create" />
	</div>
</form>

양식이 새 메시지 생성을 위한 올바른 요청을 생성하도록 하려면 어떻게 해야 할까요? 순진한 시도는 다음과 비슷할 수 있습니다:

mockMvc.perform(get("/messages/form"))
		.andExpect(xpath("//input[@name='summary']").exists())
		.andExpect(xpath("//textarea[@name='text']").exists());

이 테스트에는 몇 가지 명백한 단점이 있습니다. 텍스트 대신 매개변수메시지를 사용하도록 컨트롤러를 업데이트하면 HTML 양식이 컨트롤러와 동기화되지 않더라도 양식 테스트는 계속 통과합니다. 이 문제를 해결하기 위해 다음과 같이 두 테스트를 결합할 수 있습니다:

String summaryParamName = "summary";
String textParamName = "text";
mockMvc.perform(get("/messages/form"))
		.andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
		.andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());

MockHttpServletRequestBuilder createMessage = post("/messages/")
		.param(summaryParamName, "Spring Rocks")
		.param(textParamName, "In case you didn't know, Spring Rocks!");

mockMvc.perform(createMessage)
		.andExpect(status().is3xxRedirection())
		.andExpect(redirectedUrl("/messages/123"));

이렇게 하면 테스트가 잘못 통과할 위험이 줄어들지만 여전히 몇 가지 문제가 있습니다:

  • 페이지에 여러 개의 양식이 있는 경우 어떻게 해야 할까요? 물론 XPath 표현식을 업데이트할 수도 있지만 더 많은 요소를 고려할수록 더 복잡해집니다: 필드가 올바른 유형인가? 필드가 활성화되어 있는가? 등등.
  • 또 다른 문제는 예상보다 두 배의 작업을 수행해야 한다는 것입니다. 먼저 뷰를 확인한 다음 방금 확인한 것과 동일한 매개 변수를 사용하여 뷰를 제출해야 합니다. 이상적으로는 이 모든 작업을 한 번에 수행할 수 있습니다.
  • 마지막으로, 여전히 고려할 수 없는 몇 가지 사항이 있습니다. 예를 들어 양식에 테스트하려는 JavaScript 유효성 검사도 있는 경우 어떻게 해야 할까요?

전반적인 문제는 웹 페이지 테스트에는 단일 상호작용이 아니라 사용자가 웹 페이지와 상호작용하는 방식과 해당 웹 페이지가 다른 리소스와 상호작용하는 방식의 조합이라는 점입니다. 예를 들어 양식 보기의 결과는 메시지를 작성하기 위한 사용자의 입력으로 사용됩니다. 또한 양식 보기는 JavaScript 유효성 검사와 같이 페이지의 동작에 영향을 미치는 추가 리소스를 사용할 수 있습니다.

Integration Testing to the Rescue?

앞서 언급한 문제를 해결하기 위해 엔드투엔드 통합 테스트를 수행할 수 있지만 여기에는 몇 가지 단점이 있습니다. 메시지를 페이지로 이동할 수 있는 보기를 테스트해 보세요. 다음과 같은 테스트가 필요할 수 있습니다:

  • 메시지가 비어 있을 때 사용할 수 있는 결과가 없음을 알리는 알림이 사용자에게 표시되는가?
  • 페이지에 단일 메시지가 제대로 표시되나요?
  • 페이지가 페이징을 제대로 지원하는가?

이러한 테스트를 설정하려면 데이터베이스에 적절한 메시지가 포함되어 있는지 확인해야 합니다. 이로 인해 여러 가지 추가적인 문제가 발생합니다:

  • 데이터베이스에 적절한 메시지가 있는지 확인하는 것은 지루할 수 있습니다. (외래 키 제약 조건을 고려하세요.)
  • 테스트할 때마다 데이터베이스가 올바른 상태인지 확인해야 하므로 테스트 속도가 느려질 수 있습니다.
  • 데이터베이스가 특정 상태여야 하므로 테스트를 병렬로 실행할 수 없습니다.
  • 자동 생성된 ID, 타임스탬프 등의 항목에 대한 어설션을 수행하는 것은 어려울 수 있습니다.

이러한 어려움이 있다고 해서 엔드투엔드 통합 테스트를 완전히 포기해야 한다는 의미는 아닙니다. 대신 훨씬 빠르고 안정적이며 부작용 없이 실행되는 모의 서비스를 사용하도록 세부 테스트를 리팩토링하여 엔드투엔드 통합 테스트 횟수를 줄일 수 있습니다. 그런 다음 간단한 워크플로를 검증하는 소수의 실제 엔드투엔드 통합 테스트를 구현하여 모든 것이 제대로 함께 작동하는지 확인할 수 있습니다.

Enter HtmlUnit Integration

그렇다면 어떻게 하면 페이지의 상호 작용을 테스트하는 동시에 테스트 스위트 내에서 좋은 성능을 유지할 수 있을까요? 답은 "MockMvc와 HtmlUnit을 통합하는 것"입니다

HtmlUnit Integration Options

MockMvc를 HtmlUnit과 통합하려는 경우 여러 가지 옵션이 있습니다:

  • MockMvc 및 HtmlUnit: 원시 HtmlUnit 라이브러리를 사용하려는 경우 이 옵션을 사용합니다.
  • MockMvc 및 WebDriver: 통합과 엔드투엔드 테스트 간에 코드를 쉽게 개발하고 재사용하려면 이 옵션을 사용합니다.
  • MockMvc 및 Geb: Groovy를 테스트에 사용하고, 개발을 쉽게 하며, 통합과 엔드투엔드 테스트 간에 코드를 재사용하려면 이 옵션을 사용하세요.

 

MockMvc and HtmlUnit

이 섹션에서는 MockMvc와 HtmlUnit을 통합하는 방법에 대해 설명합니다. 원시 HtmlUnit 라이브러리를 사용하려면 이 옵션을 사용합니다.

MockMvc and HtmlUnit Setup

먼저,net.sourceforge.htmlunit:htmlunit에 테스트 종속성을 포함했는지 확인하세요. Apache HttpComponents 4.5 이상에서 HtmlUnit을 사용하려면 HtmlUnit 2.18 이상을 사용해야 합니다.

다음과 같이MockMvcWebClientBuilder를 사용하여 MockMvc와 통합되는 HtmlUnit WebClient를 쉽게 만들 수 있습니다:

WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
	webClient = MockMvcWebClientBuilder
			.webAppContextSetup(context)
			.build();
}
  다음은 MockMvcWebClientBuilder를 사용하는 간단한 예제입니다. 고급 사용법은 고급 MockMvcWebClientBuilder를 참조하세요

이렇게 하면 로컬호스트를 서버로 참조하는 모든 URL이 실제 HTTP 연결 없이도MockMvc 인스턴스로 연결됩니다. 다른 URL은 평소와 같이 네트워크 연결을 사용하여 요청합니다. 이를 통해 CDN 사용을 쉽게 테스트할 수 있습니다.

MockMvc and HtmlUnit Usage

이제 서블릿 컨테이너에 애플리케이션을 배포할 필요 없이 평소와 같이 HtmlUnit을 사용할 수 있습니다. 예를 들어 뷰에 다음과 같은 메시지를 생성하도록 요청할 수 있습니다:

HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
  기본 컨텍스트 경로는 ""입니다. 또는 고급 MockMvcWebClientBuilder에 설명된 대로 컨텍스트 경로를 지정할 수 있습니다

HtmlPage에 대한 참조가 있으면 다음 예제에서와 같이 양식을 작성하고 제출하여 메시지를 만들 수 있습니다:

HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();

마지막으로 새 메시지가 성공적으로 생성되었는지 확인할 수 있습니다. 다음 어설션은 AssertJ 라이브러리를 사용합니다:

assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");

앞의 코드는 여러 가지 면에서MockMvc 테스트를 개선합니다. 첫째, 더 이상 양식을 명시적으로 확인한 다음 양식과 유사한 요청을 생성할 필요가 없습니다. 대신 양식을 요청하여 작성하고 제출하면 되므로 오버헤드가 크게 줄어듭니다.

또 다른 중요한 요소는 HtmlUnit이 Mozilla Rhino 엔진을 사용하여 JavaScript를 평가한다는 점입니다. 즉, 페이지 내에서 자바스크립트의 동작을 테스트할 수도 있습니다.

HtmlUnit 사용에 대한 자세한 내용은 HtmlUnit 설명서를 참조하세요.

Advanced MockMvcWebClientBuilder

지금까지의 예제에서는 스프링 테스트 컨텍스트 프레임워크에서 로드한 웹 애플리케이션 컨텍스트를 기반으로 웹 클라이언트를 빌드하여 가능한 가장 간단한 방법으로 MockMvcWebClientBuilder를 사용했습니다. 이 접근 방식은 다음 예제에서 반복됩니다:

WebClient webClient;

@BeforeEach
void setup(WebApplicationContext context) {
	webClient = MockMvcWebClientBuilder
			.webAppContextSetup(context)
			.build();
}

다음 예제에서 볼 수 있듯이 추가 구성 옵션을 지정할 수도 있습니다:

WebClient webClient;

@BeforeEach
void setup() {
	webClient = MockMvcWebClientBuilder
		// demonstrates applying a MockMvcConfigurer (Spring Security)
		.webAppContextSetup(context, springSecurity())
		// for illustration only - defaults to ""
		.contextPath("")
		// By default MockMvc is used for localhost only;
		// the following will use MockMvc for example.com and example.org as well
		.useMockMvcForHosts("example.com","example.org")
		.build();
}

또는 다음과 같이 MockMvc인스턴스를 별도로 구성하고 MockMvcWebClientBuilder에 제공하여 똑같은 설정을 수행할 수 있습니다:

MockMvc mockMvc = MockMvcBuilders
		.webAppContextSetup(context)
		.apply(springSecurity())
		.build();

webClient = MockMvcWebClientBuilder
		.mockMvcSetup(mockMvc)
		// for illustration only - defaults to ""
		.contextPath("")
		// By default MockMvc is used for localhost only;
		// the following will use MockMvc for example.com and example.org as well
		.useMockMvcForHosts("example.com","example.org")
		.build();

이 방법은 더 장황하지만, MockMvc 인스턴스로 WebClient를 빌드하면 MockMvc의 모든 기능을 손쉽게 사용할 수 있습니다.

  MockMvc 인스턴스 생성에 대한 자세한 내용은설정 선택 사항을 참조하세요

 

MockMvc and WebDriver

이전 섹션에서는 MockMvc를 원시 HtmlUnit API와 함께 사용하는 방법을 살펴봤습니다. 이 섹션에서는 작업을 더욱 쉽게 하기 위해 SeleniumWebDriver 내에서 추가 추상화를 사용합니다.

Why WebDriver and MockMvc?

이미 HtmlUnit과 MockMvc를 사용할 수 있는데 왜 WebDriver를 사용해야 할까요? 셀레늄 웹드라이버는 코드를 쉽게 구성할 수 있는 매우 우아한 API를 제공합니다. 어떻게 작동하는지 더 잘 보여드리기 위해 이 섹션에서 예제를 살펴보겠습니다.

  웹드라이버는 셀레늄의 일부이긴 하지만 테스트를 실행하는 데 셀레늄 서버가 필요하지 않습니다

메시지가 제대로 생성되었는지 확인해야 한다고 가정해 보겠습니다. 테스트에는 HTML 양식 입력 요소를 찾아서 채우고 다양한 어설션을 만드는 작업이 포함됩니다.

이 접근 방식에서는 오류 조건도 테스트해야 하므로 수많은 개별 테스트가 필요합니다. 예를 들어, 양식의 일부만 입력할 경우 오류가 발생하는지 확인하고자 합니다. 전체 양식을 작성하면 나중에 새로 생성된 메시지가 표시되어야 합니다.

필드 중 하나의 이름이 "요약"이라면 다음과 유사한 내용이 테스트 내 여러 곳에서 반복될 수 있습니다:

HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);

그렇다면 ID를 smmry로 변경하면 어떻게 될까요? 그렇게 하면 이 변경 사항을 반영하도록 모든 테스트를 업데이트해야 합니다. 이는 DRY 원칙에 위배되므로 다음과 같이 이 코드를 자체 메서드로 추출하는 것이 이상적입니다:

public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
	setSummary(currentPage, summary);
	// ...
}

public void setSummary(HtmlPage currentPage, String summary) {
	HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
	summaryInput.setValueAttribute(summary);
}

이렇게 하면 UI를 변경할 때 모든 테스트를 업데이트할 필요가 없습니다.

한 단계 더 나아가 다음 예제에서 볼 수 있듯이 이 로직을 현재 있는 HtmlPage를 나타내는 객체 내에 배치할 수도 있습니다:

public class CreateMessagePage {

	final HtmlPage currentPage;

	final HtmlTextInput summaryInput;

	final HtmlSubmitInput submit;

	public CreateMessagePage(HtmlPage currentPage) {
		this.currentPage = currentPage;
		this.summaryInput = currentPage.getHtmlElementById("summary");
		this.submit = currentPage.getHtmlElementById("submit");
	}

	public <T> T createMessage(String summary, String text) throws Exception {
		setSummary(summary);

		HtmlPage result = submit.click();
		boolean error = CreateMessagePage.at(result);

		return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
	}

	public void setSummary(String summary) throws Exception {
		summaryInput.setValueAttribute(summary);
	}

	public static boolean at(HtmlPage page) {
		return "Create Message".equals(page.getTitleText());
	}
}

이전에는 이 패턴을페이지 객체 패턴이라고 불렀습니다. 이전에는 이 패턴을 페이지 오브젝트 패턴이라고 불렀습니다. 이전에는 이 패턴을 페이지 오브젝트 패턴이라고 불렀지만, 웹 드라이버는 다음 섹션에서 살펴볼 몇 가지 도구를 제공하여 이 패턴을 훨씬 쉽게 구현할 수 있습니다.

MockMvc and WebDriver Setup

Spring MVC 테스트 프레임워크와 함께 셀레늄 웹드라이버를 사용하려면 프로젝트에 org.seleniumhq.selenium:selenium-htmlunit-driver에 대한 테스트 의존성이 포함되어 있는지 확인하세요.

다음 예제에서 볼 수 있듯이MockMvcHtmlUnitDriverBuilder를 사용하여 MockMvc와 통합되는 셀레늄 웹 드라이버를 쉽게 만들 수 있습니다:

WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
	driver = MockMvcHtmlUnitDriverBuilder
			.webAppContextSetup(context)
			.build();
}
  이것은 MockMvcHtmlUnitDriverBuilder를 사용하는 간단한 예제입니다. 보다 고급 사용법은 고급 MockMvcHtmlUnitDriverBuilder를 참조하세요

앞의 예제에서는 실제 HTTP 연결 없이도 localhost를 서버로 참조하는 모든 URL이 MockMvc 인스턴스로 연결되도록 합니다. 다른 URL은 평소와 같이 네트워크 연결을 사용하여 요청합니다. 이를 통해 CDN 사용을 쉽게 테스트할 수 있습니다.

MockMvc and WebDriver Usage

이제 서블릿 컨테이너에 애플리케이션을 배포할 필요 없이 평소와 같이 WebDriver를 사용할 수 있습니다. 예를 들어 다음과 같은 메시지를 생성하도록 뷰에 요청할 수 있습니다:

CreateMessagePage page = CreateMessagePage.to(driver);

그런 다음 다음과 같이 양식을 작성하고 제출하여 메시지를 생성할 수 있습니다:

ViewMessagePage viewMessagePage =
		page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);

이렇게 하면 페이지 객체 패턴을 활용하여 HtmlUnit 테스트의디자인을 개선할 수 있습니다.왜 웹드라이버와 MockMvc인가? 에서 언급했듯이 HtmlUnit에서도 페이지 객체 패턴을 사용할 수 있지만, 웹드라이버를 사용하면 훨씬 더 쉽습니다. 다음CreateMessagePage 구현을 고려해 보세요:

public class CreateMessagePage extends AbstractPage { 

	
	private WebElement summary;
	private WebElement text;

	@FindBy(css = "input[type=submit]") 
	private WebElement submit;

	public CreateMessagePage(WebDriver driver) {
		super(driver);
	}

	public <T> T createMessage(Class<T> resultPage, String summary, String details) {
		this.summary.sendKeys(summary);
		this.text.sendKeys(details);
		this.submit.click();
		return PageFactory.initElements(driver, resultPage);
	}

	public static CreateMessagePage to(WebDriver driver) {
		driver.get("http://localhost:9990/mail/messages/form");
		return PageFactory.initElements(driver, CreateMessagePage.class);
	}
}
  CreateMessagePage는 AbstractPage를 확장합니다.AbstractPage에 대한 자세한 내용은 다루지 않지만 요약하면 모든 페이지에 대한 공통 기능을 포함합니다. 예를 들어 애플리케이션에 탐색 표시줄, 전역 오류 메시지 및 기타 기능이 있는 경우 이 로직을 공유 위치에 배치할 수 있습니다.
  관심 있는 HTML 페이지의 각 부분에 대한 멤버 변수가 있습니다. 이러한 변수는 WebElement 유형입니다. 웹드라이버의페이지팩토리는  웹엘리먼트를 자동으로 확인하여 CreateMessagePage의 HtmlUnit 버전에서 많은 코드를 제거할 수 있게 해줍니다.PageFactory#initElements(WebDriver,Class<T>)메서드는 필드 이름을 사용하고 HTML 페이지 내의 요소의 ID 또는 이름으로 조회하여 각 웹 엘리 먼트를 자동으로 확인합니다.
  FindBy 어노테이션을사용하여 기본 조회 동작을 재정의할 수 있습니다. 이 예에서는 @FindBy어노테이션을 사용하여 css 선택기(입력[type=submit])로 제출 버튼을 조회하는 방법을 보여 줍니다.

마지막으로 새 메시지가 성공적으로 생성되었는지 확인할 수 있습니다. 다음 어설션은 AssertJ 어설션 라이브러리를 사용합니다:

assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");

뷰메시지 페이지를 통해 사용자 정의 도메인 모델과 상호 작용할 수 있음을 알 수 있습니다. 예를 들어, 메시지 객체를 반환하는 메서드를 노출합니다:

public Message getMessage() throws ParseException {
	Message message = new Message();
	message.setId(getId());
	message.setCreated(getCreated());
	message.setSummary(getSummary());
	message.setText(getText());
	return message;
}

그러면 어설션에서 리치 도메인 객체를 사용할 수 있습니다.

마지막으로, 테스트가 완료되면 다음과 같이 WebDriver 인스턴스를 닫는 것을 잊지 말아야 합니다:

@AfterEach
void destroy() {
	if (driver != null) {
		driver.close();
	}
}

WebDriver 사용에 대한 자세한 내용은 셀레늄웹 드라이버 설명서를 참조하세요.

Advanced MockMvcHtmlUnitDriverBuilder

지금까지의 예제에서는 스프링 테스트 컨텍스트 프레임워크에서 로드한 웹 애플리케이션 컨텍스트를 기반으로 웹 드라이버를 빌드하여 가능한 가장 간단한 방법으로 MockMvcHtmlUnitDriverBuilder를 사용했습니다. 이 접근 방식은 여기에서도 다음과 같이 반복됩니다:

WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
	driver = MockMvcHtmlUnitDriverBuilder
			.webAppContextSetup(context)
			.build();
}

다음과 같이 추가 구성 옵션을 지정할 수도 있습니다:

WebDriver driver;

@BeforeEach
void setup() {
	driver = MockMvcHtmlUnitDriverBuilder
			// demonstrates applying a MockMvcConfigurer (Spring Security)
			.webAppContextSetup(context, springSecurity())
			// for illustration only - defaults to ""
			.contextPath("")
			// By default MockMvc is used for localhost only;
			// the following will use MockMvc for example.com and example.org as well
			.useMockMvcForHosts("example.com","example.org")
			.build();
}

또는 다음과 같이 MockMvc인스턴스를 별도로 구성하고 이를 MockMvcHtmlUnitDriverBuilder에 제공하여 똑같은 설정을 수행할 수 있습니다:

MockMvc mockMvc = MockMvcBuilders
		.webAppContextSetup(context)
		.apply(springSecurity())
		.build();

driver = MockMvcHtmlUnitDriverBuilder
		.mockMvcSetup(mockMvc)
		// for illustration only - defaults to ""
		.contextPath("")
		// By default MockMvc is used for localhost only;
		// the following will use MockMvc for example.com and example.org as well
		.useMockMvcForHosts("example.com","example.org")
		.build();

좀 더 장황하지만, MockMvc 인스턴스로 WebDriver를 빌드하면 MockMvc의 모든 기능을 손쉽게 사용할 수 있습니다.

  MockMvc 인스턴스 생성에 대한 자세한 내용은설정 선택 사항을 참조하세요

 

MockMvc and Geb

이전 섹션에서는 WebDriver와 함께 MockMvc를 사용하는 방법을 살펴봤습니다. 이 섹션에서는 Geb을 사용하여 테스트를 더욱 그루비하게 만들겠습니다.

Why Geb and MockMvc?

Geb는 WebDriver의 지원을 받으므로 WebDriver에서 제공하는 것과동일한 많은이점을 제공합니다. 하지만 Geb은 상용구 코드 일부를 처리하여 작업을 더욱 쉽게 만들어 줍니다.

MockMvc and Geb Setup

다음과 같이 MockMvc를 사용하는 셀레늄 웹드라이버로 Geb 브라우저를 쉽게 초기화할 수 있습니다:

def setup() {
	browser.driver = MockMvcHtmlUnitDriverBuilder
		.webAppContextSetup(context)
		.build()
}
  다음은 MockMvcHtmlUnitDriverBuilder를 사용하는 간단한 예제입니다. 보다 고급 사용법은 고급 MockMvcHtmlUnitDriverBuilder를 참조하세요

이렇게 하면 로컬호스트를 서버로 참조하는 모든 URL이 실제 HTTP 연결 없이도MockMvc 인스턴스로 연결됩니다. 다른 모든 URL은 정상적으로 네트워크 연결을 사용하여 요청됩니다. 이를 통해 CDN 사용을 쉽게 테스트할 수 있습니다.

MockMvc and Geb Usage

이제 서블릿 컨테이너에 애플리케이션을 배포할 필요 없이 평소와 같이 Geb을 사용할 수 있습니다. 예를 들어 다음과 같은 메시지를 생성하도록 뷰에 요청할 수 있습니다:

to CreateMessagePage

그런 다음 다음과 같이 양식을 작성하고 제출하여 메시지를 생성할 수 있습니다:

when:
form.summary = expectedSummary
form.text = expectedMessage
submit.click(ViewMessagePage)

인식할 수 없는 메서드 호출이나 속성 액세스 또는 참조가 발견되면 현재 페이지 객체로 전달됩니다. 이렇게 하면 WebDriver를 직접 사용할 때 필요했던 상용구 코드가 많이 제거됩니다.

웹드라이버를 직접 사용할 때와 마찬가지로 페이지 객체 패턴을 사용하면HtmlUnit 테스트의 디자인이 개선됩니다. 앞서 언급했듯이 페이지 객체 패턴은 HtmlUnit과 WebDriver에서 사용할 수 있지만, Geb을 사용하면 훨씬 더 쉽습니다. 새로운 Groovy 기반CreateMessagePage 구현을 살펴봅시다:

class CreateMessagePage extends Page {
	static url = 'messages/form'
	static at = { assert title == 'Messages : Create'; true }
	static content =  {
		submit { $('input[type=submit]') }
		form { $('form') }
		errors(required:false) { $('label.error, .alert-error')?.text() }
	}
}

CreateMessagePage는 Page를 확장합니다. Page에 대한 자세한 내용은 다루지 않겠지만 요약하자면 모든 페이지에 공통된 기능을 포함하고 있습니다. 이 페이지를 찾을 수 있는 URL을 정의합니다. 이를 통해 다음과 같이 페이지로 이동할 수 있습니다:

to CreateMessagePage

또한 지정된 페이지에 있는지 여부를 판단하는 at closure도 있습니다. 올바른 페이지에 있는 경우 참을 반환해야 합니다. 따라서 다음과 같이 올바른 페이지에 있다고 어설션할 수 있습니다:

then:
at CreateMessagePage
errors.contains('This field is required.')
  클로저에 어설션을 사용하면 잘못된 페이지에 있는 경우 어디에서 문제가 발생했는지 확인할 수 있습니다

다음으로 페이지 내의 모든 관심 영역을 지정하는 콘텐츠 클로저를 만듭니다.JQuery와 유사한 내비게이터 API를 사용하여 관심 있는 콘텐츠를 선택할 수 있습니다.

마지막으로 다음과 같이 새 메시지가 성공적으로 생성되었는지 확인할 수 있습니다:

then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage

Geb을 최대한 활용하는 방법에 대한 자세한 내용은Geb 사용 설명서를 참조하세요.

 

Testing Client Applications

클라이언트 측 테스트를 사용하여 내부적으로 RestTemplate을 사용하는 코드를 테스트할 수 있습니다. 이 아이디어는 예상 요청을 선언하고 "스텁" 응답을 제공하여 서버를 실행하지 않고 코드를 단독으로 테스트하는 데 집중할 수 있도록 하는 것입니다. 다음 예제는 이를 수행하는 방법을 보여줍니다:

RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());

// Test code that uses the above RestTemplate ...

mockServer.verify();

앞의 예제에서, 클라이언트 측 REST 테스트를 위한 중앙 클래스인 MockRestServiceServer는 예상에 대해 실제 요청을 어설트하고 "스텁" 응답을 반환하는 사용자 지정 ClientHttpRequestFactory로 RestTemplate을 구성합니다. 이 경우 /greeting에 대한 요청이 예상되며텍스트/일반 콘텐츠가 포함된 200 응답을 반환하려고 합니다. 필요에 따라 추가 예상 요청 및 스텁 응답을 정의할 수 있습니다. 예상 요청과 스텁 응답을 정의하면 평소처럼 클라이언트 측 코드에서 RestTemplate을 사용할 수 있습니다. 테스트가 끝나면 mockServer.verify() 를 사용하여 모든 예상이 충족되었는지 확인할 수 있습니다.

기본적으로 요청은 기대치가 선언된 순서대로 예상됩니다. 서버를 빌드할 때 무시ExpectOrder 옵션을 설정하면 모든 기대치를 순서대로 확인하여 주어진 요청과 일치하는 것을 찾을 수 있습니다. 즉, 요청은 어떤 순서로든 들어올 수 있습니다. 다음 예제에서는 무시ExpectOrder를 사용합니다:

server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();

기본적으로 순서가 지정되지 않은 요청이 있더라도 각 요청은 한 번만 실행할 수 있습니다. expect 메서드는 카운트 범위(예: once, manyTimes, max, min,between 등)를 지정하는 ExpectedCount인수를 허용하는 오버로드된 변형을 제공합니다. 다음 예제에서는 times를 사용합니다:

RestTemplate restTemplate = new RestTemplate();

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess());
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess());

// ...

mockServer.verify();

무시ExpectOrder가 설정되어 있지 않아(기본값) 선언 순서대로 요청이 예상되는 경우 해당 순서는 예상되는 요청 중 첫 번째 요청에만 적용된다는 점에 유의하세요. 예를 들어 "/something"이 두 번, "/somewhere"가 세 번 예상되는 경우 "/something"에 대한 요청이 있어야 "/somewhere"에 대한 요청이 있을 수 있지만 그 이후의 "/something" 및 "/somewhere"를 제외하고는 언제든지 요청이 올 수 있습니다.

위의 모든 것에 대한 대안으로 클라이언트 측 테스트 지원은 RestTemplate에 구성하여 MockMvc 인스턴스에 바인딩할 수 있는ClientHttpRequestFactory 구현도 제공합니다. 이를 통해 서버를 실행하지 않고도 실제 서버 측 로직을 사용하여 요청을 처리할 수 있습니다. 다음 예제는 이를 수행하는 방법을 보여줍니다:

MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));

// Test code that uses the above RestTemplate ...

경우에 따라 응답을 모방하는 대신 원격 서비스에 대한 실제 호출을 수행해야 할 수도 있습니다. 다음 예제는ExecutingResponseCreator를 통해 이를 수행하는 방법을 보여줍니다:

RestTemplate restTemplate = new RestTemplate();

// Create ExecutingResponseCreator with the original request factory
ExecutingResponseCreator withActualResponse = new ExecutingResponseCreator(restTemplate.getRequestFactory());

MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
mockServer.expect(requestTo("/profile")).andRespond(withSuccess());
mockServer.expect(requestTo("/quoteOfTheDay")).andRespond(withActualResponse);

// Test code that uses the above RestTemplate ...

mockServer.verify();

앞의 예제에서는 MockRestServiceServer가 응답을 모의하는 다른 것으로 대체하기 전에 RestTemplate에서ClientHttpRequestFactory를 사용하여 ExecutingResponseCreator를 생성합니다. 그런 다음 두 종류의 응답으로 기대치를 정의합니다:

  • 프로필 엔드포인트에 대한 스텁 200 응답(실제 요청은 실행되지 않음)
  • quoteOfTheDay 엔드포인트에 대한 호출을 통해 얻은 응답

두 번째 경우, 요청은 앞서 캡처한 ClientHttpRequestFactory를 통해 실행됩니다. 이렇게 하면 RestTemplate이 원래 구성된 방식에 따라 실제 원격 서버에서 올 수 있는 응답이 생성됩니다.

Static Imports

서버 측 테스트와 마찬가지로 클라이언트 측 테스트를 위한 유창한 API를 사용하려면 몇 가지 정적 임포트가 필요합니다. MockRest*를 검색하면 쉽게 찾을 수 있습니다. Eclipse 사용자는 Java → 에디터 → 콘텐츠 지원 → 즐겨찾기에 있는 Eclipse 환경설정에서MockRestRequestMatchers.*  MockRestResponseCreators.*를 '즐겨찾는 정적 멤버'로 추가해야 합니다. 이렇게 하면 정적 메서드 이름의 첫 글자를 입력한 후 콘텐츠 지원을 사용할 수 있습니다. 다른 IDE(예: IntelliJ)에서는 추가 구성이 필요하지 않을 수 있습니다. 정적 멤버에 대한 코드 완성 지원 여부를 확인하세요.

Further Examples of Client-side REST Tests

Spring MVC 테스트의 자체 테스트에는 클라이언트 측 REST 테스트의예시 테스트 가 포함되어 있습니다.

 

부록

섹션 요약

 

어노테이션

이 섹션에서는 Spring 애플리케이션을 테스트할 때 사용할 수 있는 어노테이션에 대해 설명합니다.

 

표준 어노테이션 지원

다음 어노테이션은 스프링 테스트 컨텍스트 프레임워크의 모든 구성에 대해 표준 시맨틱으로 지원됩니다. 이러한 어노테이션은 테스트에만 국한되지 않으며 Spring 프레임워크의 모든 곳에서 사용할 수 있습니다.

  • @Autowired
  • @Qualifier
  • @Value
  • jSR-250이 있는 경우@Resource (jakarta.annotation)
  • jSR-250이 있는 경우@ManagedBean (jakarta.annotation)
  • jSR-330이 있는 경우@Inject (jakarta.inject)
  • jSR-330이 있는 경우@Named (jakarta.inject)
  • jPA가 있는 경우@PersistenceContext (jakarta.persistence)
  • jPA가 있는 경우@PersistenceUnit (jakarta.persistence)
  • 제한된 어트리뷰트를 지원하는@Transactional (org.springframework.transaction.annotation)
 
JSR-250 라이프사이클 어노테이션
Spring 테스트 컨텍스트 프레임워크에서 @PostConstruct  @PreDestroy를 ApplicationContext에 구성된 모든 애플리케이션 구성 요소에 표준 의미와 함께 사용할 수 있습니다. 그러나 이러한 수명 주기 어노테이션은 실제 테스트 클래스 내에서 사용이 제한됩니다.
테스트 클래스 내의 메서드에 @PostConstruct 어노테이션이 있는 경우, 해당 메서드는 기본 테스트 프레임워크의 모든 전 메서드(예: JUnit Jupiter의 @BeforeEach 어노테이션이 있는 메서드) 전에 실행되며, 이는 테스트 클래스의 모든 테스트 메서드에 적용됩니다. 반면에 테스트 클래스 내의 메서드에@PreDestroy가 어노테이션된 경우 해당 메서드는 절대 실행되지 않습니다. 따라서 테스트 클래스 내에서는@PostConstruct  @PreDestroy 대신 기본 테스트 프레임워크의 테스트 수명 주기 콜백을 사용하는 것이 좋습니다.

 

스프링 테스트 어노테이션 (하위 섹션 번역 안함)

Spring 프레임워크는 TestContext 프레임워크와 함께 유닛 및 통합 테스트에서 사용할 수 있는 다음과 같은 Spring 관련 어노테이션 세트를 제공합니다. 기본 속성 값, 속성 별칭 및 기타 세부 정보를 포함한 자세한 내용은 해당 javadoc을 참조하세요.

Spring의 테스트 어노테이션에는 다음이 포함됩니다:


Spring JUnit 4 Testing Annotations

다음 어노테이션은SpringRunner, Spring의 JUnit 4 규칙, 또는 Spring의 JUnit 4 지원 클래스와 함께 사용할 때만 지원됩니다:

@IfProfileValue

ifProfileValue는 특정 테스트 환경에 대해 주석이 달린 테스트가 활성화되어 있음을 나타냅니다. 구성된 ProfileValueSource가 제공된 이름과 일치하는 값을 반환하면 테스트가 활성화됩니다. 그렇지 않으면 테스트가 비활성화되고 사실상 무시됩니다.

클래스 수준, 메서드 수준 또는 둘 다에서 @IfProfileValue를 적용할 수 있습니다. 클래스 수준에서 @IfProfileValue를 사용하면 해당 클래스 또는 그 하위 클래스 내의 모든 메서드에 대해 메서드 수준보다 우선적으로 사용됩니다. 즉, 클래스 수준과 메서드 수준 모두에서 활성화된 경우 테스트가 활성화됩니다. IfProfileValue가없다는 것은 테스트가 암시적으로 활성화되어 있음을 의미합니다. 이는 @Ignore가 있으면 항상 테스트가 비활성화된다는 점을 제외하면 JUnit 4의@Ignore 어노테이션의 의미와 유사합니다.

다음 예제는 @IfProfileValue 어노테이션이 있는 테스트를 보여줍니다:

@IfProfileValue(name="java.vendor", value="Oracle Corporation") 
@Test
public void testProcessWhichRunsOnlyOnOracleJvm() {
	// some logic that should run only on Java VMs from Oracle Corporation
}
  Java 공급업체가 "Oracle Corporation"인 경우에만 이 테스트를 실행합니다.

또는 JUnit 4 환경에서 테스트 그룹에 대한 TestNG와 유사한 지원을 얻기 위해  목록( OR의미 포함)으로 @IfProfileValue를 구성할 수 있습니다. 다음 예를 고려하십시오:

@IfProfileValue(name="test-groups", values={"unit-tests", "integration-tests"}) 
@Test
public void testProcessWhichRunsForUnitOrIntegrationTestGroups() {
	// some logic that should run only for unit and integration test groups
}
  단위 테스트 및 통합 테스트에 대해 이 테스트를 실행합니다.

@ProfileValueSourceConfiguration

프로필 값 소스 구성은 클래스 수준 어노테이션으로,@IfProfileValue 어노테이션을 통해 구성된 프로필 값을 검색할 때 사용할 프로필 값 소스 유형을 지정합니다. 테스트에 대해 @ProfileValueSourceConfiguration이 선언되지 않은 경우 기본적으로 SystemProfileValueSource가 사용됩니다. 다음 예제는 @ProfileValueSourceConfiguration을 사용하는 방법을 보여줍니다:

@ProfileValueSourceConfiguration(CustomProfileValueSource.class) 
public class CustomProfileValueSourceTests {
	// class body...
}
  사용자 지정 프로필 값 소스를 사용합니다.

@Timed

timed는 주석이 달린 테스트 메서드가 지정된 시간(밀리초) 내에 실행을 완료해야 함을 나타냅니다. 텍스트 실행 시간이 지정된 기간을 초과하면 테스트가 실패합니다.

이 기간에는 테스트 메서드 자체 실행, 테스트 반복(@Repeat 참조), 테스트 픽스처의 설정 또는 해제가 모두 포함됩니다. 다음 예는 이 기능을 사용하는 방법을 보여줍니다:

@Timed(millis = 1000) 
public void testProcessWithOneSecondTimeout() {
	// some logic that should not take longer than 1 second to run
}
  테스트 기간을 1초로 설정합니다.

Spring의 @Timed 어노테이션은 JUnit 4의 @Test(timeout=...)지원과는 다른 의미를 가집니다. 특히, JUnit 4가 테스트 실행 시간 초과를 처리하는 방식(즉, 별도의 스레드에서 테스트 메서드를 실행하는 방식)으로 인해 테스트가 너무 오래 걸리면 @Test(timeout=...)는 선제적으로 테스트를 실패시킵니다. 반면 Spring의 @Timed는 테스트를 선제적으로 실패시키지 않고 테스트가 완료될 때까지 기다렸다가 실패합니다.

@Repeat

반복은 주석이 달린 테스트 메서드를 반복적으로 실행해야 함을 나타냅니다. 테스트 메서드를 실행할 횟수는 어노테이션에 지정되어 있습니다.

반복할 실행 범위에는 테스트 메서드 자체의 실행뿐만 아니라 테스트 픽스처의 설정 또는 해체가 포함됩니다.SpringMethodRule과 함께 사용하는 경우, 이 범위에는 TestExecutionListener 구현에 의한 테스트 인스턴스 준비가 추가로 포함됩니다. 다음 예제는 @Repeat 어노테이션을 사용하는 방법을 보여줍니다:

@Repeat(10) 
@Test
public void testProcessRepeatedly() {
	// ...
}
  이 테스트를 10번 반복합니다.

 

Spring JUnit Jupiter Testing Annotations

다음 어노테이션은SpringExtension 및 JUnit Jupiter(즉, JUnit 5의 프로그래밍 모델)와 함께 사용할 때 지원됩니다:

@SpringJUnitConfig

springJUnitConfig는 JUnit Jupiter의@ExtendWith(SpringExtension.class )와 Spring TestContext 프레임워크의 @ContextConfiguration을 결합한 컴포넌트 어노테이션입니다. 클래스 수준에서 @ContextConfiguration의 드롭인 대체로 사용할 수 있습니다. 구성 옵션과 관련하여 @ContextConfiguration과 @SpringJUnitConfig의 유일한 차이점은 컴포넌트 클래스를 @SpringJUnitConfig의  속성으로 선언할 수 있다는 것입니다.

다음 예시는 @SpringJUnitConfig 어노테이션을 사용하여 구성 클래스를 지정하는 방법을 보여줍니다:

@SpringJUnitConfig(TestConfig.class) 
class ConfigurationClassJUnitJupiterSpringTests {
	// class body...
}
  구성 클래스를 지정합니다.

다음 예는 @SpringJUnitConfig 어노테이션을 사용하여 구성 파일의 위치를 지정하는 방법을 보여줍니다:

@SpringJUnitConfig(locations = "/test-config.xml") 
class XmlJUnitJupiterSpringTests {
	// class body...
}
  구성 파일의 위치를 지정합니다.

자세한 내용은 컨텍스트 관리와@SpringJUnitConfig @ContextConfiguration에 대한 자바독을 참조하세요.

@SpringJUnitWebConfig

springJUnitWebConfig는 JUnit Jupiter의@ExtendWith(SpringExtension.class )와 Spring TestContext 프레임워크의 @ContextConfiguration 및@WebAppConfiguration을 결합한 컴포넌트 어노테이션입니다. 클래스 수준에서 @ContextConfiguration  @WebAppConfiguration의 드롭인 대체로 사용할 수 있습니다. 구성 옵션과 관련하여 @ContextConfiguration과 @SpringJUnitWebConfig의 유일한 차이점은 @SpringJUnitWebConfig의값 속성을 사용하여 구성 요소 클래스를 선언할 수 있다는 점입니다. 또한@SpringJUnitWebConfig에서 resourcePath 속성을 사용해야만 @WebAppConfiguration의 value속성을 재정의할 수 있습니다.

다음 예제는 @SpringJUnitWebConfig 어노테이션을 사용하여 구성 클래스를 지정하는 방법을 보여줍니다:

@SpringJUnitWebConfig(TestConfig.class) 
class ConfigurationClassJUnitJupiterSpringWebTests {
	// class body...
}
  구성 클래스를 지정합니다.

다음 예는 @SpringJUnitWebConfig 어노테이션을 사용하여 구성 파일의 위치를 지정하는 방법을 보여줍니다:

@SpringJUnitWebConfig(locations = "/test-config.xml") 
class XmlJUnitJupiterSpringWebTests {
	// class body...
}
  구성 파일의 위치를 지정합니다.

자세한 내용은 컨텍스트 관리와@SpringJUnitWebConfig,@ContextConfiguration @WebAppConfiguration에대한 자바독을 참조하세요.

@TestConstructor

테스트 클래스 생성자의 매개변수가 테스트의ApplicationContext에 있는 컴포넌트로부터 자동 와이어링되는 방식을 구성하는 데 사용되는 유형 수준 어노테이션입니다.

테스트 클래스에 @TestConstructor가 없거나 메타적으로 존재하지 않으면 기본 테스트 생성자 자동 와이어 모드가 사용됩니다. 기본 모드를 변경하는 방법에 대한 자세한 내용은 아래 팁을 참조하세요. 그러나 생성자에 @Autowired,@jakarta.inject.Inject 또는 @javax.inject.Inject의 로컬 선언이 @TestConstructor 및 기본 모드보다 우선한다는 점에 유의하세요.

 
기본 테스트 생성자 자동 와이어 모드 변경하기
기본 테스트 생성자 자동 와이어 모드는spring.test.constructor.autowire.mode JVM 시스템 속성을 모두로 설정하여 변경할 수 있습니다. 또는SpringProperties 메커니즘을 통해 기본 모드를 설정할 수도 있습니다.
스프링 프레임워크 5.3부터 기본 모드는JUnit 플랫폼 구성 매개변수로도 구성할 수 있습니다.
Spring.test.constructor.autowire.mode 속성이 설정되어 있지 않으면 테스트 클래스 생성자가 자동으로 자동 와이어링되지 않습니다.
  Spring 프레임워크 5.2부터 @TestConstructor는 JUnit Jupiter와 함께 사용하기 위해 SpringExtension과 함께 사용할 때만 지원됩니다. SpringExtension은 종종 자동으로 등록되는 경우가 많습니다(예:@SpringJUnitConfig  @SpringJUnitWebConfig와 같은 어노테이션이나 Spring Boot Test의 다양한 테스트 관련 어노테이션 사용 시)

@NestedTestConfiguration

네스티드테스트구성은 내부 테스트 클래스에 대한 클래스 계층 구조 내에서 스프링 테스트 구성 어노테이션이 처리되는 방식을 구성하는 데 사용되는 타입 수준 어노테이션입니다.

테스트 클래스, 상위 유형 계층 구조 또는 둘러싸는 클래스 계층 구조에 @NestedTestConfiguration이 존재하지 않거나 메타 존재하지 않으면 기본 둘러싸는 구성 상속 모드가 사용됩니다. 기본 모드를 변경하는 방법에 대한 자세한 내용은 아래 팁을 참조하세요.

 
기본 둘러싸기 구성 상속 모드 변경하기
기본 둘러싸기 구성 상속 모드는 INHERIT이지만 spring.test.enclosing.configuration JVM 시스템 속성을OVERRIDE로 설정하여 변경할 수 있습니다. 또는SpringProperties 메커니즘을 통해 기본 모드를 설정할 수도 있습니다.

Spring 테스트 컨텍스트 프레임워크는 다음 어노테이션에 대해 @NestedTestConfiguration 시맨틱을 준수합니다.

  중첩된 테스트 클래스 구성은 일반적으로 JUnit Jupiter의 @Nested 테스트 클래스와 함께 사용할 때만 의미가 있지만, 이 어노테이션을 사용하는 Spring 및 중첩된 테스트 클래스를 지원하는 다른 테스트 프레임워크가 있을 수 있습니다

예제 및 자세한 내용은 @Nested 테스트 클래스 구성을 참조하세요.

@EnabledIf

enabledIf는 주석이 달린 JUnit Jupiter 테스트 클래스 또는 테스트 메서드가 활성화되어 있고 제공된 표현식이 참으로 평가되면 실행되어야 한다는 신호를 보내는 데 사용됩니다. 구체적으로 표현식이 대소문자 무시와 같은 Boolean. TRUE또는 String으로 평가되면 테스트가 활성화됩니다. 클래스 수준에서 적용하면 해당 클래스 내의 모든 테스트 메서드도 기본적으로 자동으로 활성화됩니다.

표현식은 다음 중 하나를 사용할 수 있습니다:

  • Spring 표현식 언어 (SpEL) 표현식. 예:@EnabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")
  • Spring 환경에서 사용할 수 있는 속성의 자리 표시자입니다. 예시: @EnabledIf("${smoke.tests.enabled}")
  • 텍스트 리터럴. 예시 @EnabledIf("true")

그러나 속성 자리 표시자의 동적 확인 결과가 아닌 텍스트 리터럴은 @EnabledIf("false ")는 @Disabled와 같고 @EnabledIf("true"는 논리적으로 의미가 없으므로 실제 가치가 없습니다.

EnabledIf를 메타 주석으로 사용하여 사용자 정의 구성된 주석을 만들 수 있습니다. 예를 들어 다음과 같이 사용자 지정 @EnabledOnMac 어노테이션을 만들 수 있습니다:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EnabledIf(
	expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
	reason = "Enabled on Mac OS"
)
public @interface EnabledOnMac {}
 
enabledOnMac은 가능한 예시일 뿐입니다. 정확한 사용 사례가 있는 경우 JUnit Jupiter에 내장된 @EnabledOnOs(MAC) 지원을 사용하세요.
 
JUnit 5.7부터는 @EnabledIf라는 조건 어노테이션도 있습니다. 따라서 Spring의 @EnabledIf 지원을 사용하려면 올바른 패키지에서 어노테이션 유형을 가져와야 합니다.

@DisabledIf

disabledIf는 주석이 달린 JUnit Jupiter 테스트 클래스 또는 테스트 메서드가 비활성화되었음을 알리는 데 사용되며, 제공된 표현식이참으로 평가되는 경우 실행해서는 안 됩니다. 구체적으로, 표현식이 대소문자 무시(대/소문자 무시)로 참과 같은 Boolean.TRUE 또는 String으로 평가되면 테스트가 비활성화됩니다. 클래스 수준에서 적용하면 해당 클래스 내의 모든 테스트 메서드도 자동으로 비활성화됩니다.

표현식은 다음 중 어느 것이든 가능합니다:

  • Spring 표현식 언어 (SpEL) 표현식. 예:@DisabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")
  • Spring 환경에서 사용할 수 있는 속성의 자리 표시자입니다. 예시: @DisabledIf("${smoke.tests.disabled}")
  • 텍스트 리터럴. 예시 @DisabledIf("true")

그러나 속성 자리 표시자의 동적 확인 결과가 아닌 텍스트 리터럴은 실제 값이 0이므로 @DisabledIf ("true "는 @Disabled와 동일하고 @DisabledIf("false"는 논리적으로 무의미하다는 점에 유의하세요.

DisabledIf를 메타 주석으로 사용하여 사용자 정의 구성된 주석을 만들 수 있습니다. 예를 들어 다음과 같이 사용자 지정 @DisabledOnMac 어노테이션을 만들 수 있습니다:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@DisabledIf(
	expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
	reason = "Disabled on Mac OS"
)
public @interface DisabledOnMac {}
 
disabledOnMac은 가능한 사용 사례의 예시일 뿐입니다. 정확한 사용 사례가 있는 경우 JUnit Jupiter에 내장된 @DisabledOnOs(MAC) 지원을 사용하세요.
 
JUnit 5.7부터는 @DisabledIf라는 조건 어노테이션도 있습니다. 따라서 Spring의 @DisabledIf 지원을 사용하려면 올바른 패키지에서 어노테이션 유형을 가져와야 합니다.

 

테스트를 위한 메타 주석 지원

대부분의 테스트 관련 주석을메타 주석 으로 사용하여 사용자 지정으로 구성된 주석을 만들고 테스트 스위트 전체에서 구성 중복을 줄일 수 있습니다.

테스트 컨텍스트 프레임워크와 함께 다음 각각을 메타 어노테이션으로 사용할 수 있습니다.

  • 부트스트랩과
  • 컨텍스트 구성
  • 컨텍스트 계층
  • 컨텍스트 커스터마이저 팩토리
  • 활성 프로파일
  • 테스트 프로퍼티 소스
  • 더티 컨텍스트
  • 웹앱 구성
  • 테스트 실행 리스너
  • 트랜잭션
  • 비포트랜잭션
  • 트랜잭션 후
  • @Commit
  • 롤백
  • @Sql
  • @SqlConfig
  • @SqlMergeMode
  • @SqlGroup
  • 반복 ( JUnit 4에서만 지원됨)
  • timed ( JUnit 4에서만 지원됨)
  • ifProfileValue (JUnit 4에서만 지원됨)
  • 프로필 값 소스 구성 (JUnit 4에서만 지원)
  • @SpringJUnitConfig (JUnit Jupiter에서만 지원)
  • springJUnitWebConfig (JUnit Jupiter에서만 지원됨)
  • 테스트 생성자(@TestConstructor ) (JUnit Jupiter에서만 지원)
  • 중첩된 테스트 구성 (JUnit Jupiter에서만 지원됨)
  • 활성화됨(@EnabledIf ) (JUnit Jupiter에서만 지원됨)
  • disabledIf ( JUnit Jupiter에서만 지원됨)

다음 예제를 살펴보겠습니다:

@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class OrderRepositoryTests { }

@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class UserRepositoryTests { }

JUnit 4 기반 테스트 스위트 전체에서 이전 구성을 반복하고 있다는 것을 발견하면 다음과 같이 Spring의 공통 테스트 구성을 중앙 집중화하는 사용자 정의 구성된 주석을 도입하여 중복을 줄일 수 있습니다:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }

그런 다음 다음과 같이 사용자 정의 @TransactionalDevTestConfig 어노테이션을 사용하여 개별 JUnit 4 기반 테스트 클래스의 구성을 단순화할 수 있습니다:

@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class OrderRepositoryTests { }

@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class UserRepositoryTests { }

JUnit Jupiter를 사용하는 테스트를 작성하는 경우 JUnit 5의 어노테이션을 메타 어노테이션으로도 사용할 수 있으므로 코드 중복을 훨씬 더 줄일 수 있습니다. 다음 예를 생각해 보세요:

@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }

@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }

JUnit Jupiter 기반 테스트 스위트 전체에서 이전 구성을 반복하고 있는 것을 발견한 경우, 다음과 같이 Spring과 JUnit Jupiter의 공통 테스트 구성을 중앙 집중화하는 사용자 정의 구성 어노테이션을 도입하여 중복을 줄일 수 있습니다:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }

그런 다음 다음과 같이 사용자 정의 @TransactionalDevTestConfig 어노테이션을 사용하여 개별 JUnit Jupiter 기반 테스트 클래스의 구성을 단순화할 수 있습니다:

@TransactionalDevTestConfig
class OrderRepositoryTests { }

@TransactionalDevTestConfig
class UserRepositoryTests { }

JUnit Jupiter는 메타 어노테이션으로 @Test, 반복 테스트, 파라미터화된 테스트 등의 사용을 지원하므로 테스트 메서드 수준에서 사용자 정의 구성된 어노테이션을 생성할 수도 있습니다. 예를 들어, JUnit Jupiter의 @Test  @Tag 어노테이션과 Spring의 @Transactional어노테이션을 결합하는 구성된 어노테이션을 만들고자 하는 경우 다음과 같이 @TransactionalIntegrationTest 어노테이션을 생성할 수 있습니다:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Transactional
@Tag("integration-test") // org.junit.jupiter.api.Tag
@Test // org.junit.jupiter.api.Test
public @interface TransactionalIntegrationTest { }

그런 다음 다음과 같이 사용자 정의 @TransactionalIntegrationTest 어노테이션을 사용하여 개별 JUnit Jupiter 기반 테스트 메서드의 구성을 단순화할 수 있습니다:

@TransactionalIntegrationTest
void saveOrder() { }

@TransactionalIntegrationTest
void deleteOrder() { }

자세한 내용은Spring 어노테이션 프로그래밍 모델위키 페이지를 참조하세요.

 

추가 리소스

  • JUnit: "Java 및 JVM을 위한 프로그래머 친화적인 테스트 프레임워크". Spring 프레임워크의 테스트 스위트에서 사용되며Spring TestContext 프레임워크에서 지원됩니다.
  • TestNG: 테스트 그룹, 데이터 기반 테스트, 분산 테스트 및 기타 기능에 대한 지원이 추가된 JUnit에서 영감을 얻은 테스트 프레임워크입니다. Spring TestContext 프레임워크에서 지원됩니다
  • AssertJ: Java 8 람다, 스트림 및 기타 다양한 기능을 지원하는 "Java를 위한 유창한 어설션".
  • 모의 객체: Wikipedia의 기사.
  • MockObjects.com: 테스트 중심 개발 내에서 코드 디자인을 개선하기 위한 기술인 모의 객체 전용 웹 사이트입니다.
  • Mockito:테스트 스파이 패턴을 기반으로 하는 Java 모의 라이브러리. Spring 프레임워크의 테스트 스위트에서 사용됩니다.
  • EasyMock: "Java의 프록시 메커니즘을 사용하여 인터페이스(및 클래스 확장을 통한 객체)를 즉석에서 생성하여 인터페이스에 대한 모의 객체를 제공하는" Java 라이브러리
  • JMock: 모의 객체를 사용하여 Java 코드의 테스트 중심 개발을 지원하는 라이브러리.
  • DbUnit: 데이터베이스 기반 프로젝트를 대상으로 하며, 무엇보다도 테스트 실행 사이에 데이터베이스를 알려진 상태로 전환하는 JUnit 확장(Ant 및 Maven에서도 사용 가능)입니다.
  • 테스트 컨테이너: JUnit 테스트를 지원하는 Java 라이브러리로, 일반적인 데이터베이스, 셀레늄 웹 브라우저 또는 Docker 컨테이너에서 실행할 수 있는 모든 것의 가볍고 버려지는 인스턴스를 제공합니다.
  • Grinder: Java 부하 테스트 프레임워크.
  • SpringMockK: Mockito 대신 MockK를 사용하여 Kotlin에서 작성된 Spring Boot 통합 테스트를 지원합니다.




반응형
profile

while(1) work();

@유호건

❤️댓글은 언제나 힘이 됩니다❤️ 궁금한 점이나 잘못된 내용이 있다면 댓글로 남겨주세요.

검색 태그