はじめに
オープンソースへのコントリビュート、エンジニアなら一度はやってみたいと思いつつも、「自分なんかにできるのかな…」と尻込みしていました。
そんなことを考えながら色々調べているうちに、ひょんなことから Spring Cloud AWS プロジェクトに初めて小さなコントリビュートをすることができたので、その経験を簡単にまとめておきます。
Spring Cloud AWS とは?
Spring Cloud AWS は、Spring Boot から AWS のサービス(S3、SNS など)を簡単に扱えるようにしてくれるライブラリです。
Spring エコシステムで AWS サービスと連携する際によく使われているプロジェクトで、公式ドキュメントでも紹介されています。
なぜコントリビュートすることになったのか?
オープンソースに貢献してみたくて、まずは条件を決めて探し始めました。
- Java ベースのプロジェクトであること
- Spring エコシステムに属していること
- 公式ドキュメントに載っているくらい、ある程度の規模があるプロジェクトであること
そうやって探しているうちに、たまたま以下の Issue を見つけました!
good-for-first-time-contribution というラベルが付いていたので、すぐに「コントリビュートしてもいいですか?」とコメントしたところ、メンテナーの方が快く承諾してくれました。
何をやったのか?
問題
Issue の内容は、SnsTemplateTest が testcontainers/LocalStack を使っていないというものでした。
既存の SnsBatchTemplateTest を参考にして導入してほしいという話だったのですが、よく見てみると、すでに LocalStack を使っている他のテストクラスでも、それぞれ同じコンテナ設定コードを重複して持っていました。
@Testcontainers
class SnsSmsTemplateIntegrationTest {
private static SnsSmsTemplate snsSmsTemplate;
@Container
static LocalStackContainer localstack = new LocalStackContainer(
DockerImageName.parse("localstack/localstack:4.4.0")).withEnv("DEBUG", "1");
@BeforeAll
public static void createSnsTemplate() {
SnsClient snsClient = SnsClient.builder().endpointOverride(localstack.getEndpoint())
.region(Region.of(localstack.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("noop", "noop")))
.build();
snsSmsTemplate = new SnsSmsTemplate(snsClient);
}
Assertions.assertDoesNotThrow(() -> snsSmsTemplate.send("+385000000000", "Spring Cloud AWS got you covered!"));
await().untilAsserted(() -> {
String logs = localstack.getLogs(OutputFrame.OutputType.STDOUT, OutputFrame.OutputType.STDERR);
assertThat(logs).contains("Delivering SMS message to +385000000000: Spring Cloud AWS got you covered!");
});
}
@Container
static LocalStackContainer localstack = new LocalStackContainer
- インスタンスの宣言部
@BeforeAll
public static void createSnsTemplate() {
SnsClient snsClient = SnsClient.builder().endpointOverride(localstack.getEndpoint())
- ビルド時の共通設定コード
こういった部分に重複がありました。
解決
LocalstackContainerTest というインターフェースを新たに作成し、共通コードを一箇所にまとめました。
@Testcontainers(disabledWithoutDocker = true)
public interface LocalstackContainerTest {
@Container
LocalStackContainer LOCAL_STACK_CONTAINER = new LocalStackContainer(
DockerImageName.parse("localstack/localstack:4.4.0"));
static SnsClient snsClient() {
return applyAwsClientOptions(SnsClient.builder());
}
static SqsClient sqsClient() {
return applyAwsClientOptions(SqsClient.builder());
}
private static <B extends AwsClientBuilder<B, T>, T> T
applyAwsClientOptions(B clientBuilder) {
return clientBuilder
.region(Region.of(LOCAL_STACK_CONTAINER.getRegion()))
.credentialsProvider(credentialsProvider())
.endpointOverride(LOCAL_STACK_CONTAINER.getEndpoint())
.build();
}
}
既存の 3 つのテストクラスは implements LocalstackContainerTest を追加するだけで済み、それぞれが持っていたコンテナ宣言やクライアントビルダーのコードを削除しました。例えば SnsTemplateIntegrationTest の BeforeAll はこのように変わりました。
// Before
snsClient = SnsClient.builder().endpointOverride(localstack.getEndpoint())
.region(Region.of(localstack.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("noop", "noop")))
.build();
sqsClient = SqsClient.builder().endpointOverride(localstack.getEndpoint())
.region(Region.of(localstack.getRegion()))
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("noop", "noop")))
.build();
// After
snsClient = LocalstackContainerTest.snsClient();
sqsClient = LocalstackContainerTest.sqsClient();
SnsSmsTemplateIntegrationTest の場合はコンテナログを直接取得するコードがあったので、そちらも合わせて変更しました。
コードフォーマットを揃えようとフォーマッターを実行したら全部崩れたので、そっと取り消しました- テストに問題がなかったので PR を出しました
変更ファイルは全部で 4 つ、+94 行 / -70 行でした。
レビュー
PR を出してから約 2 週間後、メンテナーの方がレビューしてくれました。
リクエストは以下の 2 点でした。
-
.withEnv("DEBUG", "1")の削除 — もともとあったデバッグ用の環境変数を、共通インターフェースからは外してほしいとのこと -
@Containerアノテーションの使用 — 最初は@BeforeAllで手動でlocalstack.start()を呼んでいたが、@Containerを使えばTestcontainersがライフサイクルを自動管理してくれる
疑問と解決
2 つ目のリクエストを受けて、ひとつ疑問が湧きました。
@Container を使うと Testcontainers がテストごとにコンテナを再生成してしまうのでは? @BeforeAll で手動管理した方がパフォーマンス的に良いのでは?と思い、以下のように質問しました。
しばらく返信がなかったので、改めてじっくり読み直してみたところ、static フィールドに定義されているため、どのみちクラスごとに一度しか生成されないことに気づきました。
Java インターフェースのフィールドは自動的に static final になるので、@BeforeAll で手動管理するのとライフサイクルは同じです。
ちょっと恥ずかしかったので、そっとコメントを削除しました
マージ
- 修正を反映したら、すぐにマージされました
- これで自分もオープンソースコントリビューター!!!
ふりかえり
- 何かすごいことをやったわけではありません。リファレンスもあったし、シンプルなリファクタリングでした
- それでも、自分が書いたコードが実際に誰かが使っているプロジェクトに取り込まれたという事実は、なかなか嬉しかったです
- 大きくても小さくても、何かの一員になれた感覚とでもいうか
- 次はパフォーマンス改善やコア機能の実装など、もう少し深い部分にも挑戦してみたいです



