如何使用WireMock对spring-cloud-netflix和Feign进行集成测试。

28
我在微服务之间使用Spring-Cloud-Netflix进行通信。假设我有两个服务,Foo和Bar,Foo消费了Bar的一个REST端点。我使用一个带有@FeignClient注解的接口:
@FeignClient
public interface BarClient {
  @RequestMapping(value = "/some/url", method = "POST")
  void bazzle(@RequestBody BazzleRequest);
}

然后我在Foo中有一个服务类SomeService,它调用了BarClient

@Component
public class SomeService {
    @Autowired
    BarClient barClient;

    public String doSomething() {
      try {
        barClient.bazzle(new BazzleRequest(...));
        return "so bazzle my eyes dazzle";
      } catch(FeignException e) {
        return "Not bazzle today!";
      }

    }
}

现在,为了确保服务之间的通信正常工作,我想构建一个测试,对一个“假”的 Bar 服务器发送真实的 HTTP 请求,使用 WireMock。该测试应该确保 Feign 正确解码服务响应并将其报告给 SomeService。
public class SomeServiceIntegrationTest {

    @Autowired SomeService someService;

    @Test
    public void shouldSucceed() {
      stubFor(get(urlEqualTo("/some/url"))
        .willReturn(aResponse()
            .withStatus(204);

      String result = someService.doSomething();

      assertThat(result, is("so bazzle my eyes dazzle"));
    }

    @Test
    public void shouldFail() {
      stubFor(get(urlEqualTo("/some/url"))
        .willReturn(aResponse()
            .withStatus(404);

      String result = someService.doSomething();

      assertThat(result, is("Not bazzle today!"));
    }
}

如何成功地将 WireMock 服务器注入到 Eureka 中,以便 Feign 能够找到它并与之通信?

我试图为您提供答案,但我理解您的目标可能并不是很好。如果您谈论集成测试,则无需模拟BarClient逻辑。如果这样做,那么您的测试将是单元测试,而不是集成测试。如果这是一个单元测试,那么您可以使用Mokito简单地模拟BarClient,而不需要进行任何http请求。我不明白为什么您需要http请求? - Sergey Bespalov
5
我不想编写集成测试来集成多个微服务。当我说集成测试时,我的意思是测试FooService中所有技术层的集成,而不是只测试一个类并用模拟或存根替换其他部分的单元测试。 - Bastian Voigt
你看过Spring Boot 1.4中的RestClientTest和它的MockRestServiceServer吗? - Tim
你找到了实现这个的方法吗?我也在尝试着做同样的事情。运行微服务时,所有外部依赖(例如Eureka服务器)都被模拟为进程外部。 - Raipe
正如您在下面的答案中所看到的,我已经切换到了RestTemplate。 - Bastian Voigt
7个回答

22

这是使用WireMock测试SpringBoot配置的示例,其中包括Feign客户端和Hystrix回退。

如果您正在使用Eureka作为服务器发现,您需要通过设置属性"eureka.client.enabled=false"来禁用它。

首先,我们需要为我们的应用启用Feign/Hystrix配置:

@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@FeignClient(
        name = "bookstore-server",
        fallback = BookClientFallback.class,
        qualifier = "bookClient"
)
public interface BookClient {

    @RequestMapping(method = RequestMethod.GET, path = "/book/{id}")
    Book findById(@PathVariable("id") String id);
}

@Component
public class BookClientFallback implements BookClient {

    @Override
    public Book findById(String id) {
        return Book.builder().id("fallback-id").title("default").isbn("default").build();
    }
}
请注意,我们为Feign客户端指定了一个回退类。当Feign客户端调用失败时(例如连接超时),将调用回退类。
为了使测试正常工作,我们需要配置Ribbon负载均衡器(在发送http请求时,Feign客户端将在内部使用该负载均衡器)。
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
        "feign.hystrix.enabled=true"
})
@ContextConfiguration(classes = {BookClientTest.LocalRibbonClientConfiguration.class})
public class BookClientTest {

    @Autowired
    public BookClient bookClient;

    @ClassRule
    public static WireMockClassRule wiremock = new WireMockClassRule(
            wireMockConfig().dynamicPort()));

    @Before
    public void setup() throws IOException {
        stubFor(get(urlEqualTo("/book/12345"))
                .willReturn(aResponse()
                        .withStatus(HttpStatus.OK.value())
                        .withHeader("Content-Type", MediaType.APPLICATION_JSON)
                        .withBody(StreamUtils.copyToString(getClass().getClassLoader().getResourceAsStream("fixtures/book.json"), Charset.defaultCharset()))));
    }

    @Test
    public void testFindById() {
        Book result = bookClient.findById("12345");

        assertNotNull("should not be null", result);
        assertThat(result.getId(), is("12345"));
    }

    @Test
    public void testFindByIdFallback() {
        stubFor(get(urlEqualTo("/book/12345"))
                .willReturn(aResponse().withFixedDelay(60000)));

        Book result = bookClient.findById("12345");

        assertNotNull("should not be null", result);
        assertThat(result.getId(), is("fallback-id"));
    }

    @TestConfiguration
    public static class LocalRibbonClientConfiguration {
        @Bean
        public ServerList<Server> ribbonServerList() {
            return new StaticServerList<>(new Server("localhost", wiremock.port()));
        }
    }
}

需要确保 Ribbon 服务器列表与我们的 WireMock 配置的 URL(主机和端口)匹配。


请注意,此方法依赖于默认情况下禁用Hystrix,否则可能会在实时中使用回退。Spring Cloud Dalston引入了所需的选择加入方法,但在此之前,默认情况下将启用Hystrix。 - haggisandchips
这应该是被接受的答案,但它并没有测试反序列化。 - haggisandchips
对我来说完美地工作了。 请注意几点:\n如果您使用的是JUnit 4.11或更高版本,请添加以下内容:@Rule public WireMockClassRule instanceRule = wiremock - Piyush
1
此外,如果您正在测试一个Spring Boot 2应用程序,你需要依赖于com.github.tomakehurst:wiremock-jre8:2.21.0 - Piyush

7

这里有一个示例,展示了如何使用随机端口进行Feign和WireMock的连接(基于Spring-Boot github的回答)。

@RunWith(SpringRunner.class)
@SpringBootTest(properties = "google.url=http://google.com") // emulate application.properties
@ContextConfiguration(initializers = PortTest.RandomPortInitializer.class)
@EnableFeignClients(clients = PortTest.Google.class)
public class PortTest {

    @ClassRule
    public static WireMockClassRule wireMockRule = new WireMockClassRule(
        wireMockConfig().dynamicPort()
    );

    @FeignClient(name = "google", url = "${google.url}")
    public interface Google {    
        @RequestMapping(method = RequestMethod.GET, value = "/")
        String request();
    }

    @Autowired
    public Google google;

    @Test
    public void testName() throws Exception {
        stubFor(get(urlEqualTo("/"))
                .willReturn(aResponse()
                        .withStatus(HttpStatus.OK.value())
                        .withBody("Hello")));

        assertEquals("Hello", google.request());
    }


    public static class RandomPortInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {

            // If the next statement is commented out, 
            // Feign will go to google.com instead of localhost
            TestPropertySourceUtils
                .addInlinedPropertiesToEnvironment(applicationContext,
                    "google.url=" + "http://localhost:" + wireMockRule.port()
            );
        }
    }
}

或者你可以在测试的@BeforeClass方法中尝试使用System.setProperty()


嘿,谢谢你的回答!这是否涉及到Eureka?google.url是什么? - Bastian Voigt
你好!'google.url'是一个任意的属性名称,用于保存被依赖服务的URL(例如此示例中的Google主页)。Eureka不予考虑。我尝试覆盖application.properties值,最终得出了这个解决方案。 - Alexander
TestPropertySourceUtils是一个很好的提示。谢谢你。我希望我能在官方文档中找到它。 - Anddo

5

以前进行微服务应用程序的集成测试基本上有两个选择:

  1. 部署服务到测试环境并进行端到端测试
  2. 模拟其他微服务

第一种选择的明显缺点是需要部署所有依赖项(其他服务、数据库等),而且速度慢,很难调试。

第二种选择更快,更少麻烦,但由于可能的代码更改,很容易出现与现实不同的存根。因此,测试可能成功,但在部署到生产环境时应用程序失败。

更好的解决方案是使用消费者驱动的合同验证,以确保提供者服务的API符合消费者的调用。为此,Spring开发人员可以使用Spring Cloud Contract。对于其他环境,有一个名为PACT的框架。两者都可以与Feign客户端一起使用。这里是PACT的一个示例。


2
我认为这是一个非常有趣但被低估的话题,即如何在微服务环境中验证通信渠道。确保通道按预期工作实际上是至关重要的,但我仍然看到许多项目花费时间测试它们的Feign客户端。
大多数人已经回答了如何对您的Feign客户端进行最小化测试,但让我们将其提升到下一个级别。
测试纯Feign客户端、请求映射/响应映射/查询映射等只是整个过程的一小部分。在微服务环境中,您还必须注意服务的弹性,例如客户端负载平衡、断路器等。
由于现在是2021年,Spring Cloud标记Hystrix和Ribbon已过时,因此是时候看看Resilience4J了。
我不会在这里放置代码,因为可能太多了,但我会给您一些我的GitHub项目链接。
以下是使用Resilience4J实现熔断和时间限制的Feign客户端配置示例:FeignConfiguration 以下是CircuitBreaker测试示例:CircuitBreakerTest 以下是TimeLimiter测试示例:TimeLimiterTest 以下是客户端负载均衡测试示例:LoadBalancingTest 此外,这可能有点难以理解,没有更进一步的解释,我无法在一个stackoverflow答案中完成,所以您可以查看一些我的文章,以及我的Feign课程:使用Spring Cloud Feign掌握微服务通信

2
我个人更喜欢使用mockServer来模拟任何restful API,它易于使用,类似于wiremock,但与后者相比非常强大。
我附上了用groovy/spock编写的样本代码,用于使用mockServer存根GET restful调用。
首先,在测试类中自动装配mockServer实例。
@Autowired
private static ClientAndServer mockServer

从setupSpec()方法开始mockServer实例,这个方法类似于带有@BeforeClass注解的junit方法。

def setupSpec() {
     mockServer = ClientAndServer.startClientAndServer(8080)
   }

在相应的单元测试中定义所需的存根。
def "test case"() {
 given:
       new MockServerClient("localhost",8080).when(HttpRequest.request().withMethod("GET").withPath("/test/api").withQueryStringParameters(Parameter.param("param1", "param1_value"), Parameter.param("param2", "param2_value"))).respond(HttpResponse.response().withStatusCode(HttpStatus.OK.value()).withBody("{ message: 'sample response' }"))

 when:
 //your code
 then:
 //your code
}

在执行测试用例后,停止模拟服务器。

def cleanupSpec() {
     mockServer.stop()
} 

0

可能没有办法让WireMock直接与Eureka Server通信,但您可以使用其他变体来配置所需的测试环境。

  1. 在测试环境中,您可以在独立的Jetty servlet容器下部署Eureka服务注册表,并且所有注释将像在实际生产环境中一样工作。
  2. 如果您不想使用真正的BarClient端点逻辑,并且集成测试仅涉及真正的http传输层,则可以使用Mockito进行BarClient端点存根。

我认为要使用Spring-Boot实现1和2,您需要为测试环境制作两个单独的应用程序。一个用于在Jetty下的Eureka服务注册表,另一个用于在Jetty下的BarClient端点存根。

另一种解决方案是在测试应用程序上下文中手动配置Jetty和Eureka。我认为这是更好的方法,但在这种情况下,您必须了解@EnableEurekaServer@EnableDiscoveryClient注释对Spring应用程序上下文的影响。


嗨,Sergey,感谢您的回答!将Eureka注册表添加到我的服务中听起来不错,但是我该如何将我的虚假“BarService”添加到其中呢?考虑到您的第二个建议,用Mockito替换“BarClient”,是的,我也想这样做,但那只是针对单元测试。我还想进行涉及真正的Feign魔法的集成测试。 - Bastian Voigt

-9
使用Spring的RestTemplate代替Feign。 RestTemplate也可以通过Eureka解析服务名称,因此您可以执行以下操作:
@Component
public class SomeService {
   @Autowired
   RestTemplate restTemplate;

   public String doSomething() {
     try {
       restTemplate.postForEntity("http://my-service/some/url", 
                                  new BazzleRequest(...), 
                                  Void.class);
       return "so bazzle my eyes dazzle";
     } catch(HttpStatusCodeException e) {
       return "Not bazzle today!";
     }
   }
}

使用Wiremock比使用Feign更容易进行测试。


2
但是这并没有使用Feign,当然,如果您使用RestTemplate,它是有效的,但问题是关于SpringBoot中的Feign。人们使用Feign是因为它比RestTemplate更好用... - JeeBee
4
这并没有回答原始问题。你完全改变了你的策略,这意味着原始问题对你不适用,但它仍然是一个有效的问题(也正是我正在尝试做的),而这并不是它的答案。 - haggisandchips
@haggisandchips 无论如何,对我来说它解决了问题 :) - Bastian Voigt
真的不明白为什么大家都那么讨厌我的答案。毕竟这是我的问题,也是我当时完美运作的解决方案。无论如何,我现在已经完全不再使用spring-cloud了,因为整个东西有点儿糟糕和不稳定。 - Bastian Voigt
也许如果您只回答问题,那么人们会给您点赞。顺便说一下,如果您使用发现服务,那么RestTemplate是一个非常糟糕的解决方案,因为它们不能直接进行服务发现。 - Martijn Hiemstra

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接