如何对出站 HTTP 请求进行断言?

问题描述 投票:0回答:1

这是一个例子。这是

org.springframework.cloud.gateway.filter.WebClientHttpRoutingFilter
的修剪和简化版本:

package com.example.gatewaydemo.misc;

import java.net.URI;
import java.util.stream.Stream;

import reactor.core.publisher.Mono;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;

/**
 * A {@link GlobalFilter} that actually makes an asynchronous call to the proxied server.
 */
public class CallingGlobalFilter implements GlobalFilter {
    private final WebClient webClient;

    public CallingGlobalFilter(WebClient webClient) {
        this.webClient = webClient;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);

        ServerHttpRequest request = exchange.getRequest();

        HttpMethod method = request.getMethod();

        WebClient.RequestBodySpec bodySpec = this.webClient.method(method)
                .uri(requestUrl)
                .headers(h -> h.addAll(request.getHeaders()));

        WebClient.RequestHeadersSpec<?> headersSpec = requiresBody(method) ?
                bodySpec.body(BodyInserters.fromDataBuffers(request.getBody())) :
                bodySpec;

        return headersSpec.exchangeToMono(Mono::just)
                .flatMap(res -> {
                    ServerHttpResponse response = exchange.getResponse();
                    response.getHeaders().putAll(res.headers().asHttpHeaders());
                    response.setStatusCode(res.statusCode());
                    exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
                    return chain.filter(exchange);
                });
    }

    private boolean requiresBody(HttpMethod method) {
        return Stream.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH)
                .anyMatch(m -> method.matches(m.toString()));
    }
}
<!-- if you want my specific example to compile, include these dependencies -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

它是一个过滤器,使用注入的

WebClient
向代理服务器发出请求,然后将其包装在
MonoFlatMap
中。我需要确保出站请求正确。例如,如果请求的方法是
GET
,则预期行为包括忽略正文。我需要为此编写一个测试

我无法

ArgumentCapt
将交换传递给
chain.filter(..)
并对其进行断言,因为此过滤器过滤了原始交换。也就是说,它仍然会用 body 来包装请求,断言将会失败

在这种情况下,我如何实际上对出站 HTTP 请求进行断言?

java testing spring-webflux
1个回答
0
投票

过滤器方法中发生了很多事情,但是可以进行完整的单元测试。虽然会有点长。

下面我为您要求的 GET 案例编写了一个单元测试示例。

当您决定私有方法中是否有主体时,该方法不能单独测试 - 您必须通过外部过滤器类的测试来覆盖不同的情况 - 但您不必运行对每个案例进行全面测试。只需模拟、断言、验证,也许捕获就足够了,这样您就可以使用不同的输入进入私有方法,并可以区分行为。这甚至可能是参数化测试的一个很好的例子。

为了测试 lambda 和方法引用,您将看到我使用参数捕获器来获取实际的

Function
Consumer
参数,然后我调用捕获参数的方法作为带有额外模拟、验证和断言的附加测试。

随意将这个巨大的测试分成几个较小的测试,这样您就不会一次性捕获所有内容,而是更单独地检查 lambda 和方法引用 - 尽管在实际调用发生之前您仍然需要模拟很多东西。

请注意,我还将

@Mock
注释与对
Mockito.mock
的本地调用混合在一起 - 后者只是我的偏好,以便更好地了解我真正在哪里使用模拟并防止在一次测试中重用模拟时出现错误,但我使用
带有类型参数的模拟和参数捕获器的 @Mock
@Captor
变体可防止出现太多有关类转换的警告。你会发现我没有对
Mono::just
的静态方法测试部分采取额外的警告预防步骤,因为它只发生一次,而且代码真的很长。

package com.example.gatewaydemo.misc;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;

import static org.mockito.Mockito.*;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;

@ExtendWith(MockitoExtension.class)
class CallingGlobalFilterTest {

  @Captor
  ArgumentCaptor<Consumer<HttpHeaders>> captorHeaders;

  @Mock
  Mono<ClientResponse> monoClientResponseMock1;

  @Mock
  Mono<ClientResponse> monoClientResponseMock2;

  @Captor
  ArgumentCaptor<Function<ClientResponse, Mono<ClientResponse>>> exchangeToMonoCaptor;

  @Mock
  Mono<Void> expectedResult;

  @Captor
  ArgumentCaptor<Function<ClientResponse, Mono<? extends Void>>> flatMapCaptor;

  @Mock
  Mono<Void> flatMapResultMock;

  @Test
  void filter_get_request() {

    WebClient webClientMock = mock(WebClient.class);
    CallingGlobalFilter underTest = new CallingGlobalFilter(webClientMock);

    ServerWebExchange exchangeMock = mock(ServerWebExchange.class);
    GatewayFilterChain chainMock = mock(GatewayFilterChain.class);

    URI requestUrlMock = mock(URI.class);
    when(exchangeMock.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR)).thenReturn(requestUrlMock);

    ServerHttpRequest serverHttpRequestMock = mock(ServerHttpRequest.class);
    when(exchangeMock.getRequest()).thenReturn(serverHttpRequestMock);
    when(serverHttpRequestMock.getMethod()).thenReturn(HttpMethod.GET);

    WebClient.RequestBodyUriSpec requestBodyUriSpecMock1 = mock(WebClient.RequestBodyUriSpec.class);
    when(webClientMock.method(HttpMethod.GET)).thenReturn(requestBodyUriSpecMock1);
    when(requestBodyUriSpecMock1.uri(requestUrlMock)).thenReturn(requestBodyUriSpecMock1);
    when(requestBodyUriSpecMock1.headers(any())).thenReturn(requestBodyUriSpecMock1);

    when(requestBodyUriSpecMock1.exchangeToMono(Mockito.<Function<ClientResponse, Mono<ClientResponse>>>any())).thenReturn(monoClientResponseMock1);

    when(monoClientResponseMock1.flatMap(Mockito.<Function<ClientResponse, Mono<? extends Void>>>any())).thenReturn(expectedResult);

    // actual call of outer function
    Mono<Void> actualResult = underTest.filter(exchangeMock, chainMock);

    Assertions.assertSame(expectedResult, actualResult);

    verify(requestBodyUriSpecMock1).headers(captorHeaders.capture());
    Consumer<HttpHeaders> capturedHeadersConsumer = captorHeaders.getValue();
    HttpHeaders httpHeadersFromRequestMock = mock(HttpHeaders.class);
    when(serverHttpRequestMock.getHeaders()).thenReturn(httpHeadersFromRequestMock);

    HttpHeaders httpHeadersMock1 = mock(HttpHeaders.class);

    // actual call of lambda 1
    capturedHeadersConsumer.accept(httpHeadersMock1);

    verify(httpHeadersMock1).addAll(same(httpHeadersFromRequestMock));

    verify(requestBodyUriSpecMock1, never()).body(any());
    verify(serverHttpRequestMock, never()).getBody();

    verify(requestBodyUriSpecMock1).exchangeToMono(exchangeToMonoCaptor.capture());

    Function<ClientResponse, Mono<ClientResponse>> exchangeToMonoFunction = exchangeToMonoCaptor.getValue();
    ClientResponse clientResponseMock1 = mock(ClientResponse.class);

    try (MockedStatic<Mono> mono = Mockito.mockStatic(Mono.class)) {
      mono.when(() -> Mono.just(clientResponseMock1))
          .thenReturn(monoClientResponseMock2);

      // actual call of method reference to Mono::join
      Mono<ClientResponse> actualInnerResult = exchangeToMonoFunction.apply(clientResponseMock1);

      Assertions.assertSame(monoClientResponseMock2, actualInnerResult);
    }

    verify(monoClientResponseMock1).flatMap(flatMapCaptor.capture());

    Function<ClientResponse, Mono<? extends Void>> flatMapFunction = flatMapCaptor.getValue();

    ClientResponse clientResponseMock2 = mock(ClientResponse.class);

    ServerHttpResponse responseMock = mock(ServerHttpResponse.class);
    when(exchangeMock.getResponse()).thenReturn(responseMock);

    HttpHeaders httpHeadersMock2 = mock(HttpHeaders.class);
    when(responseMock.getHeaders()).thenReturn(httpHeadersMock2);

    ClientResponse.Headers clientResponseHeadersMock = mock(ClientResponse.Headers.class);
    when(clientResponseMock2.headers()).thenReturn(clientResponseHeadersMock);

    HttpHeaders httpHeadersFromClientResponseMock = mock(HttpHeaders.class);
    when(clientResponseHeadersMock.asHttpHeaders()).thenReturn(httpHeadersFromClientResponseMock);

    HttpStatusCode httpStatusCodeMock = mock(HttpStatus.class); // have to mock an implementation of the sealed interface
    when(clientResponseMock2.statusCode()).thenReturn(httpStatusCodeMock);

    Map<String, Object> attributesMap = new HashMap<>();
    when(exchangeMock.getAttributes()).thenReturn(attributesMap);

    when(chainMock.filter(exchangeMock)).thenReturn(flatMapResultMock);

    // actual call of lambda 2
    Mono<? extends Void> actualFlatMapFunctionResult = flatMapFunction.apply(clientResponseMock2);

    verify(exchangeMock).getResponse();
    verify(httpHeadersMock2).putAll(same(httpHeadersFromClientResponseMock));

    verify(responseMock).setStatusCode(same(httpStatusCodeMock));

    Assertions.assertSame(clientResponseMock2, attributesMap.get(CLIENT_RESPONSE_ATTR));

    Assertions.assertSame(flatMapResultMock, actualFlatMapFunctionResult);
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.