SlideShare uses cookies to improve functionality and performance, and to provide you with relevant advertising. If you continue browsing the site, you agree to the use of cookies on this website. See our User Agreement and Privacy Policy.
SlideShare uses cookies to improve functionality and performance, and to provide you with relevant advertising. If you continue browsing the site, you agree to the use of cookies on this website. See our Privacy Policy and User Agreement for details.
Successfully reported this slideshow.
Activate your 30 day free trial to unlock unlimited reading.
이일민 / Epril
---
스프링5에 도입된 웹플럭스와 리액티브 함수형 프로그래밍 기술에 대한 여러가지 테스트 기술을 살펴보고 이를 효과적으로 개발에 적용하는 방법을 설명합니다.
- 리액티브 함수형 프로그래밍과 테스트
- 웹플럭스를 이용하는 웹 테스트
- 스프링5 애플리케이션의 테스트 전략
이일민 / Epril
---
스프링5에 도입된 웹플럭스와 리액티브 함수형 프로그래밍 기술에 대한 여러가지 테스트 기술을 살펴보고 이를 효과적으로 개발에 적용하는 방법을 설명합니다.
- 리액티브 함수형 프로그래밍과 테스트
- 웹플럭스를 이용하는 웹 테스트
- 스프링5 애플리케이션의 테스트 전략
1.
호주에 거주하고 있지만 여러 나라를 다니며 일하기 좋아하는 개발자. 오래전 스
프링 프레임워크의 매력에 빠진 뒤로 스프링의 변화에 맞춰 기술을 공부하고 이
를 이용해 일을 하는 것을 좋아한다. 최신 스프링에 추가된 리액티브 함수형 프로
그래밍 기술을 블록체인 코어 개발을 비롯한 여러 분야에 적용해보고 있다. 토비
의 스프링이라는 책을 썼고, 유튜브에서 토비의 봄 TV라는 코딩 방송을 하기도 한
다.
24.
public UserOrder orders(String email) {
try {
User user = findUserApi(email);
List<Order> orders = getOpenOrders(user);
return new UserOrder(email, orders);
}
catch(Exception e) {
return UserOrder.FAIL;
}
}
25.
public UserOrder orders(String email) {
try {
User user = findUserApi(email);
List<Order> orders = getOpenOrders(user);
return new UserOrder(email, orders);
}
catch(Exception e) {
return UserOrder.FAIL;
}
}
두번의 동기 API 호출
RestTemplate
26.
public DeferredResult<UserOrder> asyncOrders(String email) {
DeferredResult dr = new DeferredResult();
asyncFindUserApi(email).addCallback(
user -> asyncOrdersApi(user).addCallback(
orders -> {
dr.setResult(new UserOrder(email, orders));
},
e -> dr.setErrorResult(UserOrder.FAIL) ),
e -> dr.setErrorResult(UserOrder.FAIL));
return dr;
}
27.
public DeferredResult<UserOrder> asyncOrders(String email) {
DeferredResult dr = new DeferredResult();
asyncFindUserApi(email).addCallback(
user -> asyncOrdersApi(user).addCallback(
orders -> {
dr.setResult(new UserOrder(email, orders));
},
e -> dr.setErrorResult(UserOrder.FAIL) ),
e -> dr.setErrorResult(UserOrder.FAIL));
return dr;
}
두번의 비동기 API 호출
AsyncRestTemplate
28.
public DeferredResult<UserOrder> asyncOrders(String email) {
DeferredResult dr = new DeferredResult();
asyncFindUserApi(email).addCallback(
user -> asyncOrdersApi(user).addCallback(
orders -> {
dr.setResult(new UserOrder(email, orders));
},
e -> dr.setErrorResult(UserOrder.FAIL) ),
e -> dr.setErrorResult(UserOrder.FAIL));
return dr;
}
비동기 결과처리를 위한 콜백
매 단계마다 중첩
29.
public DeferredResult<UserOrder> asyncOrders(String email) {
DeferredResult dr = new DeferredResult();
asyncFindUserApi(email).addCallback(
user -> asyncOrdersApi(user).addCallback(
orders -> {
dr.setResult(new UserOrder(email, orders));
},
e -> dr.setErrorResult(UserOrder.FAIL) ),
e -> dr.setErrorResult(UserOrder.FAIL));
return dr;
}
매 단계마다 반복되는
예외 콜백
30.
public CompletableFuture<UserOrder> asyncOrders2(String email) {
return asyncFindUser2(email)
.thenCompose(user -> asyncOrders2(user))
.thenApply(orders -> new UserOrder(email, orders))
.exceptionally(e -> UserOrder.FAIL);
}
31.
public CompletableFuture<UserOrder> asyncOrders2(String email) {
return asyncFindUser2(email)
.thenCompose(user -> asyncOrders2(user))
.thenApply(orders -> new UserOrder(email, orders))
.exceptionally(e -> UserOrder.FAIL);
} 비동기 결과처리 함수 이용
중첩되지 않음
86.
@Autowired HelloService helloService;
@GetMapping("/api")
public Mono<String> helloApi() {
return this.helloService.hello()
.map(body -> body.toUpperCase())
.switchIfEmpty(Mono.just("Empty"))
.doOnError(c -> c.printStackTrace());
}
단순한 리액티브 API를 이용하는 코드
Mock 서비스로 대체 가능
90.
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route =
route(GET("/person/{id}").and(accept(APPLICATION_JSON)), handler::getPerson)
.andRoute(GET("/person").and(accept(APPLICATION_JSON)), handler::listPeople)
.andRoute(POST("/person"), handler::createPerson);
public class PersonHandler {
public Mono<ServerResponse> listPeople(ServerRequest request) { … }
public Mono<ServerResponse> createPerson(ServerRequest request) { … }
public Mono<ServerResponse> getPerson(ServerRequest request) { … }
}
웹 요청을 처리하고 응답을 만드는 순수한 함수의 모음
91.
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route =
route(GET("/person/{id}").and(accept(APPLICATION_JSON)), handler::getPerson)
.andRoute(GET("/person").and(accept(APPLICATION_JSON)), handler::listPeople)
.andRoute(POST("/person"), handler::createPerson);
public class PersonHandler {
public Mono<ServerResponse> listPeople(ServerRequest request) { … }
public Mono<ServerResponse> createPerson(ServerRequest request) { … }
public Mono<ServerResponse> getPerson(ServerRequest request) { … }
}
웹 요청을 담당할 함수 핸들러를 찾음
92.
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route =
route(GET("/person/{id}").and(accept(APPLICATION_JSON)), handler::getPerson)
.andRoute(GET("/person").and(accept(APPLICATION_JSON)), handler::listPeople)
.andRoute(POST("/person"), handler::createPerson);
public class PersonHandler {
public Mono<ServerResponse> listPeople(ServerRequest request) { … }
public Mono<ServerResponse> createPerson(ServerRequest request) { … }
public Mono<ServerResponse> getPerson(ServerRequest request) { … }
}
ServerRequest->ServerResponse로 변환하는 리액티브 핸들러
95.
•
o bindToApplicationContext 이용
@RunWith(SpringRunner.class)
@WebFluxTest
public class WebClientBootTest {
@Autowired WebTestClient webTestClient;
@Test
public void hello() {
webTestClient.get().uri("/hello/{name}", "Spring")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("Hello Spring");
}
}
96.
•
o bindToApplicationContext 이용
@RunWith(SpringRunner.class)
@WebFluxTest
public class WebClientBootTest {
@Autowired WebTestClient webTestClient;
@Test
public void hello() {
webTestClient.get().uri("/hello/{name}", "Spring")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("Hello Spring");
}
}
SpringBoot 앱을 MockServer에 배포
테스트에 사용할 WebTestClient 생성
97.
•
o bindToApplicationContext 이용
@RunWith(SpringRunner.class)
@WebFluxTest
public class WebClientBootTest {
@Autowired WebTestClient webTestClient;
@Test
public void hello() {
webTestClient.get().uri("/hello/{name}", "Spring")
.exchange()
.expectStatus().isOk()
.expectBody(String.class)
.isEqualTo("Hello Spring");
}
}
WebClient 처럼 API 호출하고
98.
•
o bindToApplicationContext 이용
@RunWith(SpringRunner.class)
@WebFluxTest
public class WebClientBootTest {
@Autowired WebTestClient webTestClient;
@Test
public void hello() {
webTestClient.get().uri("/hello/{name}", "Spring")
.exchange()
.expectStatus().isOk()
.expectBody(String.class)
.isEqualTo("Hello Spring");
}
}
API 호출 응답 결과 검증
100.
• WebHttpHandlerBuilder가 이용할 수 있는 WebHandler API 구성
을 가진 ApplicationContext를 이용
var context = new AnnotationConfigApplicationContext(MyConfiguration.class);
WebTestClient client = WebTestClient.bindToApplicationContext(context).build();
client.get().uri("/hello/{name}", "Spring")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("Hello Spring");
101.
• WebHttpHandlerBuilder가 이용할 수 있는 WebHandler API 구성
을 가진 ApplicationContext를 이용
WebTestClient client = WebTestClient.bindToController(
new MyController(), new HelloApi()
).build();
client.get().uri("/hello/{name}", "Spring")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("Hello Spring");
102.
• WebHttpHandlerBuilder가 이용할 수 있는 WebHandler API 구성
을 가진 ApplicationContext를 이용
WebTestClient client = WebTestClient.bindToController(
new MyController(), new HelloApi()
).build();
client.get().uri("/hello/{name}", "Spring")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("Hello Spring");
특정 컨트롤러/핸들러만으로
테스트 대상 구성