0

I'm trying to figure out how best to test a simple WebFlux passthrough service starting with the routing part.

(the whole small project is available at https://github.com/ChambreNoire/passthru)

I have a simple functional routing setup like this :

@Configuration
@RequiredArgsConstructor
public class RouterConfig {

    private final RouteHandler handler;

    @Bean
    public RouterFunction<ServerResponse> passthru() {
        return route(path("/service/**"), this.handler::doPassthru);
    }
    // ...
}

which is handled by the following handler :

@Component
@RequiredArgsConstructor
public class RouteHandler {

    private final WebClient webClient;
    
    public Mono<ServerResponse> passthru(ServerRequest request) {
        return webClient.method(request.method())
            .uri(request.path())
            .headers(httpHeaders -> request.headers().asHttpHeaders().forEach(httpHeaders::put))
            .body(request.bodyToMono(PooledDataBuffer.class), PooledDataBuffer.class)
            .exchangeToMono(response -> response.bodyToMono(PooledDataBuffer.class)
                .flatMap(buffer -> ServerResponse
                    .status(response.statusCode())
                    .headers(headers -> headers.addAll(response.headers().asHttpHeaders()))
                    .bodyValue(buffer)));
    }
    // ...
}

using a WebClient defined thus :

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient(@Value("${target.url}") String targetUrl) {
        return WebClient.builder()
            .baseUrl(targetUrl)
            .clientConnector(new ReactorClientHttpConnector(HttpClient.create()
                .followRedirect(true)))
            .build();
    }
}

it just forwards the request to another server via a WebClient and returns the result. (Other routes will do things with the headers etc but I'm dealing with the simplest use case here).

@SpringBootTest(
    classes = MyApplication.class,
    webEnvironment = RANDOM_PORT
)
@RunWith(SpringRunner.class)
public class RoutingTest {

    @Autowired
    private RouterConfig config;

    @MockBean
    private RouteHandler handler;

    @Test
    public void passthru() {

        WebTestClient testClient = WebTestClient
            .bindToRouterFunction(config.passthru())
            .build();

        final AuthRequest authRequest = AuthRequest.builder()
            .secretKey("my.secret.key")
            .build();

        final AccessTokens accessTokens = AccessTokens.builder()
            .accountId("accountId")
            .accessToken("accessToken")
            .build();

        final Mono<ServerResponse> response = ServerResponse.ok()
            .body(Mono.just(accessTokens), AccessTokens.class);

        when(handler.passthru(any(ServerRequest.class))).thenReturn(response);

        testClient
            .post()
            .uri("/service/auth")
            .body(Mono.just(authRequest), AuthRequest.class)
            .exchange()
            .expectStatus().isOk()
            .expectBody(AuthRequest.class)
            .isEqualTo(authRequest);
    }
}

The objects sent and received could be anything really but here are the one's used above for the sake of completeness:

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class AccessTokens {

    @JsonProperty("account")
    private String accountId;
    private String accessToken;
}
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class AuthRequest {

    private String secretKey;
}

The test fails as the returned MyObject isn't the same as myObject but I don't understand why. Plus I'd like to capture the request but I'm not sure how to go about this.

UPDATE!

Ok so as pointed out by @Toerktumlare I shouldn't be testing for equality, however, I'm still unsure as to what the best testing approach for this method is. So far I'm using a MockWebServer and a WebTestClient thus :

@SpringBootTest(
    classes = MyApplication.class,
    webEnvironment = RANDOM_PORT
)
public class HandlerTest {

    @Autowired
    private RouterConfig config;

    static MockWebServer mockBackEnd;

    private static AuthRequest authRequest;
    private static AccessTokens accessTokens;

    @SuppressWarnings("unused")
    @DynamicPropertySource
    static void properties(DynamicPropertyRegistry registry) {
        registry.add("gateway.url", () -> "http://localhost:" + mockBackEnd.getPort());
    }

    @BeforeAll
    static void setUp() throws IOException {
        mockBackEnd = new MockWebServer();
        mockBackEnd.start();

        authRequest = AuthRequest.builder()
            .secretKey("secretKey")
            .build();

        accessTokens = AccessTokens.builder()
            .accountId("accountId")
            .accessToken("accessToken")
            .renewToken("renewToken")
            .type("Bearer")
            .build();
    }

    @AfterAll
    static void tearDown() throws IOException {
        mockBackEnd.shutdown();
    }

    @Test
    public void testPassthru() throws JsonProcessingException {

        ObjectMapper objectMapper = new ObjectMapper();

        mockBackEnd.enqueue(new MockResponse()
            .setResponseCode(HttpStatus.OK.value())
            .setBody(objectMapper.writeValueAsString(accessTokens))
            .addHeader("Content-Type", "application/json"));

        WebTestClient testClient = WebTestClient
            .bindToRouterFunction(config.passthru())
            .build();

        testClient.post()
            .uri("/service/auth")
            .body(Mono.just(authRequest), AuthRequest.class)
            .exchange()
            .expectStatus().isOk()
            .expectBody(AccessTokens.class)
            .value(tokens -> {
                assertThat(tokens)
                    .returns("accountId", from(AccessTokens::getAccountId))
                    .returns("accessToken", from(AccessTokens::getAccessToken))
                    .returns("renewToken", from(AccessTokens::getRenewToken))
                    .returns("Bearer", from(AccessTokens::getType));
            });
    }
}

It works fine however I'm not sure whether this is sufficient for testing this endpoint...

Cheers

ChambreNoire
  • 387
  • 7
  • 19
  • your code does not compile what is `accessTokens` please update with code that actually compiles – Toerktumlare Jun 30 '21 at 20:30
  • @Toerktumlare my bad! corrected, plus I included a link to a GitHub version. – ChambreNoire Jul 01 '21 at 09:09
  • 1
    why do you want to test for equality between the objects, shouldnt you be testing equality among the values in the object. – Toerktumlare Jul 01 '21 at 09:58
  • Yeah, I just figured this out. I was reasoning that given that I'm mocking the handler response I should receive the same object but it's obtained through the WebTestClient. Just adding a Lombok @EqualsAndHashCode to the object fixes the test. Thanks! – ChambreNoire Jul 01 '21 at 10:02
  • 1
    objects can be reused and different values can be set, and ppl can override the equals and hashcode in objects to compare different things. So i would say it is better to be distinct in your tests, than to rely on defaults that may or may not be overridden. After all its what is produced by the business logic that you want to test. – Toerktumlare Jul 01 '21 at 10:06

0 Answers0