Unit test Spring Cloud Gateway customRouteLocator method using RouteLocatorBuilder.

huangapple go评论71阅读模式
英文:

Unit test Spring Cloud Gateway RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder)

问题

我想使用JUnit 5来适当地对Spring Cloud Gateway的customRouteLocator(RouteLocatorBuilder routeLocatorBuilder)方法进行单元测试。

然而,我在弄清楚要测试什么、断言什么、模拟什么、如何提高覆盖率等方面遇到了困难...
如果可能的话,我只想对这个进行单元测试,不需要启动整个SpringTest等。

@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();
}

在使用集成测试时,访问在端点上启动的网关服务,查看请求转发到各自的服务,我想知道是否有一种良好的实践方法来测试这个Spring Cloud Gateway功能。

请提供一些完全覆盖测试用例的示例好吗?

谢谢


<details>
<summary>英文:</summary>

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

</details>


# 答案1
**得分**: 2

以下是翻译好的内容:

我无法理解您的测试场景(您想要测试什么,如果服务是否正确配置了路径等)。但我想向您展示两种方法,第一种是基本方法,第二种是更复杂的方法,如果您需要更多的控制。

## 简单方法

这将是直接的。我在我的SpringBootTest属性中添加了一些路由,我使用了Spring提供给我用于针对Netty进行响应式测试的WebTestClient实用程序。然后在我的测试中,我只是发送请求到这个 **/test** 端点,并期望它被配置好(根据您的实现,如果您没有扩展Spring Cloud Gateway,我可以说这个测试是无用的,我们不应该测试Spring Cloud Gateway的功能,但无论如何这是我从您的描述中理解的)

```java
@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();
    }
}

复杂方法

第二种方法是:从您的源代码中设置模拟路由定位器到上下文中,并调用您的服务,断言您的响应。这与从SpringBootProperties设置路由不同,当您出于某种原因需要一些控制时(在我的情况下,我们正在使用契约测试,我不会详细介绍),但是这里是我没有完全尝试的一些模拟示例(但在我的项目中使用相同的方法),但它应该给您一个想法和一个起点。

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

    @LocalServerPort
    private int port;

    // 其他部分省略
}

@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;
    }
}

// 其他部分省略

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
}

// 其他部分省略
英文:

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 = {
&quot;spring.cloud.gateway.routes[0].id=test&quot;,
&quot;spring.cloud.gateway.routes[0].uri=http://localhost:8081&quot;,
&quot;spring.cloud.gateway.routes[0].predicates[0]=Path=/test/**&quot;,
}, 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(&quot;/test&quot;).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&lt;ServiceInstance&gt; services;
public MockConfigurer( List&lt;ServiceInstance&gt; services)
{
this.services= services;
}
@Bean
public DiscoveryClient discoveryClient( )
{
final DiscoveryClient mock = mock( DiscoveryClient.class );
final Map&lt;String, List&lt;ServiceInstance&gt;&gt; clusters =
this.services.stream( ).collect( Collectors.groupingBy( ServiceInstance::getServiceId ) );
given( mock.getServices( ) ).willReturn( new ArrayList&lt;&gt;( clusters.keySet( ) ) );
clusters.forEach( ( clusterId, services ) -&gt; 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(&quot;test&quot;, 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( &quot;http://localhost:&quot; + this.port+ &quot;/test&quot;);
ResponseEntity&lt;String&gt; res = this.restTemplate.getForEntity( uri, String.class );
assertThat( res.getStatusCodeValue( ) ).isEqualTo( HttpURLConnection.HTTP_OK );
assertThat( res.getBody( ) ).isEqualTo( // your expectation );

huangapple
  • 本文由 发表于 2020年8月31日 22:14:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/63672491.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定