Spring BootのServiceConnectionをTestcontainers用にビルドするWireMock

Spring Boot 3.1 新しい ConnectionDetails と ServiceConnection 抽象化を追加することで 、Testcontainers のサポートを導入しました。 まだ読んでいない場合は、 Spring Boot Application Testing and Development with Testcontainersを確認してください。

バナー構築スプリングブーツサービステストコンテナwiremock1のための接続

次の Testcontainers コンテナの抽象化は、Spring Boot 3の一部としてすでにサポートされています。1:

  • Cassandraコンテナ
  • カウチベースコンテナ
  • エラスティックサーチコンテナ
  • Redisまたはopenzipkin/zipkinを使用したGenericContainer
  • JDBC データベースコンテナ
  • カフカコンテナ
  • MongoDBコンテナ
  • MariaDBコンテナー
  • MSSQLサーバーコンテナ
  • MySQLコンテナ
  • ネオ4jContainer
  • オラクルコンテナ
  • PostgreSQLコンテナ
  • RabbitMQコンテナ
  • レッドパンダコンテナ

Spring Boot 3.2。0 楨:

  • GenericContainer (symptoma/activemq または otel/opentelemetry-collector-contrib を使用)
  • OracleContainer (オラクル・フリー)
  • パルサーコンテナ

詳細については、Spring Boot 320の新機能をご覧ください。

Testcontainers を使用すると、統合テストを作成し、開発中のアプリケーションと対話するサービスとの間の統合が意図したとおりに機能することを確信できます。 WireMock は、APIの依存関係をシミュレートすることで役立ちます。 

このコンテキストでは、API 依存関係とは、アプリケーションが特定の機能またはデータに依存している外部サービスを指します。 この例では、API 依存関係として GitHub API を使用します。 このような依存関係を自分で実行することは、多くの場合、簡単ではありません。 現在、WireMockは独自のWireMock実装を提供しています。

独自の Spring Boot自動設定 を構築することは、チーム間で共有できる統合を実装する場合の一般的なユースケースです。 次の例では、GitHub GraphQL API と統合するための簡単な自動構成を記述します。

まず、WireMockのTestcontainers依存関係を宣言しましょう。

<dependency>
   <groupId>org.wiremock.integrations.testcontainers</groupId>
   <artifactId>wiremock-testcontainers-module</artifactId>
   <version>1.0-alpha-13</version>
</dependency

また、 maven-dependency-pluginを追加しましょう。

<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-dependency-plugin</artifactId>
   <executions>
       <execution>
           <id>copy</id>
           <phase>compile</phase>
           <goals>
               <goal>copy</goal>
           </goals>
           <configuration>
               <artifactItems>
                   <artifactItem>
                       <groupId>io.github.nilwurtz</groupId>
                   <artifactId>wiremock-graphql-extension</artifactId>
                       <version>0.7.1</version>
                       <classifier>jar-with-dependencies</classifier>
                   </artifactItem>
               </artifactItems>
               <outputDirectory>${project.build.directory}/test-wiremock-extension</outputDirectory>
           </configuration>
       </execution>
   </executions>
</plugin

プラグインは、テストの記述時に使用されるWireMock GraphQL Extensionをダウンロードします。

自動構成の構築

現在、次のアプローチは、アプリケーションにカスタム構成オプションを提供するための推奨される方法です。 @ConfigurationPropertiesを使用すると、環境変数、システムプロパティ、またはapplication.propertiesファイルやapplication.yamlファイルのプロパティを介してgithub.urlgithub.tokenの値を注入できるため、Springの組み込み構成機能を最大限に活用できます。

@ConfigurationProperties(prefix = "github")
public record GHProperties(String url, String token) {
}

GHProperties Bean は、以下の AutoConfiguration で確認できるように、@EnableConfigurationProperties を使用して必要な場所に注入できます。

@AutoConfiguration
@EnableConfigurationProperties(GHProperties.class)
public class GHAutoConfiguration {
   @Bean
   GraphQlClient ghGraphQlClient(GHProperties properties, WebClient.Builder webClientBuilder) {
       var githubBaseUrl = properties.url();
       var authorizationHeader = "Bearer %s".formatted(properties.token());
       return HttpGraphQlClient
                       .builder(webClientBuilder.build())
                       .url(githubBaseUrl + "/graphql")
                       .header("Authorization", authorizationHeader)
                       .build();
   }
}

次に、 src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsに自動構成を登録しましょう。

com.example.testcontainerswiremockexample.GHAutoConfiguration

GHAutoConfiguration インフラストラクチャを提供し、個別にパッケージ化および配布できます。 その後、JARは任意のプロジェクトで使用でき、アプリケーションの一部として使用できます。

自動設定の使用

GHServiceクラスを作成してGraphQlClientを使用してみましょう。これは、以前に実装された自動設定のおかげで自動的に注入されます。

@Service
public class GHService {
   private final GraphQlClient graphQlClient;
   public GHService(GraphQlClient ghGraphQlClient) {
       this.ghGraphQlClient = ghGraphQlClient;
   }
   public Mono<GitHubResponse> getStats(Map<String, Object> variables) {
       return this.graphQlClient.documentName("githubStats")
                       .operationName("Stats")
                       .variables(variables)
                       .retrieve("repository")
                       .toEntity(GitHubResponse.class);
   }
}

GraphQL クエリは、githubStats.graphqlまた、この操作は GitHubResponse エンティティを返します。

public record GitHubResponse(Issues issues, PullRequests pullRequests, Stargazers stargazers, Watchers watchers, Forks forks) {
   record Issues(int totalCount) {}
   record PullRequests(int totalCount) {}
   record Stargazers(int totalCount) {}
   record Watchers(int totalCount) {}
   record Forks(int totalCount) {}
}

次に、以下のコンテンツを src/main/java/graphql-documents/githubStats.graphqlで追加しましょう。

query Stats($owner: String!, $name: String!) {
   repository(owner: $owner, name: $name) {
       issues(states: OPEN) {
           totalCount
       }
       pullRequests(states: OPEN) {
           totalCount
       }
       stargazers {
           totalCount
       } watchers {
           totalCount
       } forks {
           totalCount
       }
   }
}

@DynamicPropertySourceによるテスト

@DynamicPropertySourceのおかげで、実行時にGHPropertiesで定義されたプロパティを指定できるため、テストには、Testcontainersを介して実行されるWireMockインスタンスに接続するための適切な構成が含まれます。

@SpringBootTest
@Testcontainers
class TestcontainersWiremockExampleApplicationTests {
   private static final Logger LOGGER = LoggerFactory.getLogger("wiremock");
   @Container
   static WireMockContainer wireMock = new WireMockContainer("wiremock/wiremock:3.2.0-alpine")
           .withMapping("graphql", TestcontainersWiremockExampleApplicationTests.class, "graphql-resource.json")
           .withExtensions("graphql",
                   List.of("io.github.nilwurtz.GraphqlBodyMatcher"),
                   List.of(Paths.get("target", "test-wiremock-extension", "wiremock-graphql-extension-0.7.1-jar-with-dependencies.jar").toFile()))
           .withFileFromResource("testcontainers-java.json", TestcontainersWiremockExampleApplicationTests.class, "testcontainers-java.json")
           .withLogConsumer(new Slf4jLogConsumer(LOGGER));
   @DynamicPropertySource
   static void properties(DynamicPropertyRegistry registry) {
       registry.add("github.url", wireMock::getBaseUrl);
       registry.add("github.token", () -> "test");
   }
   @Autowired
   private GHService ghService;
   @Test
   void contextLoads() {
       var variables = Map.<String, Object>of("owner", "testcontainers", "name", "testcontainers-java");
       StepVerifier.create(this.ghService.getStats(variables))
               .expectNext(new GitHubResponse(
                       new GitHubResponse.Issues(385),
                       new GitHubResponse.PullRequests(90),
                       new GitHubResponse.Stargazers(6560),
                       new GitHubResponse.Watchers(142),
                       new GitHubResponse.Forks(1295)))
               .verifyComplete();
   }
}

このテストでは、マッピング、拡張機能、および応答と共に WireMockContainer を定義します。 また、リクエストが一致しない場合にWireMockからフィードバックを取得するために、ログを設定します。

統合テストには、追加のリソースも必要です src/test/resources/com/example/testcontainerswiremockexamples/TestcontainersWiremockExampleApplicationTests/graphql-resource.json:

{
 "request": {
   "method": "POST",
   "url": "/graphql",
   "headers": {
     "Authorization": {
       "contains": "Bearer"
     }
   },
   "bodyPatterns": [
     {
       "equalToJson": "{\"query\":\"query Stats($owner: String!, $name: String!) {\\n    repository(owner: $owner, name: $name) {\\n        issues(states: OPEN) {\\n            totalCount\\n        }\\n        pullRequests(states: OPEN) {\\n            totalCount\\n        }\\n        stargazers {\\n            totalCount\\n        } watchers {\\n            totalCount\\n        } forks {\\n            totalCount\\n        }\\n    }\\n}\", \"operationName\": \"Stats\", \"variables\":{\"owner\":\"testcontainers\",\"name\":\"testcontainers-java\"}}"
     }
   ]
 },
 "response": {
   "status": 200,
   "bodyFileName": "testcontainers-java.json",
   "headers": {
     "Content-Type": "application/json"
   }
 }
}

そして、 src/test/resources/com/example/testcontainerswiremockexamples/TestcontainersWiremockExampleApplicationTests/testcontainers-java.json:

{
   "data": {
       "repository": {
           "issues": {
               "totalCount": 385
           },
           "pullRequests": {
               "totalCount": 90
           },
           "stargazers": {
               "totalCount": 6560
           },
           "watchers": {
               "totalCount": 142
           },
           "forks": {
               "totalCount": 1295
           }
       }
   }
}

ConnectionDetails サポートの追加

ConnectionDetailsを追加して、自動設定を最新化しましょう。この場合、 GHProperties と同じパラメータが公開されています。これは、WireMockでシミュレーションしているサービスに接続するために必要だからです。

public interface GHConnectionDetails extends ConnectionDetails {
   String url();
   String token();
}

GHConnectionsDetails は 2 つの実装を提供します。最初のものは引き続きプロパティに依存し、2番目のものは後で示します。

class PropertiesGHConnectionDetails implements GHConnectionDetails {
   private final GHProperties properties;
   public PropertiesGHConnectionDetails(GHProperties properties) {
       this.properties = properties;
   }
   @Override
   public String url() {
       return this.properties.url();
   }
   @Override
   public String token() {
       return this.properties.token();
   }
}

次に、自動設定の一部として、他の Bean が提供されていない場合に GHConnectionDetails Bean が作成されます。 PropertiesGHConnectionDetails は、他に提供されていない場合のデフォルトの実装になります。

@AutoConfiguration
@EnableConfigurationProperties(GHProperties.class)
public class GHAutoConfiguration {
   @Bean
   @ConditionalOnMissingBean(GHConnectionDetails.class)
   GHConnectionDetails ghConnectionDetails(GHProperties properties) {
       return new PropertiesGHConnectionDetails(properties);
   }
   @Bean
   GraphQlClient ghGraphQlClient(GHConnectionDetails connectionDetails, WebClient.Builder webClientBuilder) {
       var githubBaseUrl = connectionDetails.url();
       var authorizationHeader = "Bearer %s".formatted(connectionDetails.token());
       return HttpGraphQlClient
                       .builder(webClientBuilder.build())
                       .url(githubBaseUrl + "/graphql")
                       .header("Authorization", authorizationHeader)
                       .build();
   }
}

Spring BootのServiceConnectionにWireMock Testcontainersを追加する

続行する前に、 spring-boot-testcontainers 依存関係として宣言しましょう。

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-testcontainers</artifactId>
</dependency>

この時点では、実装は 1 つだけで、 PropertiesGHConnectionDetails です。 しかし、WireMockContainerに依存する GHConnectionDetails の 2 番目の実装を追加しましょう。

class WireMockContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory<WireMockContainer, GHConnectionDetails> {
   WireMockContainerConnectionDetailsFactory() {
   }
   protected GHConnectionDetails getContainerConnectionDetails(ContainerConnectionSource<WireMockContainer> source) {
       return new WireMockContainerConnectionDetails(source);
   }
   private static final class WireMockContainerConnectionDetails extends ContainerConnectionDetailsFactory.ContainerConnectionDetails<WireMockContainer> implements GHConnectionDetails {
       private WireMockContainerConnectionDetails(ContainerConnectionSource<WireMockContainer> source) {
           super(source);
       }
       @Override
       public String url() {
           return getContainer().getBaseUrl();
       }
       @Override
       public String token() {
           return "test-token";
       }
   }
}

src/main/resources/META-INF/spring.factoriesWireMockContainerConnectionDetailsFactoryを登録しましょう。

org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\
com.example.testcontainerswiremockexample.WireMockContainerConnectionDetailsFactor

WireMock Testcontainers ServiceConnectionの使用

これで、 WireMockContainer のプロパティの手動マッピングを削除し、 @ServiceConnectionに直接依存できます。

@SpringBootTest
@Testcontainers
class TestcontainersWiremockExampleApplicationTests {
   private static final Logger LOGGER = LoggerFactory.getLogger("wiremock");
   @Container
   @ServiceConnection
   static WireMockContainer wireMock = new WireMockContainer("wiremock/wiremock:3.2.0-alpine")
           .withMapping("graphql", TestcontainersWiremockExampleApplicationTests.class, "graphql-resource.json")
           .withExtensions("graphql",
                   List.of("io.github.nilwurtz.GraphqlBodyMatcher"),
                   List.of(Paths.get("target", "test-wiremock-extension", "wiremock-graphql-extension-0.7.1-jar-with-dependencies.jar").toFile()))
           .withFileFromResource("testcontainers-java.json", TestcontainersWiremockExampleApplicationTests.class, "testcontainers-java.json")
           .withLogConsumer(new Slf4jLogConsumer(LOGGER));
   @Autowired
   private GHService ghService;
   @Test
   void contextLoads() {
       var variables = Map.<String, Object>of("owner", "testcontainers", "name", "testcontainers-java");
       StepVerifier.create(this.ghService.getStats(variables))
               .expectNext(new GitHubResponse(
                       new GitHubResponse.Issues(385),
                       new GitHubResponse.PullRequests(90),
                       new GitHubResponse.Stargazers(6560),
                       new GitHubResponse.Watchers(142),
                       new GitHubResponse.Forks(1295)))
               .verifyComplete();
   }
}

そして、そのように、テストまたは開発中にこれらの外部サービスに接続するためのサポートを Testcontainers によって実現できます。 ソースコードは GitHubにあります。 

結論

TestcontainersとWireMockを使用してテストおよび開発中にAPIの動作をシミュレートすることは、ソフトウェア開発チームに多くのメリットを提供する強力で効率的なアプローチです。 

この組み合わせにより、エンジニアリングチームは、実際のAPIインタラクションを厳密に反映した制御された分離されたテスト環境を作成し、堅牢で信頼性の高いテストプラクティスを促進できます。 また、Spring Boot の ConnectionDetails と ServiceConnection の抽象化により、Testcontainers の統合と開発者エクスペリエンスはシームレスになります。

さらに詳しく