27

我使用 Spring-Cloud-Netflix 进行微服务之间的通信。假设我有两个服务,Foo 和 Bar,Foo 使用 Bar 的 REST 端点之一。我使用带有注释的接口@FeignClient

@FeignClient
public interface BarClient {
  @RequestMapping(value = "/some/url", method = "POST")
  void bazzle(@RequestBody BazzleRequest);
}

然后我SomeService在 Foo 中有一个服务类,它调用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!";
      }

    }
}

现在,为了确保服务之间的通信正常进行,我想构建一个测试,使用 WireMock 之类的东西向一个假的 Bar 服务器触发一个真实的 HTTP 请求。测试应确保 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 能够找到它并与之通信?我需要什么样的注释魔法?

4

7 回答 7

20

这是一个使用 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 客户端调用失败(例如连接超时)时都会调用 Fallback 类。

为了让测试正常工作,我们需要配置 Ribbon 负载均衡器(Feign 客户端在发送 http 请求时会在内部使用):

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

功能区服务器列表需要匹配我们的 WireMock 配置的 url(主机和端口)。

于 2017-11-30T22:20:54.660 回答
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()
            );
        }
    }
}

或者,您可以尝试使用您System.setProperty()@BeforeClass测试方法。

于 2017-08-11T20:21:07.660 回答
5

过去基本上有两种方法可以对微服务应用程序进行集成测试:

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

第一个选项的明显缺点是部署所有依赖项(其他服务、数据库等)也很麻烦。此外,它很慢且难以调试。

第二种选择更快且麻烦更少,但由于可能的代码更改,很容易最终得到行为与现实不同的存根。因此,在部署到 prod 时,可能会进行成功的测试但应用程序失败。

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

于 2017-12-01T11:33:38.173 回答
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()
} 
于 2020-02-04T09:33:13.253 回答
0

我认为这是一个非常有趣但被低估的话题,如何在微服务环境中验证您的通信渠道。确保您的渠道按预期工作非常重要,但我仍然看到大量项目花费时间测试他们的 Feign 客户。

大多数人已经回答了如何为你的 Feign 客户做最少的测试,但让我们把它提升到一个新的水平。

测试普通的 Feign 客户端,请求映射/响应映射/查询映射等只是图片的一小部分。在微服务环境中,您还必须注意服务弹性,例如客户端负载平衡、断路等。

由于是 2021 年,Spring Cloud 标记了 Hystrix 和 Ribbon 已弃用,是时候看看 Resilience4J。

我不会把代码放在这里,因为它可能读起来可能太多了,但我会给你一些指向我的 GitHub 项目之一的链接。

另外,如果没有进一步的解释,这可能有点太理解了,但我不能在一个 stackoverflow 答案中做到这一点,所以你可以查看我的几篇文章以及我关于 Feign 的课程:Mastering microservice communication with Spring Cloud假装

于 2021-12-04T16:39:21.553 回答
0

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

  1. 在测试环境中,您可以在独立的 Jetty servlet 容器下部署 Eureka Service Registry,所有注释都将像在实际生产环境中一样工作。
  2. 如果您不想使用真正的BarClient端点逻辑,并且集成测试只是关于真正的http传输层,那么您可以使用 Mockito 作为BarClient端点存根。

我想为了使用 Spring-Boot 实现 1 和 2,您需要为测试环境制作两个单独的应用程序。一个用于 Jetty 下的 Eureka Service Registry,另一个用于 JettyBarClient下的端点存根。

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

于 2016-09-21T04:20:34.573 回答
-9

Use Spring's RestTemplate instead of feign. RestTemplate is also able to resolve service names via eureka, so you can do something like this:

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

This is way easier testable with Wiremock than feign.

于 2017-02-03T05:13:27.560 回答