1. 概要

この記事では、FeignClient統合テストについて説明します。

基本的なOpenFeign Client を作成し、 WireMockを使用して簡単な統合テストを作成します。

その後、クライアントリボン構成を追加し、その統合テストを構築します。 最後に、 Eureka テストコンテナーを構成し、このセットアップをテストして、構成全体が期待どおりに機能することを確認します。

2. 偽のクライアント

Feignクライアントをセットアップするには、最初に Spring Cloud OpenFeignMaven依存関係を追加する必要があります。

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

その後、モデルのBookクラスを作成しましょう。

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

最後に、FeignClientインターフェイスを作成しましょう。

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

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

}

これで、RESTサービスから書籍のリストを取得するFeignClientができました。次に、統合テストを作成してみましょう。

3. WireMock

3.1. WireMockサーバーのセットアップ

BooksClient、をテストする場合は、 /booksエンドポイントを提供する模擬サービスが必要です。 クライアントはこのモックサービスに対して呼び出しを行います。この目的のために、WireMockを使用します。

それでは、 WireMockMaven依存関係を追加しましょう。

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

モックサーバーを構成します。

@TestConfiguration
public class WireMockConfig {

    @Autowired
    private WireMockServer wireMockServer;

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

}

これで、ポート9651で接続を受け入れる模擬サーバーが実行されました。

3.2. モックの設定

プロパティbook.service.urlWireMockServerポートを指すapplication-test.ymlに追加しましょう。

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

また、 /booksエンドポイントの模擬応答get-books-response.jsonを準備しましょう。

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

次に、 /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()))));
    }

}

この時点で、必要なすべての構成が整っています。 先に進んで、最初のテストを書いてみましょう。

4. 私たちの最初の統合テスト

統合テスト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);
    }

    // ...
}

この時点で、 / booksエンドポイントがBooksClientによって呼び出されます。

最後に、テストメソッドを追加しましょう。

@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. リボンとの統合

次に、リボンが提供する負荷分散機能を追加して、クライアントを改善しましょう。

クライアントインターフェイスで行う必要があるのは、ハードコードされたサービスURLを削除し、代わりにサービス名book-serviceでサービスを参照することです。

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

次に、NetflixリボンMaven依存関係を追加します。

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

最後に、 application-test.yml ファイルで、 book.service.url を削除し、代わりにリボンlistOfServersを定義する必要があります。

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

BooksClientIntegrationTestをもう一度実行してみましょう。 新しいセットアップが期待どおりに機能することを確認して、合格するはずです。

5.1. 動的ポート構成

サーバーのポートをハードコーディングしたくない場合は、起動時に動的ポートを使用するようにWireMockを構成できます。

このために、別のテスト構成 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()));
    }

}

この構成では、2つのWireMockサーバーがセットアップされ、それぞれが実行時に動的に割り当てられた異なるポートで実行されます。 さらに、2台のモックサーバーでリボンサーバーリストを構成します。

5.2. 負荷分散テスト

リボンロードバランサーが構成されたので、BooksClientが2つのモックサーバーを正しく切り替えることを確認しましょう。

@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のようなサービス検出システムを使用している場合。 BooksClientが期待どおりに機能することを確認する統合テストを作成する必要がありますそのような文脈でも。

この目的のために、テストコンテナとしてEurekaサーバーを実行します。 次に、モックbook-serviceを起動してEurekaコンテナに登録します。 そして最後に、このインストールが完了したら、それに対してテストを実行できます。

先に進む前に、TestcontainersNetflixEureka ClientMavenの依存関係を追加しましょう。

<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のセットアップ

Eurekaサーバーを起動するTestContainer構成を作成しましょう。

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

ご覧のとおり、上記のイニシャライザはコンテナを起動します。 次に、Eurekaサーバーがリッスンしているポート8761を公開します。

最後に、Eurekaサービスの開始後、eureka.client.serviceUrl.defaultZoneプロパティを更新する必要があります。 これは、サービス検出に使用されるEurekaサーバーのアドレスを定義します。

6.2. モックサーバーを登録する

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"));
    }
}

このコントローラーを登録するには、application-eureka-test.ymlspring.application.nameプロパティがであることを確認するだけです。 books-service、 BooksClientインターフェースで使用されるサービス名と同じです。

注: netflix-eureka-client ライブラリが依存関係のリストに含まれるようになったため、サービス検出にはデフォルトでEurekaが使用されます。 したがって、Eurekaを使用しない以前のテストで合格し続けるには、eureka.client.enabledをfalseに手動で設定する必要があります。 このように、ライブラリがパス上にある場合でも、 BooksClient はサービスの検索にEurekaを使用しようとせず、代わりにリボン構成を使用します。

6.3. 統合テスト

繰り返しになりますが、必要な構成要素がすべて揃っているので、それらをすべてまとめてテストしてみましょう。

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

}

このテストでは、いくつかのことが起こります。 それらを一つずつ見ていきましょう。

まず、EurekaContainerConfig内のコンテキスト初期化子がEurekaサービスを開始します。

次に、 SpringBootTest は、MockBookServiceConfigで定義されたコントローラーを公開するbooks-serviceアプリケーションを起動します。

EurekaコンテナとWebアプリケーションの起動には数秒かかることがあるためbooks-serviceが登録されるまで待つ必要があります。 これは、テストのsetUpで発生します。

そして最後に、testsメソッドは、BooksClientがEureka構成と組み合わせて実際に正しく機能することを確認します。

7. 結論

この記事では、 Spring Cloud FeignClientの統合テストを作成するさまざまな方法について説明しました。 WireMockを使用してテストした基本的なクライアントから始めました。 その後、リボンを使用した負荷分散の追加に移りました。 統合テストを作成し、FeignClientがRibbonが提供するクライアント側の負荷分散で正しく機能することを確認しました。 そして最後に、Eurekaサービスディスカバリをミックスに追加しました。 また、クライアントが引き続き期待どおりに機能することを確認しました。

いつものように、完全なコードはGitHubから入手できます。