Testing Spring Cloud Gateway

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

Comments |0|

Legend *) Required fields are marked
**) You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>