ScalaTestでTestcontainersのコンテナを使い回してみた

ScalaTestにおけるTestcontainersコンテナの効率的な利用方法

Katsuya Kubo
nextbeat-engineering
10 min readMar 19, 2024

--

画像引用元:https://github.com/testcontainers/testcontainers-scala/blob/master/logo.png

はじめに

こんにちは、ネクストビートでエンジニアをしている久保です。

Testcontainers は、テストコード中でDockerコンテナを使うためのライブラリです。
DBやファイルシステムの部分をクリーンな状態でテストすることができ、様々な言語に対応しています。
今回はそのTestcontainersを使って、ScalaTestでコンテナを使い回す方法を紹介します。
testcontainers-java のScalaラッパーであり公式でサポートされている testcontainers-scala を使います。

testcontainers-scalaはScalaTestをラップしたトレイトも提供しており使い勝手が良いです。基本的な使用方法は割愛します。

なぜコンテナを使い回すのか

Testcontainersは通常、テストケースやテストスイートごとにコンテナを起動します。
そうすることで、テストケース・スイート間で影響を及ぼさずに済むのが利点です。
しかしコンテナの起動には時間がかかるため、テストが増えるたびに実行時間が長くなってしまいます。 そのため、ライブラリのページでも「sbtのIntegrationTestでの実行を推奨する」と言及されています。

Although Testcontainer tests are mere unit tests, it’s best (although not mandatory) to use them as sbt integration tests since they spawn a separate environment, can be slower and require specific configuration.

機械翻訳:
「Testcontainersのテストは単なるユニットテストですが、別の環境を起動するため、遅くなり、特定の設定が必要になるため、sbtの統合テストとして使用することが最適です。」

つまり頻繁に実行するユニットテストとしてではなく、例えばリリース前の検証の段階でCIで実行する等が推奨されています。
ただ、もし開発段階で気軽に実行してバグの混入を早期に検知できるのであればそれに越したことはないなと思いました。
同じニーズはやはりあるようで、実験的な機能としてですがコンテナを使い回す withReuse というオプションが提供されています。

しかしこの withReuse オプションは testcontainers-scala の ScalaTest ラッパーでは未対応です( 2024/03/14 時点)

At the current moment, there is no supported way for doing this. It’s because scalatest doesn’t provide any functionality for sharing data like this between tests.

機械翻訳:
「現在のところ、これを行う方法はサポートされていません。ScalaTest はテスト間でこのようなデータを共有する機能を提供していないからです。」

そこで、今回は withReuse を使わずに、コンテナを使い回す方法を紹介します。

Singleton Container Pattern

本家のtestcontainersでも Javaでの実装例 が紹介されている方法です。
しかしScalaにstaticは無いのでそれに相当する シングルトンオブジェクト で実装する必要があります。

注:コード例はScala3で書かれていますが2.13でも適用できます

① シングルトンオブジェクトの定義

トップレベルにあるオブジェクト(=シングルトンオブジェクト)として定義することでプロセス内で一つのコンテナを使い回すことができます。さらにこれを各テストスイートに提供するためのトレイトを定義します。

② 汎用テストトレイトの定義

このトレイトをテストスイートでextends、もしくはmixinすることで、テストケース間でコンテナを使い回すことができます。

③ テストスイートで利用する

これと同じようなテストスイートをもう一つ書いてみましたが、実際にDocker Desktopで起動しているコンテナを見ると一つになっていることがわかります。

※ ryukコンテナはTestcontainersのライフサイクル管理用のコンテナです。

注意点

この Singleton Container Pattern にはいくつか注意点があります。

① テストデータの初期化が必要
コンテナを使い回すことで他のテスト実行時のデータが残ってしまい、そのままだとクリーンな状態でテストが行えません。
そのため明示的にDBの初期化を行う必要があります。

今回の例では SingletonMySQLContainerSpec.scalabeforeAllScriptUtils.runInitScriptを使って下記のようなクエリを読み込んで実行しています。

DROP DATABASE IF EXISTS `test`;
CREATE DATABASE `test`;

② 複数のテストを並列実行できない
同じコンテナを使い回す都合上、各テストケースで行われたDBの変更がお互いに影響しあい、実質的に並列実行できなくなります。
限られた条件下では可能かもしれませんが、この方法を採用する場合は下記のオプションを指定してテストの並列実行を無効化する必要があります。

// build.sbt
Test / fork := true,

当初、並列の無効化なら Test / parallelExecution := falseなのでは?と思い設定したのですがこれだと同一プロジェクト内のテストは直列実行されるのですが、サブプロジェクト間のテストは並列実行されてしまいます
今回のサンプルコードは単一プロジェクトですが現場ではマルチプロジェクトになることも多いかと思います。
そこで代わりに指定するのが Test / fork := true です。
このオプションは指定するとテスト時に新たなJVMプロセスを立ち上げて実行してくれるのですが、実はその中で実行されるテストは サブプロジェクトも含めた全てのテストにおいて直列で実行されます。
そのためこちらを指定することで全てのテスト間で意図しない干渉を防ぐことができました。
ここは私も完全に理解できていないのですがこちらの記事を参考にさせていただきました。

ただこの並列テストの問題は通常のTestcontainersの利用方法でも少なからず抱えている問題で、例えばテストスイートごとにコンテナを立ち上げる場合はテストスイート内のテストケース同士が干渉してしまうのでテストケースの並列実行には「レコードのIDを被らせない」等の工夫が必要になります。
テストケースごとにコンテナを立ち上げることもできますが、それこそコンテナの起動時間がさらに伸びてしまいます。

またこの Test / fork := trueの設定ではコンテナテスト的に嬉しい点もあり、 「testコマンド終了時に自動的にコンテナが終了」 してくれます。
これはforkを有効にすることでtest実行時に新たなJVMプロセスが作られ、テスト終了と共にそのJVMプロセスも終了するためです。
参考: https://www.scala-sbt.org/1.x/docs/Forking.html

これは 「開発時に繰り返しテストを実行したい」という要件ととても相性が良い です。
「全テストが終了したらコンテナを終了する」という処理を独自で書くのは難易度が高い気がするのでこれはありがたい副次的効果でした。

おわりに

本記事では Singleton Container Pattern を用いて Testcontainers のコンテナを使い回す方法を紹介しました。
本来の利用方法とは異なりますが、同じニーズを抱えていらっしゃる方の参考になれば幸いです。

今回用いたソースは実際に動くものをこちらに用意しています。https://github.com/katzkb/tc-scala-sample

We are hiring!

本記事をご覧いただき、ネクストビートの技術や組織についてもっと話を聞いてみたいと思われた方、カジュアルにお話しませんか?

・今後のキャリアについて悩んでいる
・記事だけでなく、より詳しい内容について知りたい
・実際に働いている人の声を聴いてみたい

など、まだ転職を決められていない方でも、ネクストビートに少しでもご興味をお持ちいただけましたら、ぜひカジュアルにお話しましょう!

🔽申し込みはこちら
https://hrmos.co/pages/nextbeat/jobs/1000008

また、ネクストビートについてはこちらもご覧ください。

🔽エントランスブック
https://note.nextbeat.co.jp/n/nd6f64ba9b8dc

--

--