Integration Tests With Spring Cloud Netflix and Feign – 与Spring Cloud Netflix和Feign的集成测试

最后修改: 2020年 12月 24日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this article, we’re going to explore the integration testing of a Feign Client.

在这篇文章中,我们将探讨Feign客户端的集成测试

We’ll create a basic Open Feign Client for which we’ll write a simple integration test with the help of WireMock.

我们将创建一个基本的Open Feign客户端我们将在WireMock的帮助下编写一个简单的集成测试

After that, we’ll add a Ribbon configuration to our client and also build an integration test for it. And finally, we’ll configure a Eureka test container and test this setup to make sure our entire configuration works as expected.

之后,我们将为我们的客户端添加Ribbon 配置,同时为其构建一个集成测试。最后,我们将配置一个Eureka测试容器,并测试这个设置,以确保我们的整个配置能够按照预期运行。

2. The Feign Client

2.伪装的客户

To set up our Feign Client, we should first add the Spring Cloud OpenFeign Maven dependency:

要设置我们的Feign客户端,我们应首先添加Spring Cloud OpenFeign Maven依赖项。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

After that, let’s create a Book class for our model:

之后,让我们为我们的模型创建一个Book类。

public class Book {
    private String title;
    private String author;
}

And finally, let’s create our Feign Client interface:

最后,让我们创建我们的Feign Client界面。

@FeignClient(value="simple-books-client", url="${book.service.url}")
public interface BooksClient {

    @RequestMapping("/books")
    List<Book> getBooks();

}

Now, we have a Feign Client that retrieves a list of Books from a REST service. Now, let’s move forward and write some integration tests.

现在,我们有了一个Feign客户端,可以从REST服务中检索Books列表。现在,让我们继续前进,编写一些集成测试。

3. WireMock

3.仿真器(WireMock

3.1. Setting up the WireMock Server

3.1.设置WireMock服务器

If we want to test our BooksClient, we need a mock service that provides the /books endpoint. Our client will make calls against this mock service. For this purpose, we’ll use WireMock.

如果我们想测试我们的BooksClient,我们需要一个模拟服务来提供/books端点。我们的客户端将针对这个模拟服务进行调用。为此,我们将使用WireMock。

So, let’s add the WireMock Maven dependency:

因此,让我们添加WireMock的Maven依赖。

<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock</artifactId>
    <scope>test</scope>
</dependency>

and configure the mock server:

并配置模拟服务器。

@TestConfiguration
public class WireMockConfig {

    @Autowired
    private WireMockServer wireMockServer;

    @Bean(initMethod = "start", destroyMethod = "stop")
    public WireMockServer mockBooksService() {
        return new WireMockServer(9561);
    }

}

We now have a running mock server accepting connections on port 9651.

我们现在有一个正在运行的模拟服务器,接受9651端口的连接。

3.2. Setting up the Mock

3.2.设置模拟程序

Let’s add the property book.service.url to our application-test.yml pointing to the WireMockServer port:

让我们将属性book.service.url添加到我们的application-test.yml,指向WireMockServer端口。

book:
  service:
    url: http://localhost:9561

And let’s also prepare a mock response get-books-response.json for the /books endpoint:

让我们也为/books端点准备一个模拟的响应get-books-response.json

[
  {
    "title": "Dune",
    "author": "Frank Herbert"
  },
  {
    "title": "Foundation",
    "author": "Isaac Asimov"
  }
]

Let’s now configure the mock response for a GET request on the /books endpoint:

现在让我们为/books端点上的GET请求配置模拟响应。

public class BookMocks {

    public static void setupMockBooksResponse(WireMockServer mockService) throws IOException {
        mockService.stubFor(WireMock.get(WireMock.urlEqualTo("/books"))
          .willReturn(WireMock.aResponse()
            .withStatus(HttpStatus.OK.value())
            .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
            .withBody(
              copyToString(
                BookMocks.class.getClassLoader().getResourceAsStream("payload/get-books-response.json"),
                defaultCharset()))));
    }

}

At this point, all the required configuration is in place. Let’s go ahead and write our first test.

在这一点上,所有需要的配置都已到位。让我们继续写我们的第一个测试。

4. Our First Integration Test

4.我们的第一次集成测试

Let’s create an integration test BooksClientIntegrationTest:

让我们创建一个集成测试BooksClientIntegrationTest

@SpringBootTest
@ActiveProfiles("test")
@EnableConfigurationProperties
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { WireMockConfig.class })
class BooksClientIntegrationTest {

    @Autowired
    private WireMockServer mockBooksService;

    @Autowired
    private BooksClient booksClient;

    @BeforeEach
    void setUp() throws IOException {
        BookMocks.setupMockBooksResponse(mockBooksService);
    }

    // ...
}

At this point, we have a SpringBootTest configured with a WireMockServer ready to return a predefined list of Books when the /books endpoint is invoked by the BooksClient.

在这一点上,我们有一个SpringBootTest,配置了一个WireMockServer,准备在/books端点被BooksClient调用时,返回一个预定义的Books>列表。

And finally, let’s add our test methods:

最后,让我们添加我们的测试方法。

@Test
public void whenGetBooks_thenBooksShouldBeReturned() {
    assertFalse(booksClient.getBooks().isEmpty());
}

@Test
public void whenGetBooks_thenTheCorrectBooksShouldBeReturned() {
    assertTrue(booksClient.getBooks()
      .containsAll(asList(
        new Book("Dune", "Frank Herbert"),
        new Book("Foundation", "Isaac Asimov"))));
}

5. Integrating with Ribbon

5.与Ribbon的整合

Now let’s improve our client by adding the load-balancing capabilities provided by Ribbon.

现在让我们通过添加Ribbon提供的负载平衡功能来改进我们的客户端

All we need to do in the client interface is to remove the hard-coded service URL and instead refer to the service by the service name book-service:

我们在客户接口中需要做的就是删除硬编码的服务URL,而用服务名称book-service来引用该服务。

@FeignClient("books-service")
public interface BooksClient {
...

Next, add the Netflix Ribbon Maven dependency:

接下来,添加Netflix Ribbon的Maven依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

And finally, in the application-test.yml file, we should now remove the book.service.url and instead define the Ribbon listOfServers:

最后,在application-test.yml文件中,我们现在应该删除book.service.url,而是定义RibbonlistOfServers

books-service:
  ribbon:
    listOfServers: http://localhost:9561

Let’s now run the BooksClientIntegrationTest again. It should pass, confirming the new setup works as expected.

现在让我们再次运行BooksClientIntegrationTest。它应该通过,确认新的设置按预期工作。

5.1. Dynamic Port Configuration

5.1.动态端口配置

If we don’t want to hard-code the server’s port, we can configure WireMock to use a dynamic port at startup.

如果我们不想对服务器的端口进行硬编码,我们可以将WireMock配置为在启动时使用一个动态端口。

For this, let’s create another test configuration, RibbonTestConfig:

为此,让我们创建另一个测试配置,RibbonTestConfig:

@TestConfiguration
@ActiveProfiles("ribbon-test")
public class RibbonTestConfig {

    @Autowired
    private WireMockServer mockBooksService;

    @Autowired
    private WireMockServer secondMockBooksService;

    @Bean(initMethod = "start", destroyMethod = "stop")
    public WireMockServer mockBooksService() {
        return new WireMockServer(options().dynamicPort());
    }

    @Bean(name="secondMockBooksService", initMethod = "start", destroyMethod = "stop")
    public WireMockServer secondBooksMockService() {
        return new WireMockServer(options().dynamicPort());
    }

    @Bean
    public ServerList ribbonServerList() {
        return new StaticServerList<>(
          new Server("localhost", mockBooksService.port()),
          new Server("localhost", secondMockBooksService.port()));
    }

}

This configuration sets up two WireMock servers, each running on a different port dynamically assigned at runtime. Moreover, it also configures the Ribbon server list with the two mock servers.

这个配置设置了两个WireMock服务器,每个都运行在运行时动态分配的不同端口上。此外,它还在Ribbon服务器列表中配置了这两个模拟服务器。

5.2. Load Balancing Testing

5.2.负载平衡测试

Now that we have our Ribbon load balancer configured, let’s make sure our BooksClient correctly alternates between the two mock servers:

现在,我们已经配置了Ribbon负载平衡器,让我们确保我们的BooksClient在两个模拟服务器之间正确交替使用:

@SpringBootTest
@ActiveProfiles("ribbon-test")
@EnableConfigurationProperties
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = { RibbonTestConfig.class })
class LoadBalancerBooksClientIntegrationTest {

    @Autowired
    private WireMockServer mockBooksService;

    @Autowired
    private WireMockServer secondMockBooksService;

    @Autowired
    private BooksClient booksClient;

    @BeforeEach
    void setUp() throws IOException {
        setupMockBooksResponse(mockBooksService);
        setupMockBooksResponse(secondMockBooksService);
    }

    @Test
    void whenGetBooks_thenRequestsAreLoadBalanced() {
        for (int k = 0; k < 10; k++) {
            booksClient.getBooks();
        }

        mockBooksService.verify(
          moreThan(0), getRequestedFor(WireMock.urlEqualTo("/books")));
        secondMockBooksService.verify(
          moreThan(0), getRequestedFor(WireMock.urlEqualTo("/books")));
    }

    @Test
    public void whenGetBooks_thenTheCorrectBooksShouldBeReturned() {
        assertTrue(booksClient.getBooks()
          .containsAll(asList(
            new Book("Dune", "Frank Herbert"),
            new Book("Foundation", "Isaac Asimov"))));
    }
}

6. Eureka Integration

6.尤里卡整合

We have seen, so far, how to test a client that uses Ribbon for load balancing. But what if our setup uses a service discovery system like Eureka. We should write an integration test that makes sure that our BooksClient works as expected in such a context also.

到目前为止,我们已经看到如何测试一个使用Ribbon进行负载平衡的客户端。但是如果我们的设置使用了像Eureka这样的服务发现系统。我们应该写一个集成测试,确保我们的BooksClient在这种情况下也能按预期工作

For this purpose, we’ll run a Eureka server as a test container. Then we startup and register a mock book-service with our Eureka container. And finally, once this installation is up, we can run our test against it.

为此,我们将运行一个Eureka服务器作为一个测试容器。然后我们启动并注册一个模拟的book-service与我们的Eureka容器。最后,一旦这个安装好了,我们就可以针对它运行我们的测试。

Before moving further, let’s add the Testcontainers and Netflix Eureka Client Maven dependencies:

在进一步行动之前,让我们添加TestcontainersNetflix Eureka Client Maven依赖项。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <scope>test</scope>
</dependency>

6.1. TestContainer Setup

6.1.TestContainer设置

Let’s create a TestContainer configuration that will spin up our Eureka server:

让我们创建一个TestContainer配置,它将启动我们的Eureka服务器。

public class EurekaContainerConfig {

    public static class Initializer implements ApplicationContextInitializer {

        public static GenericContainer eurekaServer = 
          new GenericContainer("springcloud/eureka").withExposedPorts(8761);

        @Override
        public void initialize(@NotNull ConfigurableApplicationContext configurableApplicationContext) {

            Startables.deepStart(Stream.of(eurekaServer)).join();

            TestPropertyValues
              .of("eureka.client.serviceUrl.defaultZone=http://localhost:" 
                + eurekaServer.getFirstMappedPort().toString() 
                + "/eureka")
              .applyTo(configurableApplicationContext);
        }
    }
}

As we can see, the initializer above starts the container. Then it exposes port 8761, on which the Eureka server is listening.

我们可以看到,上面的初始化程序启动了容器。然后它暴露了8761端口,Eureka服务器正在监听这个端口。

And finally, after the Eureka service has started, we need to update the eureka.client.serviceUrl.defaultZone property. This defines the address of the Eureka server used for service discovery.

最后,在Eureka服务启动后,我们需要更新eureka.client.serviceUrl.defaultZone属性。这定义了用于服务发现的Eureka服务器的地址。

6.2. Register Mock Server

6.2.注册模拟服务器

Now that our Eureka server is up and running we need to register a mock books-service. We do this by simply creating a RestController:

现在我们的Eureka服务器已经启动并运行,我们需要注册一个模拟的books-service。我们通过简单地创建一个RestController来做到这一点。

@Configuration
@RestController
@ActiveProfiles("eureka-test")
public class MockBookServiceConfig {

    @RequestMapping("/books")
    public List getBooks() {
        return Collections.singletonList(new Book("Hitchhiker's Guide to the Galaxy", "Douglas Adams"));
    }
}

All we have to do now, in order to register this controller, is to make sure the spring.application.name property in our application-eureka-test.yml is books-service, the same as the service name used in the BooksClient interface.

为了注册这个控制器,我们现在要做的就是确保spring.application.name属性在我们的application-ureka-test.yml中是books-service,BooksClient接口中使用的服务名相同。

Note: Now that the netflix-eureka-client library is in our list of dependencies, Eureka will be used by default for service discovery. So, if we want our previous tests, that don’t use Eureka, to keep passing, we’ll need to manually set eureka.client.enabled to false. In that way, even if the library is on the path, the BooksClient will not try to use Eureka for locating the service, but instead, use the Ribbon configuration.

注意:现在netflix-eureka-client库已经在我们的依赖列表中,Eureka将被默认用于服务发现。因此,如果我们想让我们之前的测试(不使用Eureka)继续通过,我们需要手动设置eureka.client.enabled为 false。这样一来,即使库在路径上,BooksClient也不会尝试使用Eureka来定位服务,而是使用Ribbon配置。

6.3. Integration Test

6.3.集成测试

Once again, we have all the needed configuration pieces, so let’s put them all together in a test:

再一次,我们有了所有需要的配置件,所以让我们把它们都放在一起进行测试。

@ActiveProfiles("eureka-test")
@EnableConfigurationProperties
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Application.class, webEnvironment =  SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(classes = { MockBookServiceConfig.class }, 
  initializers = { EurekaContainerConfig.Initializer.class })
class ServiceDiscoveryBooksClientIntegrationTest {

    @Autowired
    private BooksClient booksClient;

    @Lazy
    @Autowired
    private EurekaClient eurekaClient;

    @BeforeEach
    void setUp() {
        await().atMost(60, SECONDS).until(() -> eurekaClient.getApplications().size() > 0);
    }

    @Test
    public void whenGetBooks_thenTheCorrectBooksAreReturned() {
        List books = booksClient.getBooks();

        assertEquals(1, books.size());
        assertEquals(
          new Book("Hitchhiker's guide to the galaxy", "Douglas Adams"), 
          books.stream().findFirst().get());
    }

}

There are a few things happening in this test. Let’s look at them one by one.

在这个测试中,有几件事情发生。让我们逐一看一下。

Firstly, the context initializer inside EurekaContainerConfig starts the Eureka service.

首先,EurekaContainerConfig内的上下文初始化器启动Eureka服务。

Then, the SpringBootTest starts the books-service application that exposes the controller defined in MockBookServiceConfig.

然后,SpringBootTest启动books-service应用程序,该应用程序暴露了MockBookServiceConfig中定义的控制器。

Because the startup of the Eureka container and the web application can take a few seconds, we need to wait until the books-service gets registered. This happens in the setUp of the test.

因为Eureka容器和Web应用程序的启动可能需要几秒钟,我们需要等待,直到books-service被注册。这发生在测试的setUp中。

And finally, the tests method verifies that the BooksClient indeed works correctly in combination with the Eureka configuration.

最后,测试方法验证了BooksClient在与Eureka配置结合时确实能正常工作。

7. Conclusion

7.结语

In this article, we’ve explored the different ways we can write integration tests for a Spring Cloud Feign Client. We started with a basic client which we tested with the help of WireMock. After that, we moved on to adding load balancing with Ribbon. We wrote an integration test and made sure our Feign Client works correctly with the client-side load balancing provided by Ribbon. And finally, we added Eureka service discovery to the mix. And again, we made sure our client still works as expected.

在这篇文章中,我们探讨了为Spring Cloud Feign客户端编写集成测试的不同方式。我们从一个基本的客户端开始,在WireMock的帮助下进行测试。之后,我们继续用Ribbon添加负载均衡。我们写了一个集成测试,确保我们的Feign客户端能与Ribbon提供的客户端负载均衡正常工作。最后,我们把Eureka服务发现加入到这个组合中。再一次,我们确保我们的客户端仍能按预期工作。

As always, the complete code is available over on GitHub.

一如既往,完整的代码可在GitHub上获得