Testing Spring Cloud Gateway

Posted by

Here you’ll find an approach to cover your Spring Cloud Gateway Filters with tests.

Intro

There are so many articles on internet about how to use Spring Gateway.

  1. https://spring.io/guides/gs/gateway/
  2. https://www.baeldung.com/spring-cloud-gateway
  3. https://blog.knoldus.com/spring-cloud-api-gateway/
  4. https://www.educba.com/spring-cloud-gateway/
  5. https://medium.com/@niral22/spring-cloud-gateway-tutorial-5311ddd59816
  6. and others

There are many articles on internet about how to implement authorization inside Spring Gateway.

  1. https://medium.com/@niral22/spring-cloud-gateway-tutorial-5311ddd59816
  2. https://oril.co/blog/spring-cloud-gateway-security-with-jwt/
  3. https://www.baeldung.com/spring-cloud-gateway-oauth2
  4. https://www.xoriant.com/blog/microservices-security-using-jwt-authentication-gateway

But none of them explain how to cover your code with tests. Let me fill this gap up.


Few words about Spring Cloud Gateway

What is Spring Gateway in a nutshell? Reading the official documentation helps to get the sense.

This project provides a library for building an API Gateway on top of Spring WebFlux. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency.

https://spring.io/projects/spring-cloud-gateway

Simply saying Spring Cloud Gateway is a manageable router. The setup of routing can be specified in Java and application.properties (or .yaml) file both.

Here is a Java routing settings:

@SpringBootApplication
public class DemogatewayApplication {
  @Bean
  public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
      .route("path_route", r -> r.path("/get")
        .uri("http://httpbin.org"))
      .route("host_route", r -> r.host("*.myhost.org")
        .uri("http://httpbin.org"))
      .route("rewrite_route", r -> r.host("*.rewrite.org")
        .filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
        .uri("http://httpbin.org"))
      .route("hystrix_route", r -> r.host("*.hystrix.org")
        .filters(f -> f.hystrix(c -> c.setName("slowcmd")))
        .uri("http://httpbin.org"))
      .route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
        .filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
        .uri("http://httpbin.org"))
      .route("limit_route", r -> r
        .host("*.limited.org").and().path("/anything/**")
        .filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
        .uri("http://httpbin.org"))
      .build();
  }
}
Code language: Java (java)

And here is another one example of placing it in application.yaml:

spring:
  cloud:
    gateway:
      routes:
        - id: card-api
          uri: http://card-api/
          predicates:
            - Path=/card/**
        - id: payment-api
          uri: http://payment-api/
          predicates:
            - Path=/payment/**
        - id: wallet-api
          uri: http://wallet-api/
          predicates:
            - Path=/wallet/**Code language: JavaScript (javascript)

But for me the main feature of Spring Cloud Gateway is the possibility of being as authorization point of all incoming requests.

How to authorize requests with Spring Cloud Gateway

The main point where all requests can be intercepted for reasons of changing and analyzing is Filter. All filters in Spring Web linked in one filter chain and Spring Cloud Gateway is no different. Spring Web differs of Spring Cloud Gateway in part of thread model of processing incoming requests. Spring Web implemented in the paradigm of old thread model where each request consumes only one dedicated thread. And Spring Cloud Gateway implemented in reactive paradigm – listening thread does not block while waiting the response of server.

Reactivity – what does it bring to us? Non blocking processing as an increase of productivity – the first. But the other side of this boost are restrictions in testing.

How to test Spring Cloud Gateway?

Here is probably only one article on the internet referring to this question- https://stackoverflow.com/questions/59825774/how-to-unit-test-springs-gateway

The approved answer is to write an API simulator or Proxy to fakely response to each request.

Approved answer on How to test Spring Cloud

But that post on SOF does not answer properly on how to cover Spring Cloud Gateway Filter with tests!

Proper way to cover Spring Cloud Gateway Filter with tests

Here is the schema of request passing through the Spring Cloud Gateway

Schema of request passing through the Spring Cloud Gateway

In order to encapsulate test flow and make it more unit any fake web server should be used as a destination of requests. Otherwise tests will be failed with 500 Internal Server error due to destination unavailability. Good option is WireMock. Simple test class using WireMock is like:

@WireMockTest
public class DeclarativeWireMockTest {
    @Test
    void test_something_with_wiremock(WireMockRuntimeInfo wmRuntimeInfo) {
        // The static DSL will be automatically configured for you
        stubFor(get("/static-dsl").willReturn(ok()));
        // Instance DSL can be obtained from the runtime info parameter
        WireMock wireMock = wmRuntimeInfo.getWireMock();
        wireMock.register(get("/instance-dsl").willReturn(ok()));
        // Info such as port numbers is also available
        int port = wmRuntimeInfo.getHttpPort();
        // Do some testing...
    }
}
Code language: Java (java)

Trait of Reactive application to be kept in mind when testing

Another point to be taken in a view is thread model. If you write tests which access database like I do, then you should avoid database in tests on Spring Cloud Gateway.

In tests covering old MVC Spring there was an option to work with database – annotating the test with @Transactional annotation. This approach leads to all datum created for testing will be deleted right after the test automatically. This simple example of this approach is in code below. @Transactional on class or class method opens transaction before the test and do rollback right after the test automatically.

@Transactional
@SpringBootTest
@AutoconfigureMockMvc
class TestMvc {
    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;
    @Test
    void testMockMvc() throws Exception {
        // given
        createAllEntities();
        // when
        mockMvc.perform(post("...")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(objectMapper.writeValueAsBytes(createRequest())))
            // then
            .andExpect(status().isOk());
    }
}
Code language: Java (java)

But this approach does not work in reactive Spring applications. And that’s why.

Take a look at the schema above. The Spring Cloud Gateway block creates new thread for processing every request incoming. Therefore the code of gateway (including the authentication filter) has no possibility to read the datum previously written into database when transaction is not committed.

Breakpoint in the beginning of the test shows following:

Thread name in the beginning of test in Reactive Spring Cloud Gateway

Breakpoint in Filter shows the following:

Thread name in the Filter of Reactive Spring Cloud Gateway

It shows that in reactive gateway application the request is caught in separated thread.

Hint to deal with database in testing Reactive Spring Cloud Gateway

As known, when the application interacts with database, the test covering that application must:

  1. Prepare all data need for test
  2. Perform a call to the application
  3. Remove all data created previously

The easiest way to achieve this is to run test in a transaction. When the application interacts with database in separated threads it needs to replace or spoof all multi-threading work with single-threaded model. Another way to achieve database cleanliness after testing is to get rid of database access in testing. Yes, you can mock the database.

Mocking the database works fine in couple with mocking destination server in testing Spring Cloud Gateway. The simple test is below.

@Configuration
public class SimpleRouter {
    @Bean
    public RouteLocator locator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("stub-route", r -> r.path("/stub")
                        .uri("http://localhost:8080/stub")
                )
                .build();
    }
}
@ActiveProfiles("test")
@Transactional
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(OutputCaptureExtension.class)
@AutoConfigureWireMock
public class AuthFilterTest {
    @LocalServerPort
    int localPort;
    WebTestClient webClient;
    
    @BeforeEach
    public void setup() {
        final String baseUri = "http://localhost:" + localPort;
        webClient = WebTestClient.bindToServer()
                .defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
                .responseTimeout(Duration.ofSeconds(100))
                .baseUrl(baseUri).build();
    }
    @Test
    void testRegistered() throws Exception {
        // given
        final String jwt = generateJwt();
        walletExists();
        stub();
        // when
        requestLeadsToResponse(setup -> setup.header("Authorization", "Bearer " + jwt),
                // then
                200, RESPONSE_OK);
    }
    // mocking gdatabase
    void walletExists() {
        when(accountRepository.findById(eq(ACCOUNT_ID)))
                .thenReturn(Optional.of(aWallet()));
    }
    // mocking gateway destination
    void stub() throws Exception {
        stubFor(get(urlEqualTo("/stub"))
                .withHeader(HEADER_ACCOUNT_ID, new EqualToPattern(ACCOUNT_ID))
                .willReturn(ok(RESPONSE_OK)));
    }
    void requestLeadsToResponse(Consumer<WebTestClient.RequestHeadersSpec<?>> authorizer, int resultCode, String resultBody) throws Exception {
        WebTestClient.RequestHeadersSpec<?> webClientSetup = webClient.get()
                .uri("/stub");
        authorizer.accept(webClientSetup);
        EntityExchangeResult<byte[]> response = webClientSetup.exchange()
                .expectBody()
                // then
                .json(resultBody)
                .returnResult();
        log.warn("RESPONSE: " + (response.getResponseBody() == null ? "NULL" : new String(response.getResponseBody())));
        assertEquals(resultCode, response.getStatus().value());
    }
}
Code language: Java (java)

Another option to try is to set READ_UNCOMMITTED level of transaction isolation on test beginning. This case all the data saved in test transaction will be available in another thread in Filter.

Third, unfortunately, I was unable to find a proper way to replace multi-threading model of Reactive Spring Cloud Gateway with single-threaded model for testing. If you can find it, please tell me in comments below or in my telegram blog comments: https://t.me/bvn13_blog

Leave a Reply

Your email address will not be published. Required fields are marked *