Spring Framework

Spring Webflux - Test Code

흥부가귀막혀 2022. 3. 14. 13:12

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();