PatPanda
PatPanda

Reputation: 5060

Unit test Spring Cloud Gateway RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder)

I would like to properly unit test the Spring Cloud Gateway RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) { method with JUnit5.

However, I am having a hard time figuring out what to test, what to assert, what to mock, how to improve coverage, etc... If possible, I just want to unit test this, no need to start an entire SpringTest etc.

@Bean
    @Override
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
        return routeLocatorBuilder.routes()
                .route("forward_to_service_one", r -> r.path("/serviceone/**").and().uri("http://the-first-service:8080"))
                .route("forward_to_service_two", r -> r.path("/servicetwo/**").and().uri("http://the-second-service:8080"))
                .route("forward_to_service_three", r -> r.alwaysTrue().and().order(Ordered.LOWEST_PRECEDENCE).uri("http://the-default-third-service:8080"))
                .build();
    }

While working with integration tests, hit the gateway service that is started on the endpoint, seeing the requests forwarded to respective services, I was wondering if there is a good practice to test this Spring Cloud Gateway feature.

Any example of fully covered test cases please?

Thank you

Upvotes: 3

Views: 7473

Answers (1)

cmlonder
cmlonder

Reputation: 2550

I could not understand your test scenarios (what do you want to test, if service is configured correctly for the path or?) But I would like to show you 2 ways, first one is basic one and the second one is more complicated one if you need more control.

Simple

This will be straightforward, I'm adding some routes to my SpringBootTest properties, I use WebTestClient utility that provided by Spring to me for Reactive tests agains Netty. Then in my test I just send request to this /test endpoint and expect that it is configured (based on your implementation, if you don't extend spring cloud gateway I can say this test is useless, we should not test spring cloud gateway features, but anyway this is what I understand from your description)

@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 {

    @Autowired
    private ApplicationContext context;

    @Test
    @Ignore
    public void mockServerWorks() {
        WebTestClient client = WebTestClient.bindToApplicationContext(this.context)
                .build();
        client.get().uri("/test").exchange().expectStatus().isOk();
    }

Complicated

So second way to do it could be; set your mock route locators to the context from your source code and call your services, assert your response. This is different then setting routes from SpringBootProperties when you need some control for some reason (in my case we are using Contract Tests which I'm not going to in details), but here is some mock which I did not try complete example (but the same method in my projects) but it should give you the idea and some starting point;

@ExtendWith( { SpringExtension.class } )
@SpringBootTest(classes = { MockConfigurer.class },
webEnvironment = WebEnvironment.RANDOM_PORT )
public class RoutingIT
{

@LocalServerPort
private int port;

You should mock the routes like following, so this will return our ServiceInstance when requested. In next step we will also put our ServiceInstance to the context. (I'm using discovery client here where my routes are returned from consul/eureka, but important point here is there are RouteDefinitions in the context. If you are using another locater, check RouteDefinitionLocator implementation and inject corresponding routes to your context based on that);

@Configuration
public class MockConfigurer
{
    private List<ServiceInstance> services;

    public MockConfigurer( List<ServiceInstance> services)
    {
        this.services= services;
    }

    @Bean
    public DiscoveryClient discoveryClient( )
    {
        final DiscoveryClient mock = mock( DiscoveryClient.class );
        final Map<String, List<ServiceInstance>> clusters =
            this.services.stream( ).collect( Collectors.groupingBy( ServiceInstance::getServiceId ) );
        given( mock.getServices( ) ).willReturn( new ArrayList<>( clusters.keySet( ) ) );
        clusters.forEach( ( clusterId, services ) -> given( mock.getInstances( clusterId ) ).willReturn( services ) );
        return mock;
    }
}

Now implement a MockService in your tests;

public class MockService implements ServiceInstance
{
    // fields, constructors

    @Override
    public String getServiceId( )
    {
        return id;
    }

    @Override
    public int getPort( )
    {
        return port;
    }

    // and other functions as well, but you will get the point

Create instances of this MockService in your test and inject them to spring context so that they can be discovered our previous MockConfigurer as a service;

@Bean
public static MockService mockClusterInstance1( )
{
    return new MockService("test", 8081, // more fields based on your implementation, also pay attention this is what we defined in the @SpringBootTest annotation);
}

Now everything is ready to test.

@Test
public void should_GetResponseFromTest_WhenCalled( ) throws Exception
{
    URI uri= new URI( "http://localhost:" + this.port+ "/test");
    ResponseEntity<String> res = this.restTemplate.getForEntity( uri, String.class );
    assertThat( res.getStatusCodeValue( ) ).isEqualTo( HttpURLConnection.HTTP_OK );
    assertThat( res.getBody( ) ).isEqualTo( // your expectation );

Upvotes: 2

Related Questions