Spring Webflux - Test Code
Test Code
StepVerifier
- Reactor 를 사용할 경우 주로 다루는 Mono/Flux 를 반환하는 리액티브 API 에 대한 테스트는 기존 Test Code 작성 방법으로는 테스트하기 힘들다.
- 아래와 같이 reactor-test 에 대한 dependency 를 추가해야 사용이 가능하다
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
◉ 일반적인 Test Code 작성시 발생할 수 있는 실수 1
@Test
public void mono() {
Mono<Integer> mono = Mono.just(1) // (1)
.subscribeOn(Schedulers.single()); // (2)
Mono.subscribe(item -> assertThat(item).isEqualTo(1)); // (3)
}
(1) 1을 생성하는 Mono 생성.
(2) single thread 로 비동기 동작을 수행하도록 scheduler 설정.
(3) subscribe 를 통해 생성한 Mono 구독 후 검증.
- 위의 테스트 코드의 결과는 성공으로 반환되지만 실제로는 정상적인 테스트가 이루어지지 않은 것이다. 그 이유는 Scheduler 를 통해 비동기로 테스트코드가 동작하게 되면서 아직 테스트코드가 전부 실행되지 않은 상태에서 mono Test Code 함수가 정상적으로 종료되었기 때문이다.
- 실제로 isEqualTo(1) 을 isEqualTo(100) 으로 변경해도 테스트 코드는 성공한다.
◉ 일반적인 Test Code 작성시 발생할 수 있는 실수 2
@Test
public void mono() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
CountDownLatch latch = new CountDownLatch(1); // (1)
Mono.subscribe(item -> {
assertThat(item).isEqualTo(2); // (2)
latch.countDown(); // (3)
});
latch.await(); // (4)
}
(1) 1의 실수 케이스를 보완하기 위해 비동기 동작이 끝날때까지 기다리도록 CountDownLatch 선언.
(2) 테스트 코드 검증 로직에서 실패 발생.
(3) 테스트가 성공할 경우 count 가 증가하도록 하려 했으나 (2) 에서 예외가 발생하여 수행되지 못함.
(4) 테스트가 무한정 대기하게 된다.
- 비동기로 동작하는 로직을 검증하기 위해 blocking 로직을 테스트코드에 넣었다가 발생할 수 있는 실수다.
- 예외 처리를 통해 위와 같은 실수를 방지할수는 있지만 고작 1을 publish 하는 Mono 에 대한 검증치고는 테스트코드가 너무 방대하고 신경써야할게 많다.
StepVerifier 를 사용할 경우
- 위에 실수 케이스에 대해 StepVerifier 를 사용할 경우 아래와 같이 코드를 짤 수 있다.
@Test
public void mono() {
Mono<Integer> mono = Mono.just(1)
.subscribeOn(Schedulers.single());
StepVerifier.create(mono) // (1)
.expectNext(1) // (2)
.verifyComplete(); // (3)
}
(1) mono 에 대한 StepVerifier Step 생성
(2) Mono 에서 처음 publish 한 데이터가 1인지 검증
(3) 테스트 검증 완료
예외 발생에 대한 검증
- 예외 발생에 대한 검증을 위해 제공하는 API 들도 있다.
@Test
public void flux() {
Flux<Integer> flux = Flux.just(1)
.concatWith(Mono.error(new IllegalArgumentException("BOOM!")) // (1)
.subscribeOn(Schedulers.single());
StepVerifier.create(flux)
.expectNext(1)
.expectErrorMatches(throwable -> throwable instanceof IllegalArgumentException && throwable.getMessage().equals("BOOM!")) // (2)
.verify(); // (3)
}
(1) 1 이후 IllegalArgumentException 이 발생하도록 추가
(2) 1 이후 error 가 발생할거라 기대하고 해당 에러가 IllegalArgumentException 인지, message 가 기대한것과 같은지 확인
(3) 테스트 검증 완료
WebTestClient
- Request Mapping 및 Handler, Filter 등의 동작에 대한 검증을 할 수 있도록 WebTestClient 를 제공하고 있다.
- 아래와 같이 spring-boot-starter-test 에 대한 dependency 를 추가해야 사용이 가능하다
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
- 아래와 같이 Controller 로 정의한 API 에 대해 테스트 코드를 작성해볼 수 있다.
Controller
@Controller
public class HelloController {
@GetMapping("/hello/{name}")
public Mono<String> hello(@PathVariable String name) {
return Mono.just("hello " + name);
}
}
WebTestClient Test Code
@RunWith(SpringRunner.class)
@WebFluxTest
public class HelloControllerTest {
@Autowired
private WebTestClient webTestClient; // (1)
@Test
public void hello() {
webTestClient.get()
.uri("/hello/{name}", "Spring") // (2)
.exchange()
.expectStatus()
.isOk() // (3)
.expectBody(String.class)
.isEqualTo("hello Spring"); // (4)
}
}
(1) @WebFluxTest 에서 자동으로 WebTestClient 를 생성해서 주입이 가능하게 해주는데 이때는 Test Code 실행시 만들어지는 ApplicationContext 를 활용한 WebTestClient.bindToApplicationContext(context) 함수로 WebTestClient 가 생성된다.
(2) 생성된 WebTestClient 를 통해 /hello/Spring 이라는 url 로 API 를 요청해 본다.
(3) 응답이 성공으로 왔는지 검증한다.
(4) 응답값이 기대한 결과대로 왔는지 검증한다.
WebTestClient 는 테스트 케이스에 따라 아래와 같이 여러가지 방법으로 생성이 가능하다.
- Binding to a Server : application server url 로 생성. 해당 Application Server 로 요청이 가능하다.
WebTestClient testClient = WebTestClient
.bindToServer()
.baseUrl("http://localhost:8080")
.build();
- Binding to a Router : RouterFunction 을 통해 생성. 테스트 요청은 해당 RouterFunction 에 맵핑된 url 만 할 수 있다.
RouterFunction function = RouterFunctions.route(
RequestPredicates.GET("/resource"),
request -> ServerResponse.ok().build()
);
WebTestClient testClient = WebTestClient
.bindToRouterFunction(function)
.build()
- Binding to an Application Context : ApplicationContext 를 통해 생성. ApplicationContext 를 생성한 Application 에 대해 요청이 가능하다.
@Autowired
private ApplicationContext context;
WebTestClient testClient = WebTestClient.bindToApplicationContext(context)
.build();
- Binding to a Controller : Controller 를 통해 생성. 해당 Controller 에 맵핑된 url 에 대해 테스트 요청이 가능하다.
@Autowired
private Controller controller;
WebTestClient testClient = WebTestClient.bindToController(controller).build();