I want to test my RouteLocator. I tried to do it in a manner similar to how we test endpoints in a WebFluxTest. Here are my
Attempts
Attempt #1
package com.example.gatewaydemo.filter;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory;
import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.UUID;
@WebFluxTest(controllers = GatewayTest.Config.class)
@ContextConfiguration(classes = GatewayTest.Config.class)
public class GatewayTest {
@Autowired
WebTestClient client;
@Test
void test() {
client.get()
.uri("/original-path")
.exchange()
.expectStatus().isEqualTo(HttpStatus.OK);
}
@Configuration
@EnableAutoConfiguration
static class Config {
@Bean
public RouterFunction<ServerResponse> routerFunction() {
return RouterFunctions.route()
.GET("/eventual-path", request -> ServerResponse.status(HttpStatus.OK).build())
.build();
}
@Bean
public RouteLocator routeLocator() {
// suppose I want to test this RouteLocator
GatewayFilter rewriteFilter = new RewritePathGatewayFilterFactory()
.apply(config -> config.setRegexp("/original-path")
.setReplacement("/eventual-path"));
Route route = Route.async()
.id(UUID.randomUUID().toString())
.filter(new OrderedGatewayFilter(rewriteFilter, 0)) // it's important to wrap
.predicate(new PathRoutePredicateFactory()
.apply(config -> config.setPatterns(List.of("/original-path"))))
.uri("http://localhost:8080")
.build();
return () -> Flux.just(route);
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>gatewaydemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gatewaydemo</name>
<description>gatewaydemo</description>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2022.0.4</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
However, the test fails due to a 404
2024-03-19T18:11:02.607+03:00 ERROR 14904 --- [ main] o.s.t.w.reactive.server.ExchangeResult : Request details for assertion failure:
> GET /original-path
> WebTestClient-Request-Id: [1]
No content
< 404 NOT_FOUND Not Found
< Content-Type: [application/json]
< Content-Length: [140]
{"timestamp":"2024-03-19T15:11:02.447+00:00","path":"/original-path","status":404,"error":"Not Found","message":null,"requestId":"37ea34a7"}
I strongly believe my RouteLocator is ignored. Maybe, it has to do with @WebFluxTest that is not designed to pick it up
Attempt #2
@SpringBootTest
@AutoConfigureWebTestClient
public class GatewayTest {
// the rest is the same
The test still fails, but this time it's a 500. It seems the server doesn't listen on port 8080
2024-03-19T18:36:20.582+03:00 ERROR 8860 --- [ctor-http-nio-2] a.w.r.e.AbstractErrorWebExceptionHandler : [c0fdf3a] 500 Server Error for HTTP GET "/original-path"
io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: no further information: localhost/127.0.0.1:8080
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ HTTP GET "/original-path" [ExceptionHandlingWebHandler]
Original Stack Trace:
Caused by: java.net.ConnectException: Connection refused: no further information
at java.base/sun.nio.ch.Net.pollConnect(Native Method) ~[na:na]
at java.base/sun.nio.ch.Net.pollConnectNow(Net.java:672) ~[na:na]
at java.base/sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:946) ~[na:na]
at io.netty.channel.socket.nio.NioSocketChannel.doFinishConnect(NioSocketChannel.java:337) ~[netty-transport-4.1.100.Final.jar:4.1.100.Final]
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.finishConnect(AbstractNioChannel.java:334) ~[netty-transport-4.1.100.Final.jar:4.1.100.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:776) ~[netty-transport-4.1.100.Final.jar:4.1.100.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724) ~[netty-transport-4.1.100.Final.jar:4.1.100.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650) ~[netty-transport-4.1.100.Final.jar:4.1.100.Final]
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) ~[netty-transport-4.1.100.Final.jar:4.1.100.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.100.Final.jar:4.1.100.Final]
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.100.Final.jar:4.1.100.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.100.Final.jar:4.1.100.Final]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
2024-03-19T18:36:20.699+03:00 ERROR 8860 --- [ main] o.s.t.w.reactive.server.ExchangeResult : Request details for assertion failure:
> GET /original-path
> WebTestClient-Request-Id: [1]
No content
< 500 INTERNAL_SERVER_ERROR Internal Server Error
< Content-Type: [application/json]
< Content-Length: [136]
{"timestamp":"2024-03-19T15:36:20.576+00:00","path":"/original-path","status":500,"error":"Internal Server Error","requestId":"c0fdf3a"}
Attempt #3
webEnvironment, RANDOM_PORT
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
public class GatewayTest {
@Autowired
WebTestClient client;
@LocalServerPort
int port;
@Test
void test() {
client.get()
.uri("/original-path")
.exchange()
.expectStatus().isEqualTo(HttpStatus.OK);
}
@Configuration
@EnableAutoConfiguration
class Config {
// the same RouterFunction
@Bean
public RouteLocator routeLocator() {
GatewayFilter rewriteFilter = new RewritePathGatewayFilterFactory()
.apply(config -> config.setRegexp("/original-path")
.setReplacement("/eventual-path"));
Route route = Route.async()
.id(UUID.randomUUID().toString())
.filter(new OrderedGatewayFilter(rewriteFilter, 0))
.predicate(new PathRoutePredicateFactory()
.apply(config -> config.setPatterns(List.of("/original-path"))))
.uri("http://localhost:" + port)
.build();
return () -> Flux.just(route);
}
}
}
A 404 again
2024-03-19T19:12:15.447+03:00 ERROR 3760 --- [ main] o.s.t.w.reactive.server.ExchangeResult : Request details for assertion failure:
> GET /original-path
> WebTestClient-Request-Id: [1]
No content
< 404 NOT_FOUND Not Found
< Content-Type: [application/json]
< Content-Length: [140]
{"timestamp":"2024-03-19T16:12:15.328+00:00","path":"/original-path","status":404,"error":"Not Found","message":null,"requestId":"5930e136"}
Research
SO
I examined several answers on this topic
- "Unit test Spring Cloud Gateway RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder)". I don't like the solution. It either suggests testing a
RouteLocatorautomatically created from inline properties
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
"spring.cloud.gateway.routes[0].id=test",
"spring.cloud.gateway.routes[0].uri=http://localhost:8081",
"spring.cloud.gateway.routes[0].predicates[0]=Path=/test/**",
}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class NettyRoutingFilterTests {
or involves Spring Cloud Eureka (way to many dependencies for a non-integration test, in my opinion)
@ExtendWith( { SpringExtension.class } )
@SpringBootTest(classes = { MockConfigurer.class },
webEnvironment = WebEnvironment.RANDOM_PORT )
public class RoutingIT
@Configuration
public class MockConfigurer
{
private List<ServiceInstance> services;
public MockConfigurer( List<ServiceInstance> services)
{
this.services= services;
}
@Bean
public DiscoveryClient discoveryClient( )
{
- "Spring Integration Test not working with Spring Boot 3 and Spring Cloud Gateway". It's not clear how the solution works, specifically where
WebTestClientis pulled from
@SpringBootTest(classes = MainApplication.class, webEnvironment = DEFINED_PORT)
public class HealthControllerTest {
@Autowired
private WebTestClient webTestClient;
@Test
public void health_shouldReturnHttp200() { this.webTestClient.get().uri(PATH).accept(MediaType.APPLICATION_JSON).exchange().expectStatus().isOk();
}
}
Doc
I visited the project's documentation page, but haven't found a specific chapter on testing
I found a way (likely not the way) to write a working test. The key is to use a real
WebClientinstead ofWebTestClientSince
@SpringBootTestlooks for nested@Configurations anyway, explicitly referencing it inclassesis optional